Motivation
In relation to the recent discussion about the future of Blaze, which has taken place here:
and here:
and probably in many other places, I would like to add my “2 cents” and try show by example how flexible Blaze may become when used in a certain way.
My goal here is not to convince anyone that Blaze is better than some other rendering framework, because I don’t want to discuss about opinions. I just want to prove that Blaze is good enough for building reusable components by showing you a very simple example.
The Example
<template name="myListView">
<h2>Selected items: {{list.getSelectedItemsCount}}</h2>
{{#list}}
{{#each myListItems}}
{{#list.item}}
{{name}}
{{/list.item}}
{{/each}}
{{/list}}
</template>
The above snippet explains how I would like to use my list
component.
There are at least two non-trivial parts here:
- Accessing the component state from outside, i.e.
list.getSelectedItemsCount
. - The
list.item
subcomponent must be aware of it’s parent state, or vice versa, to render itself properly, i.e. it needs to know whether it’s selected or not.
I am aware there are a lot of useful packages that would probably help a lot
to implement this component, e.g.
to mention a few, but the idea here is to find a “pure Blaze” solution without using
any additional abstraction layer.
The Solution
So let’s start with our templates, which look rather simple.
<template name="selectableList">
<div class="selectableList">
{{> Template.contentBlock}}
</div>
</template>
<template name="selectableListItem">
<div class="selectableListItem {{#if isSelected}}selected{{/if}}" data-action="{{if isSelected}}select{{else}}deselect{{/if}}">
{{> Template.contentBlock}}
</div>
</template>
The important magic is happening below. I am forced to use some pseudo-code here, because things like Template.prototype.extend
are not standard, but hopefully it’s clear what they should mean.
/**
* Creates an instance of our SelectableList component.
*/
function SelectableList (options) {
// the template which we will use to render this component
var template = Template.selectableList.extend();
// the component state is right here
var selected = [];
var selectedDep = new Tracker.Dependency();
// NOTE: we can use options object to customize the entire
// template creation process, e.g. add custom event
// handlers, behaviors and even css classes
template.events({
'click [data-action=select]': function () {
selected.push(this._id);
selectedDep.changed();
},
'click [data-action=deselect]': function () {
selected.splice(_.indexOf(selected, this._id), 1);
selectedDep.changed();
},
});
// custom component API
return _.extend(template, {
item: _.memoize(function () {
return Template.selectableListItem.extend({ helpers: {
isSelected: function () {
selectedDep.depend();
return _.indexOf(selected, this._id) >= 0;
}
} });
}),
getSelectedIds: function () {
selectedDep.depend();
return selected.slice();
},
getSelectedItemsCount: function () {
selectedDep.depend();
return selected.length;
},
});
};
The last thing we need to do is to make the component accessible at our parent template level:
Template.myListView.onCreated(function () {
this.components = {};
this.components.list = new SelectableList();
});
Template.myListView.helpers({
list: function () {
return Template.instance().components.list;
}
});
which can be shorten to
Template.myListView.component('list', function () {
return new SelectableList();
});
if we implement Template.prototype.component
properly: