Falling Into the Pit of Success with Web Components

Being able to build components (AKA “directives” if you’re in the Angular camp) has changed the way we structure web applications on the front end. This is a big move forward and provides a lot of power in addition to some much needed encapsulation.

However, even with the drastically improved tools we have at our disposal for building client side applications, I still see projects going down the wrong path. Code is tightly coupled and maintenance quickly becomes an issue.

The best way I’ve found to mitigate this is to separate concerns as much as possible. I’m going to say that again just because I think it’s that important: separate concerns of your components as much as possible.

We all start out having our components do too much, myself included. It’s hard to get into the habit, but our goal should be to make components as small and dumb as possible. This ties right in with other software development best practices like DRY and SRP, but it’s not something I see people doing a lot in client side applications.

Here are two big revelations I had about developing components that have made my life a lot easier, along with a an example from a recent project where I broke apart a component that was doing too much.

Pass In Your Data

When I started building components I often made them responsible for managing their own data. After they were instantiated, they would be responsible for making API calls to fetch and manipulate their data. This works for a while, but becomes difficult to maintain over time.

Passing data in to a component provides a lot of the same wins as dependency injection. It allows your components to be very small and focused which makes them easier to maintain and test.

This can be accomplished by creating containers, also components, that are solely responsible for managing data injecting it into their children. I’ve also done this by instantiating data through a router and exposing that to the view where the components are rendered.

If you’re coming from a framework like Angular, I’m not just talking about separating things from directives and controllers, either. It will be tempting to just toss some additional logic into your controller for managing data, but this should actually be injected into the component’s controller, so it needs to come from another source.

The main goal here is to make our components as dumb as possible so they become more reusable, testable and maintainable.

There’s No Such Thing As A Component That’s Too Small

At first it’s going to feel weird to break things down into smaller and smaller pieces. It’s common to think that it isn’t worth the overhead of creating a new component when evaluating where you might be able to break things apart. However, this is where you are able to set yourself up for success.

Let’s take a look at an example from a recent project I was working on. We had some widgets that stood alone as individual components.

Widget preview

We had a single controller and view backing this entire widget. It worked fine for a couple weeks, until we had requests to see this same data represented in similar but different forms throughout our app.

It was hard to decide whether to write a new component or try to modify the design to make our existing components fit. Neither of these options were ideal. In the end, this widget was broken out into 7 different components and composed of 10 total. I’ve annotated another screenshot to show the breakdown.

Annotated widget

We ended up breaking things out into a bunch of pieces that are very easy to maintain and test individually. Now we have all the building blocks we need to develop other complex interfaces with these same components. Another big win here is that we are passing all of our data in to the components so they can just worry about rendering. The data comes from the top level component, which is not annotated in the screenshot since it just handles data.

Conclusion

Breaking things down into small pieces that have a single responsibility is always a win. The story is no different for web components. Don’t forget, managing data is a responsibility.

If you have other techniques for writing maintainable components, please share them in the comments.

Angular Directive Testing Best Practices

Getting in the habit of testing is hard, but I’ve found that a big turning point for me when I start testing in new languages and tools is to identify patterns in how tests are written. Once I know a pattern for testing something, it’s easier to walk through each step to write my tests. Just like anything else, it’s more approachable when you break it apart into smaller pieces.

When testing directives, this is the main pattern I see:

  1. Mock
  2. Compile
  3. Interact
  4. Assert

An Example Directive

I’m going to walk through each step in the directive testing pattern for a directive I wrote recently. Here is the source of the radioButton directive, which provides a way to use custom style and markup for a radio form control.

angular
  .module('kit.forms', [])
  .directive('radioButton', radioButtonDirective);

function radioButtonDirective() {
  return {
    require: '?ngModel',
    link: function(scope, element, attributes, ngModelCtrl) {
      element.addClass('button');

      ngModelCtrl.$render = function() {
        element.toggleClass('primary', angular.equals(ngModelCtrl.$modelValue, scope.$eval(attributes.radioButton)));
      };

      element.bind('click', function() {
        if (!element.hasClass('active')) {
          scope.$apply(function () {
            ngModelCtrl.$setViewValue(scope.$eval(attributes.radioButton));
            ngModelCtrl.$render();
          });
        }
      });
    }
  };
}

Note: This example shows some of the power of ngModelController, which I think is pretty cool. If you’re interested in learning more about what you can do with ngModel, send me an email or let me know in the comments.

Mock

The first thing we need to do is mock out our dependencies. In this example, we are mocking out the kit.forms module and injecting a new instance of $rootScope to use later in our tests.

describe('Forms module', function() {
  var element, scope;

  beforeEach(module('kit.forms'));

  beforeEach(inject(function($rootScope) {
    scope = $rootScope.$new();
  }));

  // ...
});

Compile

Directives need to be compiled in order to make assertions on the HTML they produce. This can be done by injecting the $compile service and running a string of HTML through it. After it is compiled, the digest cycle needs to be kicked off with a call to $digest() from the $scope instance created earlier.

describe('[required]', function() {
  beforeEach(inject(function($compile) {
    element = angular.element(
      '<div class="button-group">' +
        '<label ng-model="model" radio-button="'yes'">Yes</label>' +
        '<label ng-model="model" radio-button="'no'">No</label>' +
        '<label ng-model="model" radio-button="'maybe'">Maybe</label>' +
      '</div>'
    );

    $compile(element)(scope);
    scope.$digest();
  }));

  // ...
});

Interact

Now you can script some interactions on the compiled HTML before you make assertions. Storing a reference to the element(s) you want to manipulate and make assertions against at the beginning of your specs is usually helpful.

it('applies the "primary" class when clicked', function() {
  var radios = element.find('.button');

  radios.eq(1).click();
  // ...

  radios.eq(2).click();
  // ...
});

Assert

Finally, we can make assertions that the markup has changed the way it’s expected to.

it('applies the "primary" class when clicked', function() {
  var radios = element.find('.button');

  radios.eq(1).click();
  expect(radios.eq(1)).toHaveClass('primary');
  expect(radios.eq(0)).not.toHaveClass('primary');
  expect(radios.eq(2)).not.toHaveClass('primary');

  radios.eq(2).click();
  expect(radios.eq(2)).toHaveClass('primary');
  expect(radios.eq(1)).not.toHaveClass('primary');
});

Sometimes the interaction step isn’t necessary for making assertions. For example, it’s usually best to assert that a directive initially compiles the way you expect it to.

it('adds the "button" class', function() {
  expect(element.find('.button').length).toBe(3);
});

E2E Testing Directives

When I first started testing directives I thought I was doing it wrong because I was testing the DOM but I wasn’t using a true integration testing tool, like Protractor. However, I realized a lot of times it’s hard to know the context a directive is going to be used in so it’s difficult to end-to-end test effectively. In most cases it’s easier to avoid an integration testing tool for an individual directive test and just rely on that for flexing the main flows of your SPAs.

Other Patterns

Directives are easily the most difficult thing to test in Angular, but I’ll be sharing some other Angular testing patterns and best practices I use over the next few weeks. Make sure to subscribe to email updates if you want to keep up with this series of articles on Angular testing and please share any other patterns you’ve seen in the comments.