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!