Blog

Designing components with Angular

This post is pretty old, and might contain outdated advice or links. We’re keeping it online, but recommend that you check newer posts to see if there’s a better approach.

All blog posts

Comparing its design philosophy with Bootstrap, Polymer and Aurelia.

At De Voorhoede, we did our first Angular project in 2013. What started as "getting it to frigging work", quickly evolved into a more opinionated way of designing components and apps. In this post, I'll cover patterns and tools that we use to design our components and be productive at it.

Problems to solve

In order to find out what sort of components and architecture we're going to need, we need to know what sort of "problems" we need to solve. I'm very problem-oriented and practical when making architectural decisions. I never just copy the architecture from another project.

Let's take a look at the requirements of one of our largest Angular projects.

5 different apps. Multiple organizations that designed their own backends. At some point they all became part of the same umbrella organization and now they want their apps to get a uniform branding and UX.

They are asking for a frontend that provides them with re-usable components. The first few backends will be redesigned while we work on the frontend components, so that we can design the functionality and corresponding JSON together. The JSON data structures will be the contracts between backend and frontend.

After the first few apps have been built, the component library will have reached a level of maturity. It will allow the backend teams to build new apps just by building a new backend for it; no additional frontend work required.

All backend teams will do their best to be as consistent as possible in the JSON structure that they deliver. However, some exceptions will happen. The components will need to be usable with slight JSON structure variations.

We work agile. As apps get built, changes will be made to components constantly. Features will be added, bugs will be fixed, UX enhancements will be done, etc. Sometimes, there will be breaking changes. Each app will need to be able to control if and how they want to deal with (breaking) changes.

Components will need to work together. When we receive a JSON response for a page, it will be passed to a router. The router will pass on most of the JSON structure to other components. Components will delegate further to other components internally, etc.

A page response might also contain data to instantiate a new child router. The app structure is not set in stone; it is constantly extended with new routes as responses are coming in.

We will be working in a Scrum team with backenders, frontenders and designers. Communication and documentation is essential for making quick progress and re-using (or building on top of) existing work.

Deliverables

Based on the project requirements, we'll need to deliver:

  • A component library. All components will be maintained and tested in one place.
  • Package management. Apps will be able to either conveniently install the entire component library at a specific version, or only specific components to keep the frontend more lightweight.
  • Component changelogs. As components are constantly changing, we'll need to document added features and bug fixes. Especially the breaking changes, and grouped by component.
  • A component API reference with runnable examples. This will become the home of the component library, for backenders and frontenders. People will look here to find out how a component works, or which component(s) they want to use for a particular scenario. It will also be the place where we develop and review components.
  • JSON contracts. We'll need them in two formats: human readable and machine readable. The human readable format will be used in design sessions and documentation, and should be very easy to read and write. Where applicable, these should be linked with specific component documentation. The machine readable format will be used for runtime validation of incoming/outgoing JSON, on both server and client. It will help in tracing the source of any incorrect JSON during development and testing.
  • An example app with mocked backend. The last station. It will be the proof that you can compose an extremely dynamic app with just JSON responses. Using one frontend, switch one backend mock with another, and get an entirely different app.

Component philosophy

Component definition

In general, the meaning of the term "component" can vary based on the context. In our case, it means: an isolated responsibility within the architecture. It can be a visual widget (e.g. responsible for rendering a form field), an API (e.g. responsible for querying data from the server), or a composition of multiple components to combine their functionality (in this case its only responsibility is to pass instructions on to the other components).

Each component has just one responsibility. Some components will be really tiny. That's okay, as it will make them highly re-usable.

More complex components will delegate most of their functionality to other, smaller components. For example, a form field component might internally use our server query component, to implement server side validation of its model value. Other small components can be used to consistently render validation reponses across different form field components.

Philosophical influences

Web Components

Web Components are an excellent starting point for forming your own philosophy in component design. I like how Web Components make HTML the center of the architecture. This makes component APIs very declarative, and more agnostic from JavaScript implementations and frameworks.

Polymer

The "everything is an element" philosophy, as explained in The Polymer World-View:

