Thinking in Components

Andrew Krespanis
Tweet

Web Components, React, Polymer, Flight — all are intended for building interface components. This is a different toolset from the big MVC and MVVM frameworks, and requires a different mindset when planning how you’ll implement your interface. While I still use models like MVC for server applications, I’m a dedicated convert to the benefits of a component approach for interface development. In this article I’ll outline how thinking in components differs from thinking in MVC and implement that approach in a real-world example.

In my mind, the headspace for MVC design is “How do I model my business domain? How do I model the processes of interacting with that domain? How do I model the interface to facilitate those processes?”. It is my opinion that this headspace does not facilitate good component design. In fact it’s the polar opposite for how you should be thinking when you set out to break down an interface into composable components. At best you’ll end up with micro apps. At worst you’ll build God components. The last thing you want to do is model your business domain as components. What you should aim to model is the smallest abstract sections of interaction you can describe.

Designing for Re-Use

Instead of “How do I make this dismissible alert panel?”, ask yourself “If I was adding new HTML elements to facilitate this interaction, what would they be?”. I find this leads to components which are safely distanced from the business domain and inherently the most re-usable in different contexts.

As another example, don’t make a Type-Ahead Help Search component that be used everywhere you want to allow searching the Help system, make a suggestive text input component that knows about the interactions involved in providing input suggestions. Then make a Help Search API data component that knows how to receive requests for data, interact with the Help Search API and broadcast results. Now your suggestive text input’s tests don’t need any mocking of APIs, and when you’re asked to add suggestions to a “tag” field, you can drop in your existing suggestive text input component, wire up a simple data component that talks to the tag API, and done!

Practical Example – “Project List”

For a concrete example, lets take a look at implementing a simple interface as isolated components. The following mockup is an extraction from 99designs 1-to-1 Projects system. While the UI has been drastically simplified, the JavaScript we’ll build up to is production code from our site at the time of writing. Here is the wireframe:

Wireframe

What we have is navigation between three lists of projects — Active, Drafts, and Archived. Each project has an action that can be performed on it — archiving an active project, deleting a draft, or re-activating an archived project. In app design thinking we’d start modeling a project and giving it methods like “archive” and “delete”, and a “status” property to track which of the three lists it belongs in. Bringing that line of reasoning to component design is exactly what we want to avoid, so we’re going to concern ourselves only with the interactions and what is needed to facilitate them.

At the core of it we have an action per row. When that action is performed we want to remove the row from the list. Already we’ve shed any Project-specific domain knowledge! Further, we have a count with how many items are in each list. To restrain the scope of this article, we assume each page to be generated server-side, with the tab navigation causing a full page refresh. As we don’t need to force dependance on JavaScript, our action buttons will be form elements with submit event handlers that will asynchronously perform the form’s action and broadcast an event when it’s complete.

Here’s some HTML for a single project row:

<li>
  <a href="/projects/99" title="View project">Need sticker designs for XYZ Co.</a>
  <div class="project__actions">
    <a href="/projects/99" class="button">View</a>
    <form class="action" action="/projects/99/archive" method="post">
        <button>Archive</button>
    </form>
  </div>
</li>

I’ll be using Flight to build our components. Flight is currently our default JS component library at 99designs for the reasons I outlined in my previous SitePoint JavaScript article.

Here’s our AsyncForm component for handling the form submission and broadcasting an event:

define(function(require) {
  'use strict';

  var defineComponent = require('flight/lib/component');

  function AsyncForm() {
    this.defaultAttrs({
      broadcastEvent: 'uiFormProcessed'
    });

    this.after('initialize', function() {
      this.on(this.node, 'submit', this.asyncSubmit.bind(this));
    });

    this.asyncSubmit = function(event) {
      event.preventDefault();
      $.ajax({
        'url': this.$node.attr('action'),
        'dataType': 'json',
        'data': this.$node.serializeArray(),
        'type': this.$node.attr('method')
      }).done(function(response, data) {
        this.$node.trigger(this.attr.broadcastEvent, data);
      }.bind(this)).fail(function() {
        // error handling excluded for brevity
      });
    };
  }

  return defineComponent(AsyncForm);
});

