Top of page

Designing components with Angular

Blog

Comparing its design philosophy with Bootstrap, Polymer and Aurelia.

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:

[data-{verb}={component}]
[data-target={selector}]
.{component}-{modifier}
<!--
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>

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}
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
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)
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
{
  "name": "PROJECTNAMESPACE-login-form",
  "version": "X.Y.Z",
  "dependencies": {
    "PROJECTNAMESPACE-authentication": "X.Y.Z",
    "PROJECTNAMESPACE-form": "X.Y.Z",
    "PROJECTNAMESPACE-modal": "X.Y.Z"
  }
}
angular.module('PROJECTNAMESPACE.components.loginForm', [
  'PROJECTNAMESPACE.components.authentication',
  'PROJECTNAMESPACE.components.form',
  'PROJECTNAMESPACE.components.modal'
]);
angular.module('PROJECTNAMESPACE.components.loginForm')
  .directive('PROJECTNAMESPACELoginForm', loginFormDirective);
/**
 - @ngdoc directive
 - @name PROJECTNAMESPACELoginForm
 - @module PROJECTNAMESPACE.components.loginForm
 -  - @param fields {FieldType[]}
 - @param actions {FormAction[]}
 */
function loginFormDirective { ... }
enum FieldType {
  TextField,
  NumberField,
  DateField,
  (etc)
}
interface FormAction {
  label:string;
  name:ServerApi;
}
enum ServerApi {
  Login,
  Logout,
  CreateAccount,
  ResetPassword,
  (etc)
}
interface TextField extends Field {
  value:string;
  validations:TextFieldValidations;
}
interface TextFieldValidations {
  required:boolean;
  minlength:number;
  maxlength:number;
  pattern:string;
}
interface Field {
  label:string;
  name:string;
  help:string;
}
{
  "module": "PROJECTNAMESPACE.examples.loginForm",
  "files": ["index.html", "index.js"]
}
<div ng-controller="ExampleController as example">
  <PROJECTNAMESPACE-login-form
    fields="example.loginForm.fields"
    actions="example.loginForm.actions">
  </PROJECTNAMESPACE-login-form>
</div>
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;
    }));
}

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

Also in love with the web?

For us, that’s about technology and user experience. Fast, available for all, enjoyable to use. And fun to build. This is how our team bands together, adhering to the same values, to make sure we achieve a solid result for clients both large and small. Does that fit you?
Join our team