Shreds: On the Javascript

| Comments

Currently working on the re-implementation of shreds client-side and move to es6 while I’m on it. Figured out I might as well write some note on its current implementation, annotating the caveats or something.

Anyway, the main concept was to be able to add new feature (to the app) quickly, while maintaining the whole (supposed to be modular) structure. The whole app is started with this main (singleton) object called shreds.

Main Application

Shreds was supposed to be the only globals exposed by the application. All its features were implemented as shreds so-called component.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*
 * Shreds main application.
 * Shreds utilizing components as its building block.
 * Each shreds's component is a plain old javascript object
 * with init function as its `constructor'.
 *
 * Components registered by pushing its name to `Shreds.components' list.
 * As an example shreds component may be written roughly like this:
 *
 * var name = 'test';
 * Shreds.registerComponent(name, {
 *   init: function () {
 *     console.log('test component initialized.');
 *   }
 * });
 *
 * Shreds use facade-like pattern to do inter-components communication.
 * Each component may define `events' object which will be available
 * under `Shreds.$'. Later in your code you may invoke the event handler
 * by `triggering' the event like this:
 *
 * Shreds.$.trigger(eventname, data);
 *
 */

window.Shreds = {

  // Event sandbox, a jQuery object.
  '$': $({}),

  // components name list.
  components: [],

  // Shreds's `constructor'.
  // This function will iterate the components list and call init function
  // from each component. `init' will also bind component's event to `Shreds.$'
  init: function () {
    // ...
  },

  // ... omitted

The application starts when its init method called. Which in turn iterates all the registered components, registering event-handlers and calling init method from each. This design is simple and quite neat, application start-up process is centralized and adding more feature is just a matter of adding new property (component to the main Shreds object.

Events Sandbox

One of the central component of shreds is its event sandbox, a blank jQuery object. Components may define an events property which contains event-handlers, each will be registered at init. There’s no particular rule for the event handler, it can be named anything, which represents the event name, so the same event may be handled by multiple components. The sandbox act as a central dispatcher so all components can communicate even though they’re isolated from each other. Well, at least that’s the intention. Unfortunately since all components were bounded to shreds as property, and for the fact that Shreds object is imported to each component definition:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
(function (Shreds) {
  // Shreds is freely accessible here
  Shreds.registerComponent('mycomponent', {
    init: function () {},
    events: {
      'data:loaded': function (ev, data) {
        this.process(data);
      }
    },
    process: function (data) {
      // ...
    }
  });
})(window.Shreds);

(function (Shreds) {
  Shreds.registerComponent('anothercomponent', {
    init: function () {
      // This is how we supposed to communicate with `mycomponent'
      Shreds.$.trigger('data:load', 'data');

      // but this also valid, bummer.
      Shreds.mycomponent.process('data');
    }
  });
})(window.Shreds);

Given we know the name of the component, we can access any component freely from any other components. Which is not consistent and confusing in some usages.

Routing

To manage URL routes, shreds use event based push-state routing system built on top of History.js. It’s just a simple regex matcher designed so I can define the routes like this:

1
2
3
4
5
6
7
8
9
10
11
var r = new Router({
  anchor: Shreds.$,
  on_dispatch: 'shreds:route:dispatched'
});
r.map('/',                       'feeds:render:index');
r.map('/backyard/subscriptions', 'backyard:subscriptions');
r.map('/categories',             'categories:render:index');
r.map('/page/:page',             'feeds:render:page');
r.map('/:feed_id',               'feed:render:show');
r.map('/:feed_id/page/:page',    'feed:render:page');
r.map('/:feed_id/:id',           'newsitem:render:show');

When URL matched with one of the defined pattern above, the router will parse the parameters and pass it along from the assigned event trigger.

Data Model

Shreds data model is almost non-existent, it only has simple wrapper to re-index resources by its id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
(function (Shreds) { 'use strict';
  var index_prefix = '$idx:-';
  var Models = {};
  var Context = {};

  Shreds.registerComponent('model', {
    init: function () { },

    import: function (modelName, data, options) {
      cleanUpContext(modelName, options);
      if (data instanceof Array) {
        var indexed = index_prefix + modelName;
        Models[indexed] || (Models[indexed] = {});
        Models[indexed] = data.reduce(function (prev, curr, idx, arr) {
          prev[curr.id] = curr;
          return prev;
        }, Models[indexed]);
        data.forEach(function (val, idx, arr) {
          for (var child in val.has) {
            this.import.call(this, modelName + '/' + val.has[child], val[val.has[child]]);
          }
        }.bind(this));
      } else if (typeof data === 'object') {
        Models[modelName] = data;
        for (var model in data) {
          this.import.call(this, modelName + '/' + model, data[model]);
        }
      }
    },

// ... 

All models is represented as javascript object without any bindings to the view, all changes to the model should be followed by rerendering the view manually.

View Rendering

The whole view related thingy were managed by handlebars. Which totally helps separating concerns between the data and its visual representation. The only downside is since it’s render to string, any data change means rerender the whole template. Shreds use attribute binding and partials to let handlebars render parts of the template:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<li class="nav-item{{./active}}" data-on-mousedown="navigated" data-feed-id="{{./id}}" data-template="navlist_item:{{./id}}">
    <div class="favicon pull-left">
        <img src="{{./favicon}}" width="16" height="16"/>
    </div>
    <p class="feed-title ellipsis">
        <a alt="{{./title}}" href="{{./path}}">{{./title}}</a>
    </p>
    <p class="latest-news text-muted ellipsis"><abbr class="timeago" title="{{./latestEntryPubDate}}">{{readableDate ./latestEntryPubDate}}</abbr> - {{{./latestEntryTitle}}}</p>
    {{#if ./unreadCount }}
        <a href="javascript:void(0)" data-id="{{./id}}" data-on-click="markAsRead" data-on-mousedown="doNotPropagate">
            <span class="pull-right unread-num label">{{./unreadCount}}</span>
        </a>
    {{/if}}
</li>

Above, <li> tag despite not being empty has data-template attribute, which may be used later to rerender its content when the model state is changed. Though it’s still has to be done manually.

1
2
var feed = Shreds.model.find('navigation/categories/feeds', id);
Shreds.syncView('navlist_item:'+id, feed);

Up Next

shreds is being rebuilt from scratch with ES6, and its components architecture is redesigned based on theguardian.com’s wonderful Ractive.js. I’ve been putting glue and boilerplates here and there recently, hope I able to finish the migration before next weekend.

Comments