We maintain a strict policy of never using class attributes for JavaScript, so we’ll add a data-async-form attribute to our action forms, and attach our components to all matching forms like so:

AsyncForm.attachTo('[data-async-form]');

Now we have the ability to perform the action, and broadcast an event which will propagate up the DOM tree on success. The next step is listening for that event and removing the row that it bubbles up to. For that we have Removable:

define(function(require) {
  'use strict';

  var defineComponent = require('flight/lib/component');

  function Removable() {
    this.defaultAttrs({
      'removeOn': 'uiFormProcessed'
    });

    this.after('initialize', function() {
      this.on(this.attr.removeOn, this.remove.bind(this));
    });

    this.remove = function(event) {
      // Animate row removal, remove DOM node, teardown component
      $.when(this.$node
        .animate({'opacity': 0}, 'fast')
        .slideUp('fast')
      ).done(function() {
        this.$node.remove();
      }.bind(this));
    };
  }

  return defineComponent(Removable);
});

Again we add a data-removable attribute to our project rows, and attach the component to the row elements:

Removable.attachTo('[data-removable]');

Done! Two small components with one event each, and we’ve handled the three types of actions in our three forms in a way that gracefully degrades. Only one thing left, and that’s our count on each tab. Should be easy enough, all we need is to decrement the active tab’s count by one every time a row is removed. But wait! When an active project is archived, the archived count needs to increase, and when an archived project is re-activated, the activated count needs to increase. First lets make a Count component that can receive instructions to alter its number:

define(function(require) {
  'use strict';

  var defineComponent = require('flight/lib/component');

  function Count() {
    this.defaultAttrs({
      'event': null
    });

    this.after('initialize', function() {
      this.on(document, this.attr.event, this.update.bind(this));
    });

    this.update = function(event, data) {
      this.$node.text(
        parseInt(this.$node.text(), 10) + data.modifier
      );
    }
  }

  return defineComponent(Count);
});

Our Count would be represented in HTML as something like <span data-count>4</span>. Because the Count listens to events at the document level, we’ll make its event property null. This will force any use of it to define an event that this instance should listen to, and prevent accidentally having multiple Count instances listening for instructions on the same event.

Count.attachTo(
  '[data-counter="active"]',
  {'event': 'uiActiveCountChanged'}
);

Count.attachTo(
  '[data-counter="draft"]',
  {'event': 'uiDraftCountChanged'}
);

Count.attachTo(
  '[data-counter="archived"]',
  {'event': 'uiArchivedCountChanged'}
);

The final piece of the puzzle is getting our Removable instances to fire an event with a modifier to their respective counter(s) when they’re removed. We certainly don’t want any coupling between the components, so we’ll give Removable an attribute that is an array of events to fire when it is removed:

define(function(require) {
  'use strict';

  var defineComponent = require('flight/lib/component');

  function Removable() {
    this.defaultAttrs({
      'removeOn': 'uiFormProcessed',
      'broadcastEvents': [
        {'event': 'uiRemoved', 'data': {}}
      ]
    });

    this.after('initialize', function() {
      this.on(this.attr.removeOn, this.remove.bind(this));
    });

    this.remove = function(event) {
      // Broadcast events to notify the rest of the UI that this component has been removed
      this.attr.broadcastEvents.forEach(function(eventObj) {
        this.trigger(eventObj.event, eventObj.data);
      }.bind(this));

      // Animate row removal, remove DOM node, teardown component
      $.when(this.$node
        .animate({'opacity': 0}, 'fast')
        .slideUp('fast')
      ).done(function() {
        this.$node.remove();
      }.bind(this));
    };
  }

  return defineComponent(Removable);
});

Now the coupling between Count and Removable happens in the use case specific page script where we attach our components to the DOM:

