This first post in the series will cover some of the concepts applied and introduce our solutions for truly reusable components.

I’ve had the opportunity to work from scratch on the Front End of a few high availability, scalable enterprise grade solutions with Angular. These solutions, despite being for different market sectors, had a lot in common: they were mostly composed by forms, grids and access controls. Every system specification and design had to offer an evolutionary architecture with low coupling and high cohesion while keeping overall complexity as low as possible, since teams grow, requirements change and we want to keep everything tidy and in place.

In this first post I’m going to introduce some of the concepts we applied and go through our solutions for reusable components. Future posts on this series will go more in depth to present the patterns that we’ve identified while solving problems present in these systems, such as Architecture, Forms, Model Driven Development and other topics that might come up. It’s important to point out that most of what is going to be presented isn’t exclusive to Angular. These are problems and solutions that can be applied to various frameworks and technologies.

A New Hope

Starting a new project is a great opportunity to take past experiences and lessons learnt to improve wherever you can. When we’re further down into the development of a project, it’s easier to see the bigger picture and the limitations that some of the initial decisions reflected on the code base, because you’ve been living with those pains for a while.

Looking back, I can say that with each new project we invested more and more into putting a lot of OOP theoretical principles into practice. They turned out to be the foundation of all projects. The same goes for design patterns led by the SOLID principles. Interface based programming is also important, as it allows us to move faster and take architectural decisions with more confidence in their scalability.

“Program to an interface, not an implementation”

For example, when a behavior needs to be shared across various components, you can create an abstract class for it (or add it to an existing one) and then have other components extend it. Or consider that you have a widely used component that does one specific job, but on one specific screen it needs to have a completely unique behavior. Let’s say that it’s a data table that on one specific screen has an “in row edit mode” that needs to make an API call to persist the data.

Instead of adding all of this to the existing grid component and hide the behavior behind a boolean for the others, we can create a new component, extend the existing one and override it’s methods as needed so that all of the existing grids don’t have to carry something they don’t need.

Forms benefit heavily from this. If you are using a Dynamic Forms approach, which I strongly suggest, creating an individual component for each of the inputs presentation and a class for its behavior enables Separation of Concerns, that on the long run will really help with the projects maintainability. Imagine that you create an autocomplete that goes well for most of the project, but one day, you need to add multi options select to it. Once again, instead of adding this behavior to the existing component and hiding it behind a Boolean, you can create a new component for its presentation and share most, if not all of the existing behavior. I will go in depth in reusable/dynamic forms for a future post, since it usually is one of the most important parts of enterprise solutions.

Reusable components as first-class citizens

In an application where most components are similar, making them as much configurable as possible is a natural path to go down through. Creating reusable components and abstractions for 80% of the use cases is a challenge, but the real struggle starts when you need to specify and implement specific behavior for the other 20% without introducing bugs or breaking changes to the remaining others. The refactoring has to be constant at the speed things move.

One strategy we applied was to give to every component an unique ID as an @Input, so that when it loaded, it would retrieve and load it’s own configuration from a component configuration service. This would allow different components to have different configurations in a centralised, decoupled manner. Also, If we wanted to reuse that component with the same config in another context, just give it the respective ID and it will take it from there.

For example, let’s take a Data Grid component that has to retrieve data from an API, paginate, toggle columns, filter fields and so on. We can say that its config will contain the columns, the API route it will call and the inputs that will be used to materialize the filter form. When the component is instantiated by Angular, it will bind to its @Input component ID, find its respective configuration, set internal properties and move on from there.

Let’s consider we have one shared Data Grid component that is reused in two different pages with the following IDs:

export enum ComponentId {
    DataTableA = 'data-table-a',
    DataTableB = 'data-table-b'
}

 

An enum will give us a strong set of constants so that there are no duplications or magic strings when the application scales.

We can then set a different configuration for each one:

export const componentConfigs = [{
    id: ComponentId.DataTableA,
    url: 'Users',
    cols: [{
        property: 'firstName',
        label: 'First Name'
    }, {
        property: 'lastName',
        label: 'Last Name'
    }],
    actions: [{
        label: 'Remove',
        handler: (row, context) => {
            context.httpClient.delete({
                url: `/Users/${row.id}`,
            });
        }
    }]
}, {
    id: ComponentId.DataTableB,
    url: 'Cars',
    cols: [{
        property: 'model',
        label: 'Model'
    }, {
        property: 'hp',
        label: 'Horse Power'
    }, {
        property: 'cilinders',
        label: 'Cilinders'
    }],
    actions: [{
        label: 'Edit',
        handler: (row, context) => {
            context.httpClient.put({
                url: `/Cars/${row.id}`
            });
        }
    }]
}];

 

Storing callbacks is one of the strongest advantages of this pattern. It is the main way of providing different behaviour to a common shared component, as they will rely on the configuration to handle specific events.

The Data Grid might have one way to remove a row in one page, but a completely different one in some other page. It might even need to do an extra validation or extra HTTP Request. Providing callbacks allows you to implement a concrete event handler for a generic shared component that is completely agnostic to the domain he is being instanced on.

We can now reuse this component in different pages, adapting it to each context as needed:

(Open on StackBlitz)

Now that our components have a configuration provided by the component registry, to build upon good practices, they should not keep any other state variables as we will always aim to go for stateless components with a single source of truth. This means that data retrieved from the API, actual selected row and any other data that you would store on a variable should be kept inside the configuration.

Let’s create a Component Registry Store to handle this:

export  type  State  = { [key: string]: object };
const state: State = {};

@Injectable()
export class ComponentRegistryService {
    private subject = new BehaviorSubject<State>(state);
    store = this.subject
        .asObservable()
        .pipe(
            distinctUntilChanged()
        );

    constructor() { }

    select(id: ComponentId): Observable<object> {
        return this.store
            .pipe(
                pluck(id)
            );
    }

    add(id: ComponentId, config?: object) {
        if (!config) {
            config = componentConfigs.find(c => c.id === id);
        }
        const value = this.subject.value;
        this.subject.next(Object.assign({}, value, {[id]: config}));
    }
}

 

Below you can see the changes made to other elements of the application:

(Open on StackBlitz here)

This will allow us to, for example, save a component’s actual state for later use, apply undo and redo operations and so on. We can even store it in the localstorage for when a user opens the app in a new browser tab, having the exact same components state as he had previously. We can even use websockets and sync components in different clients, as this opens a lot of possibilities.

We can now introduce the concept of unregistered components, which subscribe to a registered components data. What differentiates them from the others is that they have no explicit Component ID. This makes sense in scenarios where you have nested child components or relations between a group of components.

Let’s add filters to our Data Grid. These filters will show an input per column and when typed on, will filter the rows beneath. The filter will be a new component that will interact with the Data Grid through the store.

(Open in StackBlitz here)

As you might have noticed, quite a bit has changed. Since we want our store to be immutable, the select method in the Component Registry now returns a frozen object. This makes us dispatch a new config every time we want to make any change over it, making the chances of getting undesired side effects greatly reduced. The filter input in the Data Grid Filter component has a keyup event bind that will dispatch a new configuration to the store, triggering the Data Grid configuration subscription to update to the newly dispatched state and apply the filter to its rows.

Since we are subscribed to a specific ComponentID in both components, every time one makes a change, both will update their configuration. This is a major advantage of using this pattern.

You might have also noticed that there is now some code that is common across shared components, such as the store selector and configuration unsubscriber. We can abstract this behaviour to a Base Component Class:

export  abstract  class  BaseComponent  {
    @Input() id:  ComponentId;
    private config$:  Subscription;
    private _config:  object;
      
    constructor(protected componentRegistry:  ComponentRegistryService) {}
      
    init(callback?: Function) {
        if (this.id) {
            this.config$ = this.componentRegistry.select(this.id)
                .subscribe(config => {
                    this._config = config;
                    if (callback) {
                        callback();
                    }
                });
        }
    }
      
    get config() {
        return this._config;
    }
      
    dispose() {
        this.config$.unsubscribe();
    }
}

We’ve added a callback to give the components a hook to act upon configuration subscription and protected the config with private visibility, only exposing a getter in order to further limit side effects with direct mutations over the Component Registry Configuration.

And you can see here the final result:

(Open on StackBlitz here)

You might come across scenarios where a group of components that are somehow related need to have an action that was triggered one component to be handled in one or multiple different components. In this case it’s better to implement an Event Bus to pass messages and payloads between components than extending the Component Registry to handle this, as its single responsibility is storing component state, not handling component events.

Let’s assume that our Data Grid, because of growing complexity in the requirements, needs a component for each row. This component will be inside a For loop on the Data Grid template. We will not create a Component ID for each row, so this is where we might add an @Input to the Data Grid Row component to pass data down.  This is a scenario where we could take advantage of an Event Bus to pass actions between the two, to keep them as decoupled and predictable as possible.

Taking this pattern and applying it on top of ngrx instead of creating your own store is an alternative way of implementing it, depending completely on your needs and vision for scalability.

This lays the foundations for what can be a centralised way to handle reusable components, handing down concrete implementations by keeping components open to extension but closed for modification.

On the next post I will describe how we can create Models to further enhance the component configurations and how they can be used to generate Forms.

 

Written by Nuno Rocha | Front End Lead at Cleverti