<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 "> Bitovi Blog - UX and UI design, JavaScript and Frontend development
Loading

Weekly Widget 1 - TreeCombo

Weekly Widget 1 - TreeCombo

Justin Meyer

Justin Meyer

Twitter Reddit

I, @justinbmeyer, am going to post a weekly widget made with CanJS. I hope to continue this for as long as I have widgets to write. If you want to see something, please tweet it to @canjs. These posts are going to be quick and dirty. Eventually, I will put these up on CanJS's recipe page with a full description.

I'm starting with the TreeCombo below:

What it does

The tree combo allows you to select items within and navigate through a hierarchial collection of data. Click "→" to see child items. Click the breadcrumb navigation at the top to return to parent items.

It also displays a list of the items the user has selected under "You've selected:".

How it works

A TreeCombo control is created like:

new TreeCombo("#treeCombo", {
  items: new can.Observe.List(data),
  title: "All Content",
  selected: selected
});

Where:

  • items - an observable list of hierarchial data
  • title - the text for the "return to top level text"
  • selected - an observable list of items selected by the widget

The trick is to maintain the state of the items the user has navigated through as this.options.breadcrumb. When a user clicks the "→" button, I add the corresponding item to breadcrumb with:

this.options.breadcrumb.push(el.closest('li').data('item'));

When someone clicks a breadcrumb, I remove all the items in breadcrumb after the clicked breadcrumb item:

".breadcrumb li click": function(el){
  var item = el.data('item');
  // if you clicked on a breadcrumb li with data
  if(item){
    // remove all breadcrumb items after it
    var index = this.options.breadcrumb.indexOf(item);
    this.options.breadcrumb.
      splice(index+1, 
             this.options.breadcrumb.length-index-1)
  } else {
    // clear the breadcrumb
    this.options.breadcrumb.replace([])
  }
}

Prior to this, using live-binding, I've setup the page to automatically update when breadcrumb changes. The items selectable to the user are always the last item in the breadcrumb's children. I made a compute to calculate this with:

var selectableItems = can.compute(function(){

  // if there's an item in the breadcrumb
  if(this.options.breadcrumb.attr('length')){

    // return the last item's children
    return this.options.breadcrumb
       .attr(""+(this.options.breadcrumb.length-1))
       .attr('children');
  } else{

    // return the top list of items
    return this.options.items;
  }
}, this);

Next, I render the template which list each breadcrumb item and selectableItems item:

<ul>
  <% breadcrumb.each(function(item){ %>
    <li <% (el)-> el.data('item', item) %>> <%= item.attr('title') %> </li>
  <%})%> 
  <% selectableItems().each(function(item){ %>
  <li class='<%= selected.indexOf(item) >= 0 ? “checked”:”” %>’ <%=(el)-> el.data(‘item’,item) %> ></p>
  <pre><code>        <input type="checkbox"
                 <%= selected.indexOf(item) >= 0 ? 
                     "checked":""%>>

          <%= item.attr('title') %>

          <%if(item.children && item.children.length){ %>
              <button class="showChildren">→</button>
          <%}%>
      </li>
  <% }) %>
</ul>

Selection is handled by adding and removing items in this.options.selected:

".options li click": function(el){
  // toggles an item's existance in the selected array
  var item = el.data('item'),
      index = this.options.selected.indexOf(item);
  if(index === -1 ){
    this.options.selected.push(item);
  } else {
    this.options.selected.splice(index, 1) 
  }
}

The live-binding template above includes:

<li class='<%= selected.indexOf(item) >= 0 ? "checked":"" %>'>

Which handles adding a "checked" class that highlights checked rows.

Finally, at the end of the script, I use another template to show what's selected:

$("#selected").html(can.view('selectedEJS', selected));

This also changes auto-magically via live-binding.

Conclusion

Seriously ... how awesome is this! Observes and computes can make formerly tricky widgets almost stupidly easy. The hard part is determining how you want to represet state.

Give me some suggestions for next week!