I created a widget to handle multiple selections in CoffeeScript. There's a working attached snippet that uses the compiled JavaScript (Stack Snippets don't support CoffeeScript), which is available to see. There are two classes: DocList
and Doc
(I didn't use Document
fearing a name clash).
The DocList
class keeps track of how many documents it has, how many have been selected. It handles displaying the right controls, selecting and deselecting all, and maintaining the state of its checkbox.
class DocList
numOfcheckedBoxes: 0
numOfDocs: 0
constructor: (@list) ->
@docs = (new Doc(doc, @) for doc in @list.find('[data-behaviour~="doc"]'))
@numOfDocs = @docs.length
@filters = @list.find '[data-behaviour~="documentListFilters"]'
@actions = @list.find '[data-behaviour~="documentListActions"]'
@showOrHideActions()
@selectAll = @list.find '[data-behaviour~="documentListSelectAll"]'
# this displays how mant docs are selected to the user
@documentsSelected = @list.find '[data-behaviour="documentsSelected"]'
@setEvents()
setEvents: ->
@selectAll.on 'click', =>
if @selectAll.prop 'checked'
@checkAll()
else
@uncheckAll()
checkAll: ->
doc.check() for doc in @docs
@updateSelectedCount @numOfDocs
uncheckAll: ->
doc.uncheck() for doc in @docs
@updateSelectedCount 0
handleCheckBoxEvent: (checked) ->
if checked
@updateSelectedCount @numOfcheckedBoxes + 1
else
@updateSelectedCount @numOfcheckedBoxes - 1
updateSelectedCount: (count) ->
@numOfcheckedBoxes = count
@documentsSelected.html "#{@numOfcheckedBoxes} #{@documentsSelected.data 'label'}"
if @numOfcheckedBoxes
if @numOfcheckedBoxes < @numOfDocs
@selectAll.prop 'indeterminate', true
else
@selectAll.prop 'indeterminate', false
@selectAll.prop 'checked', true
else
@selectAll.prop 'indeterminate', false
@selectAll.prop 'checked', false
@showOrHideActions()
showOrHideActions: ->
if @numOfcheckedBoxes
@filters.hide()
@actions.show()
else
@filters.show()
@actions.hide()
The Doc
class maintains a reference to its parent so that, when its checked, it can tell its parent it's been checked (to update counts and state).
class Doc
constructor: (doc, @list) ->
@doc = $(doc)
@checkbox = @doc.find 'input[type="checkbox"]'
@setEvents()
setEvents: ->
@checkbox.on 'click', =>
@handleClick()
handleClick: ->
checked = @checkbox.prop 'checked'
if checked
@doc.addClass 'document--selected'
else
@doc.removeClass 'document--selected'
@list.handleCheckBoxEvent(checked)
check: ->
@checkbox.prop 'checked', true
@handleClick()
uncheck: ->
@checkbox.prop 'checked', false
@handleClick()
And here's the initialization:
$(document).on 'ready', ->
new DocList $('[data-behaviour~="documentList"]')
Some notes:
I don't like how the updateSelectedCount()
works. Perhaps increment or decrement would be better here? But that might complicate other logic. Another issue is the conditionals to make sure the selectAll
checkbox keeps a correct state (indeterminate, checked, etc...). It looks confusing. I like how I'm handling dependency injection by passing the list to its child doc, but is there a better way?
Some additional notes. If this were Ruby, the checkAll()
, uncheckAll()
, and updateSelectedCount()
in the DocList
class would be private methods. As well as handleClick()
in the Doc
class. Alas, it doesn't seem that there's an easy way to define and use private methods in CoffeeScript, at at least it's beyond my understanding.
var Doc, DocList;
DocList = (function() {
DocList.prototype.numOfcheckedBoxes = 0;
DocList.numOfDocs = 0;
function DocList(list) {
var doc;
this.list = list;
this.docs = (function() {
var i, len, ref, results;
ref = this.list.find('[data-behaviour~="doc"]');
results = [];
for (i = 0, len = ref.length; i < len; i++) {
doc = ref[i];
results.push(new Doc(doc, this));
}
return results;
}).call(this);
this.numOfDocs = this.docs.length;
this.filters = this.list.find('[data-behaviour~="documentListFilters"]');
this.actions = this.list.find('[data-behaviour~="documentListActions"]');
this.showOrHideActions();
this.selectAll = this.list.find('[data-behaviour~="documentListSelectAll"]');
this.documentsSelected = this.list.find('[data-behaviour="documentsSelected"]');
this.setEvents();
console.log('inited');
}
DocList.prototype.setEvents = function() {
return this.selectAll.on('click', (function(_this) {
return function() {
if (_this.selectAll.prop('checked')) {
return _this.checkAll();
} else {
return _this.uncheckAll();
}
};
})(this));
};
DocList.prototype.checkAll = function() {
var doc, i, len, ref;
ref = this.docs;
for (i = 0, len = ref.length; i < len; i++) {
doc = ref[i];
doc.check();
}
return this.updateSelectedCount(this.numOfDocs);
};
DocList.prototype.uncheckAll = function() {
var doc, i, len, ref;
ref = this.docs;
for (i = 0, len = ref.length; i < len; i++) {
doc = ref[i];
doc.uncheck();
}
return this.updateSelectedCount(0);
};
DocList.prototype.handleCheckBoxEvent = function(checked) {
if (checked) {
return this.updateSelectedCount(this.numOfcheckedBoxes + 1);
} else {
return this.updateSelectedCount(this.numOfcheckedBoxes - 1);
}
};
DocList.prototype.updateSelectedCount = function(count) {
this.numOfcheckedBoxes = count;
this.documentsSelected.html(this.numOfcheckedBoxes + " " + (this.documentsSelected.data('label')));
if (this.numOfcheckedBoxes) {
if (this.numOfcheckedBoxes < this.numOfDocs) {
this.selectAll.prop('indeterminate', true);
} else {
this.selectAll.prop('indeterminate', false);
this.selectAll.prop('checked', true);
}
} else {
this.selectAll.prop('indeterminate', false);
this.selectAll.prop('checked', false);
}
return this.showOrHideActions();
};
DocList.prototype.showOrHideActions = function() {
if (this.numOfcheckedBoxes) {
this.filters.hide();
return this.actions.show();
} else {
this.filters.show();
return this.actions.hide();
}
};
return DocList;
})();
Doc = (function() {
function Doc(doc, list) {
this.list = list;
this.doc = $(doc);
this.checkbox = this.doc.find('input[type="checkbox"]');
this.setEvents();
}
Doc.prototype.setEvents = function() {
return this.checkbox.on('click', (function(_this) {
return function() {
return _this.handleClick();
};
})(this));
};
Doc.prototype.handleClick = function() {
var checked;
checked = this.checkbox.prop('checked');
if (checked) {
this.doc.addClass('document--selected');
} else {
this.doc.removeClass('document--selected');
}
return this.list.handleCheckBoxEvent(checked);
};
Doc.prototype.check = function() {
this.checkbox.prop('checked', true);
return this.handleClick();
};
Doc.prototype.uncheck = function() {
this.checkbox.prop('checked', false);
return this.handleClick();
};
return Doc;
})();
$(document).on('ready', function() {
return new DocList($('[data-behaviour~="documentList"]'));
});
// ---
// generated by coffee-script 1.9.2
label {
margin-right: 10px;
}
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="panel panel-default" data-behaviour="documentList">
<div class="panel-heading">
<ul class="list-inline" data-behaviour="documentListFilters">
<li>Order by</li>
<li><a href="#">Recent</a></li>
<li><a href="#">Oldest</a></li>
</ul>
<ul class="list-inline" data-behaviour="documentListActions">
<li><input type="checkbox" data-behaviour="documentListSelectAll" /></li>
<li><strong data-behaviour="documentsSelected" data-label="selected"></strong></li>
<li><a href="#">Change Status</a></li>
<li><a href="#">Change Phase</a></li>
</ul>
</div>
<ul class="list-group">
<li data-behaviour="doc" class="list-group-item">
<label class="pull-left">
<input type="checkbox" />
</label>
<a class="text-link" href="AEROGAMI.png">AEROGAMI-1.png</a>
</li>
<li data-behaviour="doc" class="list-group-item">
<label class="pull-left">
<input type="checkbox" />
</label>
<a class="text-link" href="AEROGAMI.png">AEROGAMI-1.png</a>
</li>
<li data-behaviour="doc" class="list-group-item">
<label class="pull-left">
<input type="checkbox" />
</label>
<a class="text-link" href="AEROGAMI.png">AEROGAMI-1.png</a>
</li>
</ul>
</div>