define(function(require) {
  'use strict';

  var AsyncForm = require('component_ui/async-form');
  var Count = require('component_ui/count');
  var Removable = require('component_ui/removable');

  $(function() {

    // Enhance action forms
    AsyncForm.attachTo('[data-async-form]');

    // Active Projects
    Count.attachTo(
      '[data-counter="active"]',
      {'event': 'uiActiveCountChanged'}
    );

    Removable.attachTo('[data-removable="active"]',
      {
        'broadcastEvents': [
          {
            'event': 'uiArchivedCountChanged',
            'data' : {'modifier' : 1}
          },
          {
            'event': 'uiActiveCountChanged',
            'data' : {'modifier' : -1}
          }
        ]
      }
    );

    // Draft Projects
    Count.attachTo(
      '[data-counter="drafts"]',
      {'event': 'uiDraftCountChanged'}
    );

    Removable.attachTo(
      '[data-removable="drafts"]',
      {
       'broadcastEvents': [
          {
            'event': 'uiDraftCountChanged',
            'data' : {'modifier' : -1}
          }
        ]
      }
    );

    // Archived Projects
    Count.attachTo('[data-counter="archived"]',
      {'event': 'uiArchivedCountChanged'}
    );

    Removable.attachTo('[data-removable="archived"]',
      {
        'broadcastEvents': [
          {
            'event': 'uiArchivedCountChanged',
            'data' : {'modifier' : -1}
          },
          {
            'event': 'uiActiveCountChanged',
            'data' : {'modifier' : 1}
          }
        ]
      }
    );
  });
});

Mission accomplished. Our counters know nothing of our project list rows, which know nothing of the forms inside them. And none of the components are in the slightest way designed around the concept of a list of projects.

Last Minute Addition

Our UX designer has pointed out that it would be better if we asked for confirmation when someone tries to delete a draft, as this action cannot be undone. No problem, we can whip up a component that does just that:

define(function(require) {
  'use strict';

  var defineComponent = require('flight/lib/component');

  function Confirm() {
    this.defaultAttrs({
      'event': 'click'
    });

    this.after('initialize', function() {
      this.$node.on(this.attr.event, this.confirm.bind(this));
    });

    this.confirm = function(e, data) {
      if (window.confirm(this.$node.data('confirm'))) {
        return true;
      } else {
        e.preventDefault();
      }
    };
  }

  return defineComponent(Confirm);
});

Attach that to the delete buttons, and we’ve got what we were asked for. The confirm dialog will intercept the button, and allow the form submission if the user selects “OK”. We haven’t had to alter our AsyncForm component, as we can compose these components without interfering with each other. In our production code we also use a SingleSubmit component on the action button which gives visual feedback that the form has been submitted and prevents multiple submissions.

Final Components, Tests, and Fixtures

Hopefully this article has demonstrated how your projects could benefit from breaking down interfaces into composable components. An important benefit of component design that I haven’t covered is their ease of isolated testing, so here are the final components along with their jasmine tests and HTML test fixtures:

If you have any questions regarding what I’ve covered, please ask for details in the comments and I’ll do my best to help.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • sarfraznawaz2005

    Really useful, yes breaking things in independent components sounds good idea, hope we have good framework both client-side and server-side to do component-oriented programming. Domain driven design seems similar idea from server-side perspective but still not much in the light.

  • Michael

    Sounds pretty cool! How do you debug what events get fired when something happens? I imagine that getting tricky pretty quickly, especially when using such “abstract” components. And on the same note, how do you (if at all) prevent multiple paints etc?

  • WooDzu

    Nice read! How about packaging the code into a WebComponent?

  • Tarabass

    It’s just OOP where objects start growing and user controls are born. Before MVC there was nothing else then user controls and objects with logic. Most of it was build server side. Maybe that’s the reason why all of this things are new for front end developers, and they think it all has to be reinvented.And maybe that’s the difference between oo languages and script languages..

  • Maykonn Welington Candido

    MVC is a architectural pattern, not a design pattern. So, “In my mind, the headspace for MVC design is “How do I model my business domain?(…)” , is related to Domain Driven Design for example.