Justin
Sorry for the delay - a very long drive across Europe to Vienna where I have been living for the last 7 years but now moving to S of France. Life’s a bitch (
Background
We are in enterprise land here, not Joe Public on the web …
I have a json object comprising a list of ‘entries’ which can be one of a few different ‘types’, each of whci requires different business logic. This json is used to generate the user interface via stache templates. Before this the ‘raw’ json has to be cooked and transformed into a hierarchy as an entry may contain entries and it’s turtles all the way down, at least for about 3 levels. json becomes an observable via can.Map
The user presentation takes the form of a list of form entry ‘questions’ based on not only the usual html elements but additional constructs that allow drag and drop sorting of lists ( a so called ‘rank), drag and drop categorisation mechanism so the user can drag from a list and drop into various categories.
Edits to this interface create events which are monitored via can.Controls
Think of an online survey as a concrete example where each entry is one of the survey questions and questions are grouped into sections and maybe subsections each with their own presentation idiosyncrasies. It’s way beyond the SurveyMonkey simple stuff and allows for dependency management and many additional bits of functionality. If q.1 is ‘ please enter marital status’ with single/married/divorced as possible choices and q.2 as ‘how long have you been marries’, then q.2 needs to be disabled unless q.1 answer ==married. This is what I mean by simple dependency.
Interface edits trigger 2-way binding usually to an ‘answer’ prop eg
<input name=“testfield” {($value)}=“answer"/>, with a bit of supporting JS to deal with checkboxes, selects etc so that they also generate an ‘answer’ prop.
I also need to be able to figure out which object in the observable heirarchy is related to any given prop change. To do this I create ‘helper’ objects which as hashes of all entries in the hierarchy based on maybe fieldname or entry’s uuid as keys. That way I can instantly get the obj given a has key regardless of where it is in a maybe deep hierarchy.
Bottom line : I need to monitor changes to the ‘answer’ prop in an efficient manner as changes have many consequences … Changes to ‘answer’ not only trigger server saves may also involve calculating a score (for this question), maintaining total score, displaying question status via a ‘traffic lights’ red/green/amber) display, counting unanswered questions, determining if the questions are all ‘finished’ in which case we might move on to a workflow / approval and so on.
Define
At app init time I run an initHandlers() function containing a series of IIFEs like the following
(function answer(surveyObs) {
// delegate listener for changes to normal and matrix question answers
surveyObs.entries.delegate('*.answer', 'set', handler); // a normal question
surveyObs.entries.delegate('*.entries.*.answer', 'set', handler); // a matrix question, deeper in the hierarchy
function handler(ev, newVal, oldVal, prop) {
var entry = ev.target, json;
// validate if applicable and do not continue if we fail
if (entry.validation && newVal!=='') {
validate(entry, oldVal);
}
saveToServer(surveyObs, entry, json);
}
})(surveyObs);
The definition of the delegate wildcard tells me what is changing without me having to inspect the target and ask what type it is.
Another example in the same mould
(function multiple(surveyObs) {
// delegate listener for changes to multiple choice changes
// except for rank and buckets which are handled by a compute
surveyObs.entries.delegate('*.choices', 'set', handler);
surveyObs.entries.delegate('*.selected', 'set', handler);
function handler(ev, newVal, oldVal, prop) {
// this will fire for multiple selects, checkbox , rank and buckets
var entry, answer;
// do the business
Without delegate I have to listen for all changes to the observable, inspect the ‘attr’ then despatch to whichever subroutine to deal with the business logic.
In addition to the above observable handlers I have a series of can.computes doing other stuff, each compute being setup by initComputes() when the app load eg
(function state(surveyObs, realQuestions) {
// at the mom this is the best way I can find to do this
var groups, count;
groups = utils.filterEntries(surveyObs.entries, 'isGroup');
count = can.compute(function() {
// ok run through every object’s status, figure out
// and set the traffic lights
// whilst we are about it we calculate the total completed
// do the business of figuring out the state of this question
// which might be not answered, answered, part answered, error, warning etc etc
}).length+groupsCompleted;
});
surveyObs.summary.attr('completedCount', count);
})(surveyObs);
Some of the computes take the alternative format :
(function ranking(surveyObs) {
// whenever we rank/unrank we need to compute the 'answer' for the rank question
var rankings = utils.filterEntries(surveyObs.entries, 'tagtype', 'rank');
_.each(rankings, function(o) {
o.compute('answer').bind('change', function(ev, newVal, oldVal) {
var answers, rankings = [], ranker = {}, ranked = [];
// compute an answer from a user sorted collection of <li>
// used jQueryUI’s sortable ...
});
});
})(surveyObs);
ALL OF THIS WORKS GREAT !!
Only issue was as noted in my forum entry a couple of things in delegate that don’t work as expected …
Define is not something that I’m currently using but I can certainly see value in integrating it in … time …
Sorry for the length of this, I REALLY appreciate the work that you guys are doing in this framework
BTW If you guys allow ‘guest’ blog entries than I am considering writing up some of my learning experience with canjs ??? FAR too few people seem to be using this framework despite it’s maturity and holistic approach.
Regards
Ron Yuen