A collection of articles, apps, and other digital resources, thematically tied to the subjects of art, design, programming and general philosophy. All content created by "The Imp".

This website does not use cookies, because they are high in sugar and saturated fat. (Yes, they are tasty too, I know, I know...)

Building a simple web component framework from scratch

01/05/2019

In 2019, the act of modularising web application code as web components is an incredibly popular practice. By a wide margin, it is, in fact, the most popular way of structuring javascript code in non-trivial applications. Dozens of javascript frameworks, including Polymer, Vue, React and Angular, are built around the concept of the self-contained web component, and any large scale web application not leveraging any of these frameworks will likely utilise a web-component-centric architecture in some other way. As we shall see, it is also quite trivial to establish such an architecture with just a small measure of custom code, and this can work to bring structure and modularity to a simple project - in the absence of dependencies. Any developer seeking to bring more structure and modularity to their javascript code can greatly benefit from adopting, or at least learning about web component based architecture.

What is a web component?

In general terms, a web component is a small assemblage of HTML, CSS and javascript, which describes the structure, appearance and behaviour of a single class of graphical user interface widget. For lack of a better word, we can clarify "widget" to mean "a logical fragment of a user interface". A web component may be a slideshow, a colourpicker, a header, a calendar, or even something very small like a button or input box. Notably, a web component may have several instances of itself operating on the page at any one time. For example, an interface designed to help a user specify arrival and departure dates may have two instances of a datepicker component. Breaking a user interface down as widgets, as opposed to the larger unit that is the page, allows developers to fluidly recombine parts of the interface in different configurations to create new pages and dashboards, affording greater code reusability, and working to mitigate code duplication.

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 leftmost image shows an unedited screenshot of ClassicFM's twitter homepage. In the middle image, the interface has been divided fairly judiciously to delineate a set of web components, as per the comments above. There is a menu bar type assemblage at the top, a channel-info box to the left, and a channel feed in the middle. If we were to recreate the website using these delineations as web component building blocks, we would (most likely) have a directory for each of these user interface areas. Each folder would then contain the HTML, CSS, and Javascript (and in some cases, the images) that are necessary to describe the structure, appearance and behaviour of each respective widget. The code below demonstrates what this might look like, in the context of the "Sign up" component. (If it were slightly simplified).

HTML CSS Javascript (written around an imaginary web component framework)

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.

The remainder of this article will explain how a rudimentary web component framework can be written in only 100 lines of javascript.

Purpose

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.

Resposibilities

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?

A web component architecture must create a javascript object - an instance of a particular web component "class" or constructor - when HTML that is specific to the component in question enters the DOM. The framework must then associate the javascript object with the root node of the newly inserted HTML. If, and when, the HTML root node is removed from the DOM, the corresponding javascript object should then be deleted.

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.

Structure

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 registry is simply an area that stores javascript objects - web component instances, specifically. If a page consists of four buttons and two slideshows, the registry will contain six things. It is a singleton object.

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 uiComponentRegistry.add(myComponent). Doing so will add the component to the private, native javascript array within, and give an ID attribute to both the component object, and the corresponding HTML element. This is simply a formality to make the elements easier to locate with precision should the need arise.

The completed component registry script

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.

The 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 Slideshow, Button or SignUpCta. Or, in the case of our application, it could be Message or MessageList. $node is the jQuery representation of the DOM node that we wish to mount an instance of the component "class" onto. Finally, data is a plain javascript object that can be used to pass component-specific data into the component's constructor function, when it is invoked. In the case of a button component, the data may be the button text. In the case of a comments list component, it may be a list of comments that the list should be initially populated with upon page load.

The first iteration of the component base prototype script

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.

However, the 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.

HTML removal listener implementation

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.

The 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.

The empty init and 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.

The 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 Main. Bearing such a short, unoriginal name, in a larger project that does not utilise javascript modules, this identifier could benefit from being named something else in order to avoid collisions. However, in the context of a little demo application, it is fine.

The container component

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.

Lines 4 and 5 are the implementation of a common javascript pattern that is used to establish classical inheritance. Specifically, this works to create the previously discussed base class / subclass type hierarchy between the base component blueprint, and the container component blueprint. The mechanisms behind the establishment of classical inheritance in Javascript are outside the scope of this article, but more information regarding 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.

The message input component

The 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 message list component, partial

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 Message component.

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

Finalising the application is now a case of combining all of the existing scripts in an HTML file, and initiating them with an entry point javascript. The entry point javascript can be seen in the code below as the last included script, app.js. This script exposes a single global variable, with an identifier of app. A single method is exposed as a property of the app object, 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.

The HTML

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 app.js script

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 MessageInput and 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 message list component, partial

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 app.js file, and replace it with a slightly different set of invocations that simply initialise our javascript web components upon existing HTML structures. Such an approach tends to be particularly well-received amount backend developers who are used to writing server-side templates. While it is a little old-fashioned, and slightly less elegant than client-side templating, I would argue that there is nothing about it that is inherently and significantly untoward.

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.

The alternate HTML body

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 alternate javascript

Now, a list of component and component-data pairs are used to dynamically mount javascript web components upon any pre-existing elements that match specified root-node selectors.

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 Message component HTML dynamically does emerge, however. We will not discuss the solution to this problem in detail, but it should be sufficient to note that it can be overcome by removing all templating code from the web component javascript, and utilising HTML 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:

Limitations

As discussed, potentially, the most limiting feature of the framework described above is a clunky component composition syntax.

The 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.

Afterthoughts

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.

Footnotes

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.

Comments (0)

Replying to: Noname

Error

Utilising this page as a billboard for marketing purposes is not allowed. Any messages posted by users that appear to be commercial in nature will be deleted, and any user found breaching this term will have their IP address reported to ICANN. This may result in their networks appearing in worldwide electronic communication blacklists.