Thinking in Components

Share this article

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.

Frequently Asked Questions (FAQs) about Thinking Components

What are the key components of effective thinking?

Effective thinking is a multifaceted process that involves several key components. These include clarity, precision, accuracy, relevance, depth, breadth, logic, significance, and fairness. Each of these components plays a crucial role in ensuring that our thinking process is effective and leads to accurate conclusions.

How can I improve my critical thinking skills?

Improving critical thinking skills involves practicing certain habits such as questioning assumptions, seeking diverse perspectives, and being open to new ideas. It also involves developing skills such as analysis, interpretation, inference, evaluation, explanation, and self-regulation.

What is the role of logic in critical thinking?

Logic is a fundamental component of critical thinking. It involves the ability to reason correctly, to derive conclusions from premises, to evaluate claims, and to avoid fallacies or errors in reasoning.

How does relevance contribute to effective thinking?

Relevance ensures that the information or ideas we are considering are directly related to the issue or problem at hand. It helps us to stay focused and avoid distractions or irrelevant information.

What is the significance of depth in critical thinking?

Depth in critical thinking refers to the ability to delve beneath the surface of an issue or problem, to understand its underlying causes or implications, and to explore it from multiple perspectives.

How can I develop breadth in my thinking?

Developing breadth in thinking involves considering a wide range of perspectives, ideas, and sources of information. It requires being open-minded, curious, and willing to explore new ideas or viewpoints.

What is the role of fairness in critical thinking?

Fairness in critical thinking involves being unbiased, impartial, and objective. It requires us to consider all relevant viewpoints and evidence, and to avoid favoritism, bias, or prejudice.

How does precision contribute to effective thinking?

Precision in thinking involves being exact, detailed, and clear in our thoughts and expressions. It helps us to avoid vagueness, ambiguity, or confusion, and to communicate our ideas effectively.

What is the significance of accuracy in critical thinking?

Accuracy in critical thinking involves ensuring that our information, ideas, and conclusions are correct, reliable, and free from errors or distortions. It is crucial for making sound decisions and judgments.

How can I improve the clarity of my thinking?

Improving clarity in thinking involves practicing clear communication, avoiding jargon or complex language, and striving for simplicity and straightforwardness in our thoughts and expressions.

Andrew KrespanisAndrew Krespanis
View Author

Andrew is a UI developer at 99designs.

ColinIFlightpolymerReactweb components
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week