unModified()

Break stuff. Now.

Component Development

My component development adventures in JavaScript

December 6, 2014

One of the few hobbies I have, if not the only hobby, is building small JS libraries, experimenting with new APIs and trying out development strategies. And over the course of experimenting (which mostly happens at work :P), I have come up with ways to building stuff in a manner that is not only faster, but easy to wrap your head around. Zero-magic, pure swordsmanship and maybe a bit of thievery. Here's a few of them.

The Tile Strategy

Usually you write markup in a nested fashion. Take for example this contrived ASCII drawing of a webpage. Notice how the stuff nests "semantically" into their places.

 ___________________________________
|  _______________________________  | <header>
| | header                        | |   <headerimage></headerimage>
| |  ___________________________  | |   <navigation>
| | | headerimage               | | |     <link>Link</link>
| | |___________________________| | |     <link>Link</link>
| |  ___________________________  | |     <link>Link</link>
| | | navigation                | | |   </navigation>
| | |  _______________________  | | | </header>
| | | | Link | Link | Link |  | | | | <content>
| | | |______|______|______|__| | | |   <sidebar>
| | |___________________________| | |     <link>Link</link>
| |_______________________________| |     <link>Link</link>
|  _______________________________  |     <link>Link</link>
| | content                       | |   </sidebar>
| |  _______   _________________  | |   <contentbody>
| | | Link  | | contentbody     | | |     <title>title</title>
| | | Link  | |  _____________  | | |     <article>content</article>
| | | Link  | | | title       | | | |   </contentbody>
| | |       | | |_____________| | | | </content>
| | |       | |  _____________  | | |
| | |       | | | article     | | | |
| | |       | | |_____________| | | |
| | |_______| |_________________| | |
| |_______________________________| |
|___________________________________|

But when you're into component development (think React or Polymer), things become fiddly. Component development deals with building applications by "components" - small, isolated pieces of code that only concern themselves. A component's implementation should know nothing about code from the outside. In the same way, it only knows about its child components, but not how they're implemented. The only way to communicate to and from components is by message-passing. This makes nesting your enemy.

If you clicked something on article which does business to headerimage that has nothing to do with data (thus can't use the data layer as your transport mechanism), you'd have to bubble the event up contentbody, then up to content, then up the top-level element for the logic to dive down again to header then header-image where the message is received.

 ___________________________________
|  _____________________-----_____  |
| | header              |   A     | |
| |  ___________________*___|___  | |
| | | headerimage       V   |   | | |
| | |___________________X___|___| | |
| |  _______________________|___  | |
| | | navigation            |   | | |
| | |  _____________________|_  | | |
| | | | Link | Link | Link || | | | |
| | | |______|______|______||_| | | |
| | |_______________________|___| | |
| |_________________________|_____| |
|  _________________________|_____  |
| | content                 *     | |
| |  _______   _____________|___  | |
| | | Link  | | contentbody *   | | |
| | | Link  | |  ___________|_  | | |
| | | Link  | | | title     | | | | |
| | |       | | |___________|_| | | |
| | |       | |  ___________|_  | | |
| | |       | | | article   X | | | |
| | |       | | |_____________| | | |
| | |_______| |_________________| | |
| |_______________________________| |
|___________________________________|

And even if you had bubbling available for your framework, it would break the rule where components should never know what is beyond them. Bubbling implies knowing an event from deep down the tree, implying that you know the component exists down there.

What I did to speed up development was to think of a page not as a tree of elements, but as a big flat floorspace and elements as tiles. You lay sections side by side. Here's an equivalent markup, with elements side by side.

 ___________________________________
|  _______________________----____  |  <headerimage></headerimage>
| | headerimage           V  A    | |  <navigation>
| |_______________________X__|____| |    <link>Link</link>
|  __________________________|____  |  </navigation>
| | Link | Link | Link |     |    | |  <sidebar>
| |______|______|______|_____|____| |    <link>Link</link>
|  ________   _______________|____  |  </sidebar>
| | Link   | | title         |    | |  <title>Title</title>
| | Link   | |_______________|____| |  <article>content</article>
| | Link   |  _______________|____  |
| |        | | article       X    | |
| |________| |____________________| |
|___________________________________|

It solves your bubbling issue by having just one level up. Also means you don't need to implement additional container components, which should shave off development time. Additionally, global state of the app, which is usually at the top level is easy to hand down to the components, since they're just one level down. Last but not least, since the components are now children of the top-level component, they can now be easily manipulated by the top level.

Aggregate Data Stores

Now imagine if we wanted to have a list of users that had a checkbox on the side that when checked, marks a user as selected. In Ember you'd have to deal with these shenenigans about multiple models or more jargon for some simple merge. Come on! Seriously? And who is controller in the template? Must be voodoo.

I'm pretty sure we could do it in a way that was stupidly simple. Let's borrow Meteor's way of data storage known as Collections, which are basically just observable array-like objects. The beauty of these stores is that not only can you listen to them for changes, you can also compose stores from of other stores, also know as "aggregation".

                 _______                           _____________
                |       |                         |             |
Main Level Data | Users |                         | SelectedIds |
                |_______|       merge data        |_____________|
                    |         _______________            |     A
                    |        |               |           |     |
                    '------->| SelectedUsers |<----------'     |
                  emit change|_______________| emit change     |
                                    |                         |
                                    | emit change             |
                                    | which updates view      |
                              _______V_______                  |
                            |               |                 |
                            |  View Logic   |-----------------'
                            |_______________| User manipulates selection

// Take 2 collections
var Users = [...];
var SelectedIds = [...];

// Create a collection that merges it
var SelectedUsers = Users.map(function(user){
  var userClone = _.cloneDeep(user);
  userClone.selected = !!~SelectedIds.indexOf(user.id);
  return userClone;
});

Now this is just a simple example, just imagine if arrays were reactive. Changes on either Users or SelectedIds would re-run dependents like SelectedUsers, and trigger changes on listening views. This is actually possible with a little plumbing, or using a library like Reflux

var SelectedUsers = Reflux.createStore({
  // Default to empty references
  data : [],
  users : [],
  selected : [],

  // Listen to changes on the Users and SelectedIds stores
  init : function(){
    this.listenTo(Users,this.usersChange);
    this.listenTo(SelectedIds,this.selectedIdsChange);
  },

  // cache references
  usersChange : function(data){this.users = data;this.recalculate();},
  selectedIdsChange : function(data){this.selected = data;this.recalculate();},

  // Recalculate the mapping
  recalculate : function(){
    this.data = this.users.map(function(user){
      var userClone = _.cloneDeep(user);
      userClone.selected = !!~this.selected.indexOf(user.id);
      return userClone;
    },this);
  });
});

// Inside the view logic
SelectedUsers.listen(function(data){
  // Update view with data
});

The logic is fakking simple! Grab needed arrays and just create another array containing data merged from the needed arrays. Update when any of the dependencies change. Repeat as required. No magic necessary, no jargons to learn. It's as simple as merging lists.

Grab and Go

One problem I had once with components is that I had to pass down the data from the top level down to the lowest level. While I said I built stuff using the tile strategy, that's just for sections. Inside sections, you'll most probably have custom input boxes and dropdowns which are also components by themselves. And not only that, they need to access data from stores.

                     ______________
                    |              |
                    |  Languages   |
                    |______________|
                            |
____________________________|______     <!-- top-level.component -->
|  __________________________*____  |    <top langs="{{languages}}" />
| | headerimage              |    | |
| |__________________________|____| |    <!-- article.component -->
|  __________________________|____  |    <article locales="{{langs}}" />
| | Link | Link | Link |     |    | |
| |______|______|______|_____|____| |    <!-- language-select.component -->
|  ________   _______________|____  |    <language-select list="{{locales}}" />
| | Link   | | title         |    | |
| | Link   | |_______________|____| |
| | Link   |  _______________|____  |
| |        | | article       *    | |
| |        | |               |    | |
| |        | | Language: ____v___ | |
| |        | |                    | |
| |________| |____________________| |
|___________________________________|

Well, I mean languages isn't really part of article. I mean, it's just a list of languages for use in a language-select component and has nothing to do with the article. This breaks a few laws of component development where 1) Components only need to be passed relevant data and 2) You're not supposed to know anything about the internals of a component. In this case, we're passing irrelevant data (languages) into an article component and the article component knows that languages-select need languages array.

Now to fix that, it's as simple as having the component itself grab its data requirements and just run.

                     ______________
                    |              |
                    |  Languages   |-.
                    |______________| |
                                      |
___________________________________  |  <!-- top-level.component -->
|  _______________________________  | |  <top />
| | headerimage                   | | |
| |_______________________________| | |  <!-- article.component -->
|  _______________________________  | |  <article />
| | Link | Link | Link |          | | |
| |______|______|______|__________| | |  <!-- language-select.component -->
|  ________   ____________________  | |  <language-select />
| | Link   | | title              | | |
| | Link   | |____________________| | |
| | Link   |  ____________________  | |
| |        | | article            | | |
| |        | |                    | | |
| |        | | Language: ______ <-----'
| |        | |                    | |
| |________| |____________________| |
|___________________________________|


// we get the languages ourselves
var Languages = $.get('/languages');

new Component({
  init : function(){
    // Plop it in!
    Languages.then(function(response){
      this.set('languages',response);
    }.bind(this));
  }
})

Containing components need not worry anymore about passing through data. The general rule I follow is that when the component is to be reusable, have it grab its own data and just go. Also, language-select is made portable by loading directly from the Languages collection and not anymore dependent on some parent component fetching languages and supplying it.

Conclusion

Like all other libraries, what may work for you may not work for me and vice versa. Mileage may vary. But remember, JavaScript was described as a toy language, why not build stuff that way - in small and bite-sized portions. Keep it small, keep it simple, keep it stupid and I guarantee you that even your manager will want to write code... after he changes the world of course. ;P