The quest for the web equivalent of 'Legos' has been going on for a while. Legos are composable: you can connect pieces in different ways. They are reusable: the same kind of piece can be used in multiple places. Legos are also modular: you can connect one assembly to another one. Unlike Legos [mostly], however, components for the web also need to be able to communicate with each other and with the outside world. Finally, web components need to be insulated (in Lego terms, the color of one item must not 'bleed' into others). CSS bleed has been a particular problem of older web component frameworks. To find an overall solution for great web components is not trivial, and many attempts have been made to achieve the holy grail of component-based development for the web.
In reality, component frameworks have had a short half-life. The last years have seen a rapid succession of candidates to be considered 'the best': Backbone, Knockout, Ember, Angular, Angular 2, React, Vue, Riot and Polymer. Find a comparison of component frameworks at http://jeffcarp.github.io/frontend-hyperpolyglot/. History shows that what you would pick today is likely not what you would pick eighteen months from now. Assuming you don't want to start from scratch and rewrite everything every eighteen months, that presents you with a great challenge of how to write applications that you can expect to be both maintainable and future-safe.
Our best practice to deal with the evolving landscape of web components is to write web apps that are structured in a component-framework-agnostic way, where you can potentially keep existing components around, but develop new components using a newer component framework. We achieve this by avoiding the use of the two kinds of common component framework features that create unattractive lock-in: custom navigation mechanisms (routers) and framework-specific component communication implementations. We use the browser's great native navigation mechanisms (URL and browser history) as shown in Lab 4, and framework-agnostic component communication as implemented in the sample project shown below.
There is, however, light at the end of the tunnel: a W3C standard for web components appears to be forming. Read about Standard Web Components at https://en.wikipedia.org/wiki/Web_Components. The Chrome browser already supports Standard Web Components natively, so there is nothing extra to load, and the polyfills for the other browers are lean and fast. The most recent contender for 'the best' component framework, Polymer, is actually developed on top of this emerging standard. We use Standard Web Components as the default component framework for this Lab. However, our architectural concepts can also be applied when using other frameworks. May we tempt you to write a sample integration for your favorite component framework and contribute it to topseed?
Among other things, newer component frameworks solve the problem of CSS bleeding by using the Shadow DOM construct. To work with web components you should understand Shadow DOM; read a good introduction at https://www.html5rocks.com/en/tutorials/webcomponents/shadowdom/. We prefer it over the more recent introduction at https://developers.google.com/web/fundamentals/getting-started/primers/shadowdom. HTML Templates are another Standard Web Component feature which will become clear with the examples in this lab. Finally, there is a Custom Element API to create new HTML tags. Unfortunately, this API uses 'class' which is not supported by Internet Explorer. (It is possible but a little painful to down-compile for IE.) We will use the Custom Element API once IE has lost its remaining popularity, or when we can rule out IE in a corporate app. Lab 10 uses the Custom Element API.
Download and unzip topseed-webcomps-master.zip from https://github.com/topseed/topseed-webcomps to your location of choice on your developer machine. Open the project in VS Code. Add the project folder /topseed-webcomps
in Prepros, but but ensure that Pug Auto-Compile is deactivated (Settings/Compiler Settings/Pug (Jade). In this project, Node.js compiles pug files on the fly when responding to HTTP requests so we don't need Prepros to precompile them. See the function 'pugComp' in /server/util/Decider.js
. Since in production we cache all generated HTML responses in the CDN, there is practically no impact on production performance, but the development project is a lot cleaner.
In VS Code, open Terminal Shell (Ctrl+Shift+`)
, type 'cd demo-srv [Enter]'
, do the 'npm install [Enter]'
and then 'node index [Enter]'
. You should see console output 'Web server listening at http://localhost:9981'. In the browser, navigate to http://localhost:9981, and visit Dashboard, List, Circle, Prelist and List menu items. When writing a component, we always advise to make things work outside of a component first. 'Prelist' is a non-component list page. In VS Code, inspect its /public/page/list-0/index.pug
. Similar to the admin linkblog in tutorials 5 and 6, this page uses /page/list/ListBusiness.js
to load a list from a JSON response promise. For data binding, ListBusiness
'list()'
uses doT.js, a fast 'moustache-style' template library. The doT template is embedded in the page html as a script of type 'text/x-dot-template'
. Inspect ListBusiness.js, beginning with 'var templateText'
to see the JavaScript used to render the template with data and attach it to '#myList'
in the page. You can read more about doT at http://www.javascriptoo.com/dot-js.
Inspect the component version of the list page at /public/page/list/index.pug
. It has a custom element in the HTML named 'list-el'
. Custom elements must include a '-'
(dash) in the tag name. The doT template has disappeared. The page 'script.'
loads ListBusiness.js
and the List component definition from the HTML Import /_webComp/List.html
, registers the component prototype with the browser using 'TW.registerComp'
and then calls 'sb.compList()'
to render an instance of the component. In /page/list/ListBusiness.js
, compare the function 'compList'
with the function 'list'
used by the non-component version. In this example, the 'compList'
function obtains the component instance with 'document.querySelector'
and calls its 'list(values)'
function. We always pass data to a component rather than making the component load data. This keeps the component simpler and more manageable. As a result of using a component, both the page and ListBusiness
are somewhat cleaner; any 'JavaScript mess' is hidden inside the component.
Optional: Inspect the list component implementation at /public/_webComp/List.pug
. You will find the Standard Web Component 'template'
tag that includes the node '#myList'
previously seen in the non-component page, as well as the 'x-dot-template'
. The 'script.'
section creates a HTMLElement prototype named 'ListEl'
, specifies to attach the insulated shadow DOM (using 'TW.attachShadow'
) when an actual component instance is created from the prototype ('createdCallback'
). Also see the 'ListEl.list'
function implementation which places the databinding result into the '#myList'
node in the shadow DOM. (You can find a copy of the 'TW.'
helper library function implementations in /demo-srv/root2/_js/tw-2.0.js
.)
As of the time of writing, due to less than perfect polyfills, CSS still bleeds in both directions in Firefox and Edge. For us this is not catastrophic because we avoid most CSS namespace collisions by using BEM syntax when naming our own component styles. Remaining potential issues with third party styles used inside components will disappear once all browsers natively support Standard Web Components. Read https://csswizardry.com/2013/01/mindbemding-getting-your-head-round-bem-syntax/ to lean how to use BEM; it is a best practice even when not using components. See /public/_webComp/circle.pug
for an example of BEM inside a component. You can also attempt to insulate component CSS by using a scope 'div'
; see the use of '.bgauge-el'
in /_sharedComps/gauge.pug
. It is also possible to write components that bleed on purpose, by making components use actual DOM vs. shadow DOM. You may choose to do this if you have a well-managed, globally applicable CSS regime (ideally using BEM throughout), and are not worried about 3rd party style bleeding. Or you can use SASS to bring global CSS styles into the component, analog to using mainA.css inline with AMP as shown in tutorial 5.
In a browser, go to the dashboard page at http://localhost:9981/page/dashboard/. Inspect the dashboard page at /public/page/dashboard/index.pug
for an example of using multiple components in one page. Find the reused 'list-el'
, as well as 'circle-el'
and 'gauge-el'
in the HTML. Inspect 'script.'
'function UIinit'
. Note that the Circle and Gauge components are loaded from absolute URLs. This means that components can easily be shared across different web projects. In this example, ListBusiness
is also used as the component communication 'message bus' (The message bus features are found in BLX, the base class for ListBusiness
.) 'sb.addComp'
connects each component to the bus, as long as the component implements a function named 'init'
. (The non-dashboard examples didn't call 'sb.addComp'
because their components did not send or receive messages.) Because ListBusines
s was already present for loading list data, it was convenient to use it as message bus as well. You can use a separate message bus insteadby using 'const _blx = new BLX(null)
'.
On the dashboard page at http://localhost:9981/page/dashboard/, click on one of the list links to see how the circle and gauge display values change. Repeat for each list item. The list component has detected that it is enabled to communicate and has pushed hidden list data (values for circle and gauge) to the bus (rather than just opening a new tab). Because circle and gauge components are also registered with the bus, they receive the values and use their component-specific implementation to update the display. This way, components are loosely coupled.
Optional: Learn about the component communication implementation. First inspect the function 'addComp'
at /public/page/list/ListBusiness.js
; it passes a reference to the bus component. Look at the 'ListEl.init'
function in /public/_webComp/List.pug
. The component instance 'listEl'
keeps a reference to the bus. In the same file, look for the 'text/x-dot-template'
and see how 'listEl.nav'
is triggered when the user clicks on the link. Inspect the function definition 'ListEl.nav'
. It sends the data to the bus using key 'mySelection'; see 'blx.emit('mySelection'
. In the Circle component implementation at /public/_webComp/circle.pug
, find function 'CircleEl.init'
(the equivalent of 'ListEl.init'
for the Circle compoment). With '_blx.on('mySelection'...'
it specifies to update the component display when the bus received a message with key 'mySelection'. In summary, the components are 'loosely coupled' because neither component requires the presence of other components.
We like creating and using components when it make us more productive, and the code becomes more maintainable. We decide this per use case. Be your own judge!
If this tutorial felt a little heavy, we would be happy to help get you started with in-house seminars, workshops and 'training the trainer'. That applies to other labs as well. Just email us at hi [at] appthings.io or contact us through https://m.appthings.io.