Consider the humble <select>. We take it for granted, but it's actually pretty impressive:

  • Functional. The browser already knows what to do with a <select> element. When it encounters <select> in markup it creates an interactive control for the user.
  • Reusable. The <select> element is a reusable package of functionality that you don't have to implement yourself.
  • Interoperable. Every JavaScript library knows how to interact with DOM elements.
  • Encapsulated. It keeps its internals all tucked away, so including one won't break the rest of your page.
  • Configurable. You can configure its behavior with HTML attributes, without using any script.
  • Programmable. If you grab the element from the DOM it also has methods and properties for things that don't make sense in markup.
  • Event Generator. It dispatches events to let you know when something interesting happens.
  • Composable. Not only can you include a <select> inside of most other kinds of element, its behavior can also change depending on which things you put inside of it.

Bootstrap

Another interesting architecture that inspired me to write declarative HTML APIs, long before I discovered Web Components or Angular, is Bootstrap's Data API.

This architecture can be summarized as follows:

[data-{verb}={component}]
[data-target={selector}]
.{component}-{modifier}

An example, using their (http://getbootstrap.com/javascript/#alerts) component:

<!--
Using data-dismiss without target will traverse
up the DOM until an "alert" component instance
is found, and will invoke its "dismiss" method.
-->
<div class="alert alert-dismissable">
    <button data-dismiss="alert"></button>
</div>
<!--
Using data-target to manually specify
the component instance.
-->
<div class="alert alert-dismissable" id="myAlert"></div>
<button data-dismiss="alert" data-target="#myAlert"></button>

Angular

The declarative HTML approach started to really interest me when I saw an (https://angularjs.org/#add-some-control">example todo app using Angular dir).

The interesting thing to me is that all the JavaScript parts are just APIs. None of them control the application flow directly. Each of them has one responsibility, and you need to tell it what to do and when.

Angular introduced three concepts to make it easy to wire components together into an app using HTML:

  • Scopes
  • Bindings
  • DOM-level dependency injection

Especially the dependency injection is powerful in composing apps using just HTML. The Angular injector goes further than injecting singleton or transient services into each other. You can declaratively inject component instances into each other, based on their position in the DOM.

For example, injecting <form-field-{type}> components into parent <form> components, and <form> components into parent <form>components. In this particular example you could leverage it to determine the validity of a form based on the validity of child component instances.

Injecting <tab-pane> components into a parent <tab-container> to automatically keep an up-to-date list of tabs based on the current DOM.

Scopes, combined with powerful single-purpose directives such as [ng-if] and [ng-repeat], will update the DOM and create/destroy component instances based on your model bindings. No JavaScript needed to wire everything together.

Aurelia

The Aurelia Framework categorizes components into three common types:

  • Custom Elements: Add new tags to your HTML markup. Each Custom Element can have its own view template which can be rendered into the Light DOM or the Shadow DOM. Custom Elements can also have any number of properties which they surface as attributes in HTML for databinding support and which they can databind to inside their view template.
  • Attached Behaviors: "Attach" new behavior or functionality to existing HTML elements by adding a custom attribute to your markup.
  • Template Controllers: Convert DOM into an inert HTML template. The controller can then decide when and where (or how many times) to instantiate the template in the DOM. Examples of this are the if and repeat behaviors. Simply place one of these behavior on a DOM node and it becomes a template, controlled by the behavior.

Angular 2.x has the same types, except they are calling them Components, Decorators and Templates.

Philosophy summarized

  • Use declarative HTML to compose apps out of components.
  • Leverage DOM hierarchy for component dependencies and life cycle management.
  • Build single-purpose components with explicit dependencies. Components can work together by letting them share bindings, and components can delegate to other components internally.

Benefits

  • We can compose apps very rapidly, while still having very maintainable code.
  • The impact of change is predictable because of a good separation of concerns.
  • Application flow in HTML is very readable. It's a good starting point when picking up a user story.
  • Easy memory management as component instances are automatically created and cleaned up, based on the scopes that they are part of.

Project architecture

Based on the list of deliverables and our component philosophy, let's take a look at the actual component library architecture.

Tools

Angular

Angular makes us extremely productive in building declarative, single purpose components.

Our Angular components will do all the heavy lifting, apps are composed using a bit of HTML.

Speaking of heavy lifting, Angular's focus on writing testable components is also a huge plus. I've experienced that writing testable Angular components actually helps in designing them well, too. Your tests will not take it easy on you if you have not separated your concerns well enough.

Having good test coverage in our components will cause apps to require very little unit testing in return. After all, the apps are just connecting all the different components and have almost no custom logic.

Bower

We split components into separate packages as much as needed. Some packages will contain only one component (e.g. authentication), others will contain a collection of smaller components (e.g. form field components).

The goal is to clearly define dependencies between packages (as opposed to stuffing everything in a single package, with more implicit internal dependencies).

Also, we don't just specify the package name as dependency but also the version. Using Semantic Versioning and a changelog per component, we'll maintain a good amount of transparency in compatibility between components and apps.

Conventional Changelog

Conventional Changelog is a set of Git commit message conventions, and a tool to generate a readable Markdown file based on your Git commits.

Nobody wants to maintain a changelog manually. Plus, a wise developer once told me that "later equals never". It's best to describe the changes while you are actually doing them, in your Git commit message.

The relevant Git commits will automatically appear in the changelog, grouped per component. (By default, only commits of the type "feature" and "bug fix" appear in the changelog. Other types are ignored.)

The convention in short:

{type}({scope}): {subject}
{BLANK LINE}
{body}

A few simple examples:

feat(authentication): add server request to #logout()
fix(authentication): notify #logout() promise of failed attempt
Use case: `authentication.logout().then(onSuccess, onError)`
Before: onError never called
After: onError called if response status is 4xx

Dgeni, TypeScript, Typson & JSON Schema

Dgeni is an extremely powerful documentation generator framework. It's built by people from the Angular team, particularly suitable for Angular projects but I've used it successfully for non-Angular projects as well.

We use the following Dgeni packages:

  • NgDoc: Generates API references for anything that is part of JSDoc, with added support for directives, controllers, filters, services and providers.
  • Examples: Generates runnable examples. We use this to provide each component with a demonstration of its features, along with code to get people started with using the component.

In addition, we created a custom "Schemas" package to document JSON contracts, component parameters and their connections. The package contains:

  • A file reader: Reads out TypeScript files and adds all the defined interfaces to the documentation index (using doc type "schema").
  • A processor: Parses the "schema" docs through the Typson JSON Schema generator.
  • A rendering filter: Connects documented @params (from directives and functions) with a schema doc if there is one with a matching name.

The Dgeni framework resembles Angular in a lot of ways. The package interface, using Angular's injector on Node.js (of course), makes it very easy to create your own documentation generator based on your specific needs.

Anatomy of a component

To make everything just a bit more visual, let's take a look at the contents of a component.

File structure template

PROJECTNAMESPACE-COMPONENTNAME/
  bower.json
  index.js
  COMPONENTNAME-{directive|filter|service}.js
  COMPONENTNAME-{directive|filter|service}_test_unit.js
  COMPONENTNAME-directive_test_e2e.js
  COMPOENNTNAME-directive.html
  COMPONENTNAME-INTERFACENAME.ts
  example/
    manifest.json
    index.html
    (+ any other files listed in manifest.json)

A component is essentially a Bower package (bower.json) containing a single Angular module (index.js). These are the entry points for other components that include it as their dependency.

Most other files are referenced only internally, or used by build processes (e.g. test runner and documentation generator).

For testing, we distinguish between Unit and End-to-End (E2E) tests:

  • Components that publish APIs will need unit tests to verify that all information is processed and returned as expected.
  • Visual components will need E2E tests to verify that all information is rendered as expected.

File structure example: login form

Let's take a look at an extremely simplified version of a login form component. It's a visual component that accepts a specific JSON structure to build up the form fields and action buttons. Then, all the hard work (actually logging in, performing form validation, toggling a modal containing the form, etc) is delegated to other components.

PROJECTNAMESPACE-login-form/
  bower.json
  index.js
  login-form-directive.js
  login-form-directive_test_e2e.js
  login-form-directive.html
  example/
    manifest.json
    index.html
    index.js
    login-form-response.json
    login-action-success-response.json
    login-action-error-response.json
bower.json
{
  "name": "PROJECTNAMESPACE-login-form",
  "version": "X.Y.Z",
  "dependencies": {
    "PROJECTNAMESPACE-authentication": "X.Y.Z",
    "PROJECTNAMESPACE-form": "X.Y.Z",
    "PROJECTNAMESPACE-modal": "X.Y.Z"
  }
}
index.js
angular.module('PROJECTNAMESPACE.components.loginForm', [
  'PROJECTNAMESPACE.components.authentication',
  'PROJECTNAMESPACE.components.form',
  'PROJECTNAMESPACE.components.modal'
]);
login-form-directive.js
angular.module('PROJECTNAMESPACE.components.loginForm')
  .directive('PROJECTNAMESPACELoginForm', loginFormDirective);
/**
 - @ngdoc directive
 - @name PROJECTNAMESPACELoginForm
 - @module PROJECTNAMESPACE.components.loginForm
 -  - @param fields {FieldType[]}
 - @param actions {FormAction[]}
 */
function loginFormDirective { ... }

The documented parameters match with some of the type definitions (next section), which are provided by components listed as a dependency for the login form component.

Used type definitions
NAMESPACE-form/field-type.ts
enum FieldType {
  TextField,
  NumberField,
  DateField,
  (etc)
}
NAMESPACE-form/form-action.ts
interface FormAction {
  label:string;
  name:ServerApi;
}
NAMESPACE-server-api/server-api.ts
enum ServerApi {
  Login,
  Logout,
  CreateAccount,
  ResetPassword,
  (etc)
}
NAMESPACE-form/text-field.ts
interface TextField extends Field {
  value:string;
  validations:TextFieldValidations;
}
NAMESPACE-form/text-field-validations.ts
interface TextFieldValidations {
  required:boolean;
  minlength:number;
  maxlength:number;
  pattern:string;
}
NAMESPACE-form/field.ts
interface Field {
  label:string;
  name:string;
  help:string;
}
Tests

In this example we have a login form directive, which is just a visual component, delegating most of the work to the APIs of other components.

The components containing the APIs would contain unit tests to verify that all information is processed and returned as expected.

The login form directive (and the smaller visual components, e.g. form field directives) would contain E2E tests to verify that all returned information from the APIs is rendered as expected.

The "example" folder
manifest.json
{
  "module": "PROJECTNAMESPACE.examples.loginForm",
  "files": ["index.html", "index.js"]
}
index.html
<div ng-controller="ExampleController as example">
  <PROJECTNAMESPACE-login-form
    fields="example.loginForm.fields"
    actions="example.loginForm.actions">
  </PROJECTNAMESPACE-login-form>
</div>
index.js
angular.module('PROJECTNAMESPACE.examples.loginForm', [
  'PROJECTNAMESPACE.components.loginForm'
])
// Example-specific components
.controller('ExampleController, ['$http', ExampleController]);
function ExampleController($http) {
  var controller = this;
  $http({url: 'login-form-response.json'})
    .then(function(response) {
      controller.loginForm = response.data;
    }));
}

Anatomy of an app

The app is essentially also just a component, called "app". Its responsibility is to bootstrap the application, delegating everything else to the other components.

Steps that could be part of the "app" component include specifying the server API(s), setting up the main router, performing the first request which can then be handled by all sorts of components which will take everything from there. The application starts to live its life once that is done.

So much to talk about!

With this blog post I wanted to give you an overview of the way we work with Angular. So many details have been left out, though. I'll be addressing some of these in separate posts.

Topics that I'm considering, include:

  • Using the CQRS to abstract away the server implementation (e.g. HTTP, WebSocket) from data components.
  • Building your own documentation generator using Dgeni, using a couple of use cases from different projects that we did.
  • A walkthrough of a complex component design: designing the API, writing tests for it, implementing the component, building a runnable example, etc.
  • Leveraging the latest features of ngModel and ngModelOptions to build a data driven form component library. I'm also speaking at meetups regularly about some of these topics. Currently, my favorite meetups are:
  • Frontend Developer Meetup Amsterdam (organized by The Frontend Lab)
  • AngularJS AMsterdam Meetup (also organized by The Frontend Lab, will probably move towards a more general framework meetup instead of just Angular)
  • Dutch AngularJS Group (organized by, let's call it "Carmen Popoviciu & Friends" :-D)
← All blog posts