Here's how to build an app with something like CanJS and AMD-like modules (using steal). For this example app, consider a tree-like nested grid where you can select locations and get more information. The app might look something like:
IL | |
Chicago | |
Naperville | |
California | |
San Fran | |
LA |
Naperville X
Naperville details |
Chicago X
Chicago details |
But, you can only select items of the same "level" (items that have the same parent). And, you can also remove an item's details by clicking the X.
So there are rules about what can be selected. Lets make that as an Observable list:
steal('can', function(can){
return can.Observe.List({
add: function(location){
if( this.length ) {
for(var i = 0 ; i < this.length ; i++ ){
if( this[i].parentId !== location.parentId ) {
this.splice(0, this.length);
break;
}
}
}
this.push(location)
},
remove: function(location){
var index = this.indexOf(location)
index >= 0 && this.splice(index, 1)
},
toggle: function(location){
var index = this.indexOf(location)
index >= 0 ? this.remove(location) : this.add(location)
}
})
})
This list's add / remove / toggle methods only allow locations with the same parentId to be in the list.
There are 2 ways to do create the widgets:
- They accept an observe, listen to changes in it and make changes on it.
- They produce events and have methods that allow you to toggle their internal state.
#2 option is more reusable, but depending on the situation #1 is just so easy that it's worth it. Lets explore each one:
To create the tree and details panel, you might do:
steal('tree','details','selected_list',
function(Tree, Details, SelectedList){
var selectedList = new SelectedList()
new Tree("#locations",{
selected: selectedList,
items: Location.findAll()
});
new Details("#details",{selected: selectedList });
})
To create the tree and details panel, you might do:
steal('tree','details','selected_list',
function(Tree, Details, SelectedList){
var selectedList = new SelectedList();
// create and hookup the Tree
var tree = new Tree("#locations",{items: Location.findAll()});
// when someone selects an item, toggle it in the list
$("#locations").on(".item","selected", function(ev, location){
selectedList.toggle(location)
})
// when the selectedList is updated, update the tree
selectedList.bind('add', function(ev, items, index){
tree.activate(index)
}).bind('remove', function(ev, items, index){
tree.deactivate(index)
});
// create an hookup Details
var details = new Details("#details");
// when a detail's X is clicked, remove
$("#details").on(".item","removeSelect", function(ev, location){
selectedList.remove(location);
});
selectedList.bind("length", function(){
details.update(selectedList)
})
})
Depending on how you intend to glue the widgets together, their code will change. I'll show the details panel for each case:
steal('can', './details.ejs',function(can, detailsEJS){
return can.Control({
init: function(){
this.element.html("Empty")
},
"{selected} length":function(selected){
this.element.html( detailsEJS(selected) )
},
".item .destroy click": function(el, ev){
var itemEl = el.closest('.item')
this.options.selected.remove( itemEl.data('item') )
}
})
});
steal('can', './details.ejs',function(can, detailsEJS){
return can.Control({
init: function(){
this.element.html("Empty")
},
"update":function(selected){
this.element.html( detailsEJS(selected) )
},
".item .destroy click": function(el, ev){
var itemEl = el.closest('.item')
itemEl.trigger("removeSelect", itemEl.data('item') )
}
})
});
Using this pattern you can build bigger and bigger functionality. But, when you build layers above this, we also organize the code in step #2 into a control. For example:
steal('can','jquery',
'tree','details','selected_list',
function(can, $, Tree, Details, SelectedList){
return can.Control({
init: function(){
this.options.selectedList = new SelectedList();
// create and hookup the Tree
this.tree = new Tree(this.element.find(".locations"),{
items: Location.findAll()
});
this.details = new Details("#details");
this.on();
},
".locations .item .selected": function(el, ev, location){
selectedList.toggle(location)
},
"{selected} add": function(selected, ev, items, index){
this.tree.activate(index);
this.details.update(selected)
},
"{selected} remove": function(selected, ev, items, index){
this.tree.deactivate(index)
this.details.update(selected)
},
".details .item removeSelect", function(el, ev, location){
selectedList.remove(location);
}
})