Web components for us are a means of consistency, an assurance that required functionality for a feature will be available and above all, we can re-use it across our platform.

We needed the ability to upgrade standard HTML elements, allow them to provide a specific function based on available features and reuse them; a date and time since an “event” took place shouldn’t only be functional in the context of a story, it could be a system event such as a long running task or save action.

Each component should work in isolation or as a collective using higher order composition. A component should improve the experience, but not be the only experience a user receives.

i.e. Published date/time should always be available, if conditions are right, it could display time since publication

Component Anatomy

Web components go through 5 lifecycle events: created, ready, attached, detached and attributeChanged.

We use ReactJS which makes sense for us as React matches most of these lifecycle events with it’s own: register, mount and unmount. There are a couple intermediate events for mounting and un-mounting which provide greater control over the DOM upgrade process, pre and post actions. React also handles property and attribute changes internally before rendering.

Our components extend a BaseComponent which provides a method for declaring required functionality, these capabilities are resolved to determine whether the browser will offer sufficient functionality for the component to be loaded — think of capabilities as a decorator providing functionality.

Mixins aren’t a good choice, particularly if a capability may be offered with many abilities (local storage, indexDB) and taking into account that they are deemed to be “dead” by the majority of the community, composition is your friend!

Lets look at some markup:

<TimeAgo data-time="Thu Jun 23 2016 21:19:31 GMT+0100 (BST)" data-format="full">Thursday 23rd Jun 09:19pm</TimeAgo>

When loaded our component `TimeAgo` will display the amount of time since the story was published.

Rendered Component `Time Ago`

Resolving Capabilities

We have (currently) 3 publications, each one requires the following:

Ability to present stories in 2 forms: micro (cards) and macro (full page)

Stories should consist of a title, meta-data for published date/time, author and category/tags

Stories may need to show/update trending data: share counts

Each publication will render advertising in-place within a story body or alongside content

There’s a couple more requirements, but lets stick with these for brevity.

Taking a quick note of features, we’re going to need to make requests (fetch), store data (local storage, indexDB) and track some user interactions (Google Analytics).

As mentioned we call these features “capabilities” they don’t have to be browser APIs, we also have service APIs such as Google Analytics, that may be required by a component to fulfil it’s purpose.

A capability defines a requirement, what is needed in order to use it and a function it can perform.

We have a guideline when creating capabilities, it has to be used by more than one component and each component must be used within a separate context. This prevents capabilities becoming SDKs and general utility libraries.

As a capability might have more than one API or feature capable of providing the functionality, adapters are used and act as a fluid interface, keeping the methods consistent. An example would be browser storage — indexDB vs local storage.

Each Capability declares a method `isCapable` which returns a boolean value based on whether the requirements of a capability is met.

We’re going to write a local storage capability as an example.

static isCapable() {

let test = 'test';

try {

localStorage.setItem(test, test);

localStorage.removeItem(test);

return true;

} catch(e) {

return false;

}

}

When our component is registered and about to be mounted, we run through it’s capabilities and assert whether the conditions meet the requirements above. If we don’t meet the conditions, we don’t mount the component, if they do we’ll proceed in checking against any other capabilities. Once all capabilities have been resolved and are available, we mount the component, upgrading the HTML element and enhancing the user experience.

This is handled by the capability resolver which is responsible for the resolution of each capability and determining which adapter should be passed to the component to provide it with the required functionality.

static hasCapability(capability) {

// If unrecognised capability, fail check

if (Object.keys(this.capabilities()).indexOf(capability) === -1)

return false;



// Test whether the capability is available

return this.capabilities()[capability].isCapable();

}

Handling Components

Upgrading the DOM is as simple as registering a component which will upgrade a HTML element. We use what we’ve called the “component handler” which is responsible for upgrading DOM elements and mounting our components.

// Declare component

let TimeAgoComponent = require('component-time-ago'); // Instantiate the component handler

let ch = new ComponentHandler();



// Register component with the handler

ch.registerComponent(TimeAgoComponent, 'TimeAgo');



// Upgrade the DOM

ch.upgradeDOM();

We can register and mount as many components as we require, the “component handler” even handles conflict resolution by utilising a FIFO stack based on whether a component fails to resolve, therefore the next component registered in the stack will be mounted onto that element.

We don’t recommend you do this, however there might be times in which you need to do so.

The registerComponent method takes a component as the first parameter and a querySelector for the second parameter. This could be an element, a class or an id or any combination supported by the HTML DOM querySelector method.

For every instance of our TimeAgo element, we’d mount our TimeAgoComponent. You could if you wished provide separately configured components for different query selectors.

e.g. At the top of our article we may want only the author’s name, at the bottom, we want our BioComponent to provide more details around the author, therefore we can configure our component twice with each additional requirement and mount it on separate elements using the same component.

... // Copy data- attributes to props

var props = this.getDataAttributes(el);

for (var prop in globalProps) {

if (globalProps.hasOwnProperty(prop)) {

props[prop] = globalProps[prop];

}

} ...

When we upgrade the DOM element we extract any `data-` attributes that may be present and pass those as props to our React component.

// Pass each of the capability's adapters as a prop

let globalProps = {}; // May container higher order props caps.forEach((cap) => {

let capClass = CapabilityResolver.capabilities()[cap];

globalProps[cap] = new capClass(this.config);

});

Any capabilities required and resolved by the capability resolver, will also be passed to the component.

renderComponent(component, props, el) {

ReactDOM.render(

React.createElement(component, props, el.innerHTML),

el

);

}

At this stage we now have an upgraded DOM, at least some if not all of our components and their capabilities have been resolved and provide an enhanced user experience, otherwise we fallback to our baseline experience.

Subscribe to our publication to find out more about our engineering team