Building a simple web component framework from scratch
What is a web component?
The images below show the website, Twitter. It may not necessarily use web component architecture (although it is very likely that it does), but we can use the outward appearance of the interface to demonstrate web component architecture by means of drawing boxes to describe how the interface could be split up into web components.
The rightmost image contains further subdivisions of some of the components, marked with blue boxes. This is to illustrate that web components may be nested, and also that web components may represent very small interface fragments. Splitting up interfaces into very small components is generally a good thing to do, as it allows for greater freedom, and less code duplication, when recombining parts. However, it is worth noting that it can sometimes be tedious to recombine dozens of tiny fragments, but the existence of this disadvantage is contingent on the framework that has been used. So in the context of larger projects, this is perhaps, more an argument to use a robust, fully-featured framework, than it is an argument to avoid highly granular interface divisions.
How to build and use a basic web component framework
Above: A simple message board. This is what we will make. Click the image for a larger view, or here to visit the live application.
Before we begin, I should clarify that I am not advocating the use of severely simplistic web component frameworks in the context of a large or commercial project. Despite a tendency to be evangelistic in the elimination of unnecessary complexity, dependency and abstraction, the features that a third party frontend framework, such as Angular, React or Vue will provide, do generally make a third-party solution a much wiser choice, in most situations, when trying to establish a suitable web component architecture.
With that said, I am not entirely condemning the use of a minimalistic, custom-made frontend framework either. In situations where the server is responsible for rendering HTML, and nested components are rare, the need for a flexible component-compositing syntax evaporates, and a simple client side web-component-instantiating and mounting system can suffice.
With this in mind, the web component framework that will be demonstrated should be considered a learning exercise for the curious, a starting point for the industrious, and a full solution for the brash.
The main responsibility of a web component architecture is the instantiation, mounting, and unmounting of components in response to DOM alterations. But what does this mean?
When a web component is mounted onto a DOM element, it typically initiates event listeners that will serve as entry points into the component's custom behaviour. For example, a slideshow component will likely add event listeners to clicks that hit encircled pagination controls, and these could invoke functions within the component that are designed to change the active slide.
A web component framework should also ideally provide a way to assemble various components into composite webpages. This is particularly the case when the server is not being used to render markup, and a more puritan, fully client-side approach is employed to implement templates.React JS code
Above: React JSX code. It uses an HTML-like syntax to declaratively describe the composition of individual components.
A web component framework may also provide a way for components to pass events and data among themselves, and this is particularly likely to be the case with a fully featured framework. However, in this demonstration, we will simply rely on custom DOM events - mediated by jQuery for the sake of conciseness and clarity. Coincidentally, this approach is the same as the inter-component event propagating system that the web component framework "FlightJS" uses. The "FlightJS" framework was created by developers based at Twitter.
As indicated by the inset above, we will be making a simple message board application. The finished version can be viewed here. Four key things will make up the application in its entirety:
- The component registry
- The component constructor
- The components
- The entry files
The last two items in the list are application-specific, whereas the first two form the framework itself.
The component constructor is a constructor function that all web component constructor functions are prototype-linked to. It can be thought of as an abstract, base class that all concrete, web component classes extend. The component constructor will also contain the functions that can be called upon to mount and unmount components. As semantically static members, these latter functions will be properties of the constructor function.
Before we get into details, take a look at the diagram below. It shows how all these things fit together macroscopically to produce the application. Note how each web component instance is mounted to a DOM node.
The component registry
The component registry singleton can generally be thought of as a specialised kind of array. Component instances may be added to it by invoking
The component constructor
To begin with, the component constructor function only has a simple function body, and two member functions: One to
mount a component onto a DOM node, and one called
create, which does this and a little bit more.
mount function is fairly straightforward. It Accepts three arguments.
ComponentClass is the constructor function for the web component that we wish to mount an instance of. It could be
SignUpCta. Or, in the case of our application, it could be
$node is the jQuery representation of the DOM node that we wish to mount an instance of the component "class" onto. Finally,
On lines 18 and 19, the
mount function invokes the component class constructor, and stores the object that it produces within the registry. The function then assumes the existence of an
init function on the web component's instance, or prototype chain, and invokes it. This is a little bit presumptuous for the sake of brevity - we can build in defensive checks shortly. Finally, the function triggers a DOM event on the root node of the component's corresponding HTML block. This way, if any other component needs to react to the mounting of the component in question, it has something to hook into. This type of event is usually known as a "Lifecycle hook".
The create function's purpose is to do all of the above, but prior to mounting, to create and append the HTML for the component. By convention, this HTML is specified by a static property of the component class. The
$targetNode argument specifies which existing DOM element the new HTML should be appended to. The
data argument is not used directly by this function, but exists in the function signature, in order that it may be passed onto the
mount function. Like the
mount function, the
create function also broadcasts a message in the form of a DOM event, to let other components know that a component lifecycle event has occurred.
This is a good start, but a few key things are lacking. First of all, there is no way of unmounting components. Such a method should exist for the purposes of freeing up memory and destroying moot component instance objects. Therefore, the following "static" method is a necessary addition.Unmounting implementation
The unmount function will take the jQuery representation of a single HTML node, and begin it's routing by first assuming the presence of, and then calling, a
deint function, which exists on the associated component instance (or it's prototype chain). This allows the component to perform any clean-up if this is necessary. The function then broadcasts a lifecycle event on the DOM node, and finally removes the object instance from the registry.
unmount function can only really come into it's own when it can be called automatically, in response to the removal of a component's HTML.
MutationObservers may be used for this purpose. A
MutationObserver will watch the DOM for any changes that match a certain criteria, and then execute arbitrary code in response. The code below creates an observer that watches the whole document for the removal of HTML nodes. Whenever a node is removed, the
UiComponent.unmount function is called, with the freshly removed node passed as a parameter. If the node is associated with a web component, the
unmount function will be called, and take care of any necessary object clean up.
Having incorporated the above additions, and a few other small tweaks, the component constructor function, and associated static methods now look like this:The completed component base prototype script
Note that three new methods have been added, and that all of these are prototype methods. This, in essence, allows them to be called as if they were properties of each "class instance", as opposed to "static" methods.
destroy method provides us with the ability to trigger the removal of a web component instance, as a function of the web component instance itself. This is just a convenience for developers wishing to write component removal code in this way.
deinit methods will prevent any runtime errors if an init or deinit method are not defined on the extending component constructor function when invoked by the framework. Admittedly, in hindsight, this is a bit of an antipattern, and perhaps a check for the presence of this property at call time would have been more tidy.
At this point, the framework is complete, and we are ready to write our actual web components.
First, we will create the container component. This is a wrapper component that gives other elements on the page a maximum width. It is a good component to begin with, as it carries little implementation, and is quite simple. The core of the web component definition is the constructor function. Here, it is called
Notice that the function calls the base
UiComponent constructor function, passing in a
this context that represents the web component instance that is being built. This is to establish the
$node object as a property of the instance - so that it can be accessed by the other functions that we may declare in this file. The calling of the base constructor function also initiates the
MutationObserver. If you are unsure of how this happens, re-read lines 1-17 of the UiComponent constructor.
Object.create in this context can be found here.
Note also the two "static" properties. Our framework has decreed that by convention, all component constructor functions should have a
rootSelector property, which is a document query string expression that can be used to unambiguously find all the root nodes of all HTML blocks that pertain to this component, and a
getHtml property, which should be a function that returns the HTML block that defines the structure for all instances of this component. In the case of the
Main component, the HTML is quite terse.
The component does also have accompanying CSS, but for our purposes, this is not really worth examining. Let's move on to a slightly more complex component.
The message input component is the widget that users use to enter, and submit, a message. Inspecting the HTML inside it's
getHtml function, you will see that it consists of a textarea, into which users may type their message, a text input, into which people may type their name, and a button, which can be used to signify that the message should be sent or stored. In our application, the "saving action" converts the message from two string values into a
Mesasge component instance, which is then appended to the
MessageList component instance.
init method on line 18 is called by the framework when the component is instantiated. This particular component uses this as a cue to register event listeners, which are defined within a nicely segregated, additional method,
addInternalListeners. When a click event is fired on the button, the message body and author strings are extracted from the textarea and input elements. If they are not both populated, a warning is shown, and the function exits. If they are both populated, the function continues. The textarea and input box are reset, but just before this happens, an event is triggered upon the component's root node. While all this is happening, the
MessageList component is slyly listening to this node for this event, and the occurance of it will evoke a reaction. The code below demonstrates how the
MesssageList starts listening, and how it's reaction is implemented.
The reaction of the
MessageList component is to insert the new message into the message list. It does this by first destroying all HTML elements within the message list element. It then adds the new message to it's data model, (the
this.messages array), and then iterates over all members of the messages array, adding each one (inclusive of the new one) afresh. Note the use of our framework function,
UiComponent.create, which we use to create and mount a
The act of destroying all messages so that no duplicates exist in the list is a fairly straightforward solution, but it is also quite a naive solution. In destroying each message component and creating it afresh, we loose any data that is particular to that instance of the component. Here, this is fine, as the component being destroyed is simple, and the data that the new version of it is populated with is fully encompassing of the state of the old version. No data is lost. However, this is likely to be problematic when more complex components and component nesting patterns are in use. Generally, a more fully-fledged web component framework will not remove and re-create web components during update operations such as this. One has to wonder if this architectural consideration is, along with performance gains, the core reason for the development of conservatively-repainting virtual DOM systems that are common among modern web component frameworks.
The entry point
app.js. This script exposes a single global variable, with an identifier of
app. A single method is exposed as a property of the
init, which is called within a docready callback, on line 35 of the HTML below, to fire up the application. The
app.js script will be discussed in more detail shortly.
Note that the only element in the
body of the page described above is a
div with a class of
app. This is the application root node. All web component elements will be mounted upon this node - either as children or further-removed descendants.
The all-important app.js script is depicted below. It is an instance of the revealing module pattern. It defines 3 key functions, but only the
init method is exposed publicly.
The code within the
init method leverages the
create function we created earlier as part of the framework, and is essentially templating code. It orchestrates the assemblage of three web components in order to form the full interface. The script creates an instance of the
Main web component, and mounts it inside the
.app div. Then instances of the
MessageList components are created, and mounted upon the root node of the
Main component instance.
Note that a collection of starting messages are used within the creation of the
MessageList component to establish pre-existing comments. These starting messages flow through the
create method, and are passed into the
mount method. The
mount method then, in turn, passes these messages into the
MessageList constructor function.
The messages are stored as an object property,
this.messages. Now, when the HTML for this component is first rendered by a call to
updateDisplay (see the other snippet of the
MessageList depicted above), the initial message components will be instantiated, mounted, visible, and operational.
The concludes the explanation of the basic web component framework. At this stage, the system is quite stark and minimalistic, but it is in a state that is well rounded-off. It could be used to bring structure to small projects that do not feature complex layouts.
In the section above, entitled "Responsibilities", I stated that a web component framework should ideally provide a way to assemble various components into composite webpages. I also stated that, in situations where the server is responsible for rendering HTML, and nested components are rare, the need for a flexible component-compositing syntax evaporates.
The main issue with the minimalistic web component framework we have created is that it does not provide a particularly elegant syntax for collating components into composite web pages and dashboards. The syntax appearing within
app.init is quite clunky, and if it were used to coordinate the assemblage of several dozen components, it would become very hard to see the document structure that was being described. Creating a system that can make sense of React-style JSX templates is a bit outside the scope of this article. However, we can adapt the system so that it moves away from it's status as an "almost practical", client-side templating curio, and towards the semblance of a reasonably practical web component instantiation framework, that defers HTML templating to the server.
In other words, we can pull out the templating code that we had written within the
An alternate entry point
So then, let us assume that the server is providing us with our component markup. Imagine that the full HTML page that was depicted earlier in this article remains as it was, but that it has it's
body supplanted with the following markup.
In order to only mount our web components upon existing HTML, we now change our
app.init method within
app.js to consist of the following:
The system is now operating quite efficiently within a more limited sphere of responsibility.
A problem existing somewhere between duplicated template code, and the need to append
template tags to extract non-user-visible markup from a centralised location on the server.
The complete application
The complete application may be accessed at the following locations:
As discussed, potentially, the most limiting feature of the framework described above is a clunky component composition syntax.
MutationObserver implementation can be slightly resource hungry, and also, may not be triggered in response to some methods of node removal.
Despite this, the framework does afford a number of features that is quite remarkable for it's size:
- A web component architecture.
- Extendable web components, which work according to principles of classical inheritance.
- A model that lets web components of the same type occupy a single document in multiplicity.
- Automatic web component disposal that is synchronised with DOM changes.
- Client side templating.
The dependency on jQuery is quite non-essential and can be factored out easily. This can work to produce an incredibly lithe web component framework.
A component-based architecture brings excellent structure to user-interface codebases. The elegance of web component architecture is presently unrivalled, and as such, no web developer should leave home without it.
It is interesting to note that a library that is 3kb in size can afford the same basic shapes and segregational benefits common to larger web component architecture solutions. Larger, more fully-fledged frameworks such as ReactJS and AngularJS weigh in much more heavily, at 135kb and 172kb, respectively. This is not to decry the pertinence of, or implementational efficiancy of, larger web component frameworks. On the contrary, they do bring a plethora of highly relevant - sometimes indispensable - advantages that make them unquestionably favourable in many - perhaps most - situations. I am also not attempting to labour a point about the importance of stripping a web application of every disposable kilobyte.
What is remarkable, however, is the fact that it is possible to gain a fairly large share of advantages from only a minute amount of implementation. Speaking within the context of software development generally, it is sometimes the case that mature, feature-rich third-party solutions are indeed wisest the choice. However, strong architectural patterns are profoundly potent, too.
1 Conceivably, the library would increase slightly if jQuery were factored out, but would still be under 5kb in size - still, a relatively tiny size.