Simplify your web apps: Build building-blocks
The basis of any good architecture is a set of well defined boundaries. Whether we’re building a larger system consisting of microservices or a user interface, being able to define and constrain our interfaces is what allows us to simplify, reuse and replace components.
For a long time building browser based user interfaces has been a bit of a mess; No componentization without a significant amount of custom code and no standards around these components causing a very fragmented landscape; Angular directives, React components, jQuery plugins and so on.
Recently the browser platform has gotten a very important construct that allows us to build well-defined, independent components in a standards compliant way; Custom elements.
Along with the introduction of custom elements the dependency on frameworks becomes smaller. We now have a way structuring our apps right out of the box. I’ve never been a big fan of frameworks myself – I think because they move around too much, my timeframe for learning and using stuff is much longer than the stability of any framework. Besides, I think its good to know the primitives before learning the abstractions.
The browser API surface moves too, but at least it is governed by standardisation and, when stuff changes in a breaking way, it usually has a long period with a deprecation warning.
My goal for any kind of project is to get to a point where adding new feature feels like starting a new project. I want to touch as little of the existing code as possible. Custom elements helps us getting to this place; they allow us to simply create our own tags. Those tags can be anything from a simple date element, that automatically converts itself to the local time of the browser, to entire applications maybe consisting of many more custom elements.
Because custom elements are just tags, all external features was given way before they were even thought of and that’s what makes them so useful – they fit so well into the ecosystem; document model, event cycle and data flow.
Only styling is a little bit different – when we want it to be; shadow DOM allows us to style a component isolated from the rest of our app but we don’t have to use it. We can choose to let our components be styled like any other element.
Data flow
For an element to be useful, we have to be able to get data in and out.
This is an interesting problem area - because it is one of the places where the frameworks shine. But even without frameworks we can find some good solutions.
In the basic scenario we just set our elements attributes.
The local time element mentioned earlier simply has an attribute that allows us to set an universal date and time that is then converted internally to a local representation:
<local-time utc-date=”2019-01-01 20:28″></local-time>
The element can then use this value when it renders itself and also choose to react to changes to the attribute.
But what happens when the data gets more complex than what we can express as set of attributes? – lets say we want to display a list of user-details.
One way of achieving this is by simply serializing the whole list, escaping it and again adding it as an attribute. This is not really the recommended way. First of all no standard HTML elements do this. Secondly it’s quite a bit of work for the browser to not only express this potentially huge attribute but also to both escape and serialize it.
Second solution yields very much to the way HTML is structured already; expressing our data using markup: (inspired by the select element).
<user-list> <user name="luke" email="luke@erebel-alliance.com" /> <user name="leia" email="leia@erebel-alliance.com" /> </user-list>
This fits very well into the way HTML already works, and requires no second parsing step, we can simply read the child-elements and their attributes. This is how one would expect an element to work, and because of this it should be the go-to method when building independent generic standalone components.
Third solution is similar to second but using JSON instead of HTML markup:
<user-list> <script type="application/json+users"> [ { "name": "luke", "email": "luke@erebel-alliance.com" }, { "name": "leia", "email": "leia@erebel-alliance.com" } ] </script> </user-list>
Here we don’t have to escape our data, since they are surrounded by an element, but we still have to parse it in a separate step.
While this solution is less HTMLesque it still have its uses – it requires much less work when we change our data, and the representation is more compact.
Both second and third solution has the obvious problem that we don’t have a function-call when our data changes, but this can be solved using mutation-observers that can raise events based on changes to child elements.
Finally, the problem of reading data could in some cases simply be solved by our architecture. Our user-list element calls UserStore.GetUsers() and displays the data. This means no parsing and no additional markup but ties our component to our infrastructure.
Which solution you choose depends on your situation. Most solutions I’ve worked on I’ve been using a combination of the above.
Events
When our element needs to tell the world about a state change it can raise an event. As element authors we can decide whether events of child elements should be allowed to bubble out of the element or be captured and converted into our own custom events for total isolation. We can create our own events by using the built-in type CustomEvent which can include any amount of structured data via the detail argument:
this.dispatchEvent(new CustomEvent("change", { detail: { user: "luke" }}));
Rendering pipeline
When a component becomes part of the page it gets a function call to signal that it is time to start rendering its content. In a custom element scenario this can be as simple as adding a few child elements via the object model to showing a larger portion of HTML.
Displaying HTML is typically done by setting the elements innerHTML property to a string containing markup that is then parsed by the browser.
To support multi-line and access to methods and properties we can use template literals:
this.innerHTML = ` <h1>User ${user.username}</h1> <p>Email: ${user.email}</p>`;
This is fine as long as we can settle for re-rendering the whole element when changes occur but can potentially become problematic if we need to re-render lots of markup. Adding and removing DOM elements is known to be expensive. Some of the frameworks solve this by building the tree outside of the normal DOM, comparing it to the actual DOM and only applying actual needed changes. There are also libraries like lit-html and hyperHTML that reduces the problem by using features around template literals to add some intelligence to DOM updates.
Personally I haven’t found this problem to be profound. With sufficiently small components, the intelligence around which parts to update can easily be built into the element. Also understanding and utilizing the browser’s event-cycle can diminish these problems.
Custom elements helps us improve the way we build web applications. By defining a strict surface area we develop, upgrade and replace parts without causing negative ripple effects for the rest of our applications.
Having the right primitives is the basis of successful web development, and the browsers are certainly getting there. Custom elements has long been one of the missing pieces and in the mean time we’ve been filling that gap with libraries and frameworks. But the browser is growing up, and we may not need these abstractions anymore.
About the author
Poul Foged Nielsen is an Associated Software Craftsman. When he is not helping customers build scalable software solutions he travels the globe with his laptop, working on Linkstacks - a feed reader and bookmark manager.
What is an Associated Software Craftsman?
Our Associated Software Craftsmen (ASM) are strong representatives for solid craftsmanship. Poul is one of our very respected ASMs who has a special angle and attitude towards development of web applications. We believe that they are interesting and true aspects to consider before deciding to take a short cut with modern web development frameworks.