A pure-Blaze pattern for creating reusable components

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:

  1. Accessing the component state from outside, i.e. list.getSelectedItemsCount.
  2. 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:

5 Likes

We’ve got an alternate idea in the guide: http://guide.meteor.com/blaze.html#reusable-components

Can you take a look at that and see what you would change?

I think that to some extent your guide is covering the same ground, and even though I may not like some patterns - e.g. using helpers to pass callbacks to subcomponents - I think we would agree about most of the things.

There are - however - some areas which cannot be addressed with the “standard” techniques you’re talking about. Let me name just a few of them:

  1. Being able to easily check child state from the parent component and vice versa.
  2. Pass configuration to subcomponent without affecting current data context.
  3. Expose subcomponent building blocks to the component user, e.g.
{{#editor document=documentToEdit}}
  {{#editor.toolbar}}
    {{>editor.button action="select" label="select"}}
    {{>editor.button action="remove" label="remove"}}
  {{/editor.toolbar}}
  <!-- custom content goes here ... -->
{{/editor}}

To make things a little harder, imagine I want to alter the editor.toolbar behavior based on the editor.isActive state and I also want all instances of editor.button to have access to the original editor data context despite the fact I am passing some custom properties for the buttons themselves.

The pattern I described above boils down to a simple idea:

Create new templates on the fly, then customize their helpers and event hooks so that they can properly comminicate with the parent template.

What I am trying to show here is that by using this techinique one can gain enough flexibility to achieve a decent encapsulation level at the component abstraction layer. Of course, this would also work:

{{#editor isActive=isActive document=documentToEdit}}
  {{#editorToolbar isActive=isActive}}
    {{>editorButton document=documentToEdit action="select" label="select"}}
    {{>editorButton document=documentToEdit action="remove" label="remove"}}
  {{/editorToolbar}}
  <!-- custom content goes here ... -->
{{/editor}}

but it my opinion it puts too much responsibility for the proper component inclusion on the component user. Also, sharing state between editor and toolbar is too verbose and it exposes implementation details too much, which practically prevents extending the compoent behind the scenes once it’s already in use.

I think it’s the main reason Blaze is not considered good tool for creating components in general. And I am not talking about components which I use across any application I am working on, but rather components I would share with other developers in the community.

2 Likes