3
\$\begingroup\$

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>

\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.