Developing web components
It’s been around 6 months since we first started using web components at Disy. The decision to look this way, was determined by the increasing developing effort to keep up to date with the newest requirements and current best practices which often relate better to newer technologies than JavaServer Faces (JSF), the framework we were using at the time. JSF also allows you to create components for the web but its architecture is outdated, based on server side state, magic black boxes, overuse of http POST and in general an attempt to abstract away the web.
State of the art in frontend development
Today’s latest trends in frontend development presents us a myriad of libraries and frameworks, just to name some of the most popular:
ReactJs JavaScript library for building user interfaces
Angular Front-end web application framework for mobile and desktop
Vue.js Progressive javascript framework for building user interfaces
Most of these libraries, allow you to do create your own extensible, reusable and encapsulated HTML components (tags) with their corresponding style and functionality abstracted away, an important step forward from the earlier days of web development when everything was global divs and global Javascript with events and objects across the board, cumbersome and difficult to maintain. So which of these libraries should we chose? How long will they be supported? Will the next version be backwards compatible? Yes Angular we’re looking at you. And why isn’t there the possibility to create your own components, natively, in the browser without any extra libraries since it’s such a common and welcomed practice for developers to want to create their own suite of reusable components?
Welcome web components
Web components .v1 aka “2nd edition” is the effort of World Wide Web Consortium to unify and standardize best practices when it comes to creating native components for the web. Web components is made of 4 different standards:
Custom elements Allows for the definition of custom HTML elements whose functionality is defined in ES6 classes.
HTML Imports Allows HTML documents and fragments to be conveniently reused in other HTML documents.
HTML Templates Declared fragments of markup that are parsed as HTML, but implicitly inactive until instantiated later on at runtime.
Shadow DOM Provides encapsulation for JavaScript, CSS, and templating inside Web Components.
Why second edition? Because this is the second attempt to create such a standard accepted and implemented natively by all the main browser vendors, the first one, version 0, was abandoned due to lack of consensus.
We got the standards, but when will the implementation be ready? The image below shows the current status of these 4 technologies in all major browsers.
Great! Already implemented in Chrome, Opera and Safari. Firefox implementation in progress.
Web components in practice
The initial web components related task was to move from a jsf richfaces table component to a more reusable, higher customizable and more maintainable web component, to put things into perspective, it means passing from:
Tip: copy the lines to an editor if you want to see code details
<rich:extendedDataTable
value="#{tableResultBean.dataModel}"
enableContextMenu="false"
var="resultRow"
id="richResultTable"
clientRows="60" sortMode="single" selectionMode="none"
styleClass="tableResultsTable" rowKeyVar="rowIndex"
rowClasses="oddRow,evenRow"
noDataLabel="#{customMessages['resultContent.noData']}"
eventsQueue="table">
<c:forEach
var="column"
items="#{tableResultBean.dataModel.columnState}"
varStatus="columnIndex">
<rich:column id="column#{columnIndex.count}"
label="#{column.attributes.printName}"
index="#{columnIndex.count}"
rendered="#{column.attributes.visible && !(tableResultBean.hideColumns && column.attributes.type)}"
sortType="custom"
styleClass="tableColumnAlign#{column.alignment}"
selfSorted="true"
width="#{column.isTemplateColumn() ? '63px' : column.sizeInPx}"
sortOrder="#{column.richFacesSortOrder}">
<f:facet name="header">
<div class="tableHeader tableColumnAlign#{column.alignment} #{column.isTemplateColumn() ? 'templateColumnHeader' : ''}" title="#{column.attributes.printName}">
<a4j:commandLink styleClass="sortableColumnTitle"
execute="@this"
title="#{column.attributes.printName}"
value="#{column.attributes.printName}"
render="richResultTable"
rendered="#{!column.isTemplateColumn()}"
status="selectorLoadingIndicator"
action="#{tableResultBean.dataModel.toggleSort(columnIndex.count - 1)}">
<h:outputText styleClass="columnSortIcon icon-sort-indicator" rendered="#{column.ordering == 'UNSORTED'}"/>
<h:outputText styleClass="columnSortIcon icon-up" rendered="#{column.ordering == 'ASCENDING'}"/>
<h:outputText styleClass="columnSortIcon icon-down" rendered="#{column.ordering == 'DESCENDING'}"/>
</a4j:commandLink>
<h:outputText value="#{column.attributes.printName}" rendered="#{column.isTemplateColumn()}" />
<c:choose>
<c:when test="#{showFilter}">
<br />
<h:panelGroup styleClass="clearable">
<h:inputText value="#{column.filterValue}"
id="filterInput#{columnIndex.count}"
rendered="#{(column.type == 'STRING' || column.type == 'UNKNOWN') && (not column.templateColumn)}"
styleClass="tableFilterInput data_field" autocomplete="off"
onkeypress="if (event.keyCode == 13) {this.onchange(); return false;}">
<a4j:ajax event="change"
oncomplete="updateFilter(#{columnIndex.count - 1});" />
</h:inputText>
<h:outputText styleClass="icon_clear" value="x" />
</h:panelGroup>
</c:when>
<c:otherwise>
<h:outputText value=" " />
</c:otherwise>
</c:choose>
</div>
<script type="text/javascript">
if (#{column.ordering.toString()!='UNSORTED'}) {
$('.rf-edt-c-column#{columnIndex.count}').addClass('EDTselectedColumn');
}
</script>
</f:facet>
<ui:fragment rendered="#{resultRow.values[tableResultBean.dataModel.getModelColumnIndex(columnIndex.count - 1)].class.name == 'net.core.repository.objecttype.processing.TemplateProcessingBaseObject'}">
<c:choose>
<c:when test="#{column.templateTypeCategory eq webconstants.TEMPLATE_TYPE_CATEGORY_NAME_REPORT}">
<c:set var="defaultReportProcessUrl"
value="#{cw-u:encodeResourceURL(cw-u:concat3(cw-u:concat4('/pages/download/singleResultReportPopup.xhtml?columnIndex=', tableResultBean.dataModel.getModelColumnIndex(columnIndex.count - 1), '&rowIndex=', rowIndex), '&title=', column.attributes.printName))}" />
<h:outputLink value=""
onclick="Commons.windows.showModalWindow({title: '#{column.attributes.printName}', url: '#{defaultReportProcessUrl}', width: 400, height: 250}); return false;">
<c:choose>
<c:when test="#{column.hasCustomIcon}">
<span style="#{cw-u:concat3('background: url(', cw-u:encodeResourceURL(column.customIconUrl) , ') no-repeat center center;')}"
class="customIcon"/>
</c:when>
<c:otherwise>
<span class="templateColumn #{column.templateIconClass}" title="#{column.attributes.printName}"/>
</c:otherwise>
</c:choose>
</h:outputLink>
</c:when>
<c:otherwise>
<a4j:commandLink
onclick="Commons.singleResultProcessPopupUtilities.startSingleResultProcessing('#{column.attributes.printName}', #{rowIndex}, #{tableResultBean.dataModel.getModelColumnIndex(columnIndex.count - 1)});">
<c:choose>
<c:when test="#{column.hasCustomIcon}">
<span style="#{cw-u:concat3('background: url(', cw-u:encodeResourceURL(column.customIconUrl) , ') no-repeat center center;')}"
class="customIcon"/>
</c:when>
<c:otherwise>
<span class="templateColumn #{column.templateIconClass}" title="#{column.attributes.printName}"/>
</c:otherwise>
</c:choose>
</a4j:commandLink>
</c:otherwise>
</c:choose>
</ui:fragment>
<h:outputText value="#{resultRow.values[tableResultBean.dataModel.getModelColumnIndex(columnIndex.count - 1)]}"
escape="false"
rendered="#{resultRow.values[tableResultBean.dataModel.getModelColumnIndex(columnIndex.count - 1)].class.name != 'net.core.repository.objecttype.processing.TemplateProcessingBaseObject'}">
<f:attribute name="columnIndex" value="#{tableResultBean.dataModel.getModelColumnIndex(columnIndex.count - 1)}"/>
<f:converter binding="#{tableResultBean.rowFormatter}" />
</h:outputText>
</rich:column>
</c:forEach>
</rich:extendedDataTable>
to something like:
<d-table d-persistence-id="#{tableResultBean.dataModel.persistenceId}">
<c:forEach items="#{tableResultBean.dataModel.columnState}" var="column">
<c:if test="#{column.visibleAndNotHidden}">
<c:choose>
<c:when test="#{column.isTemplateColumn() and column.templateTypeCategory eq webconstants.TEMPLATE_TYPE_CATEGORY_NAME_REPORT}">
<d-table-column processing="/pages/download/singleResultReportPopup.xhtml?columnIndex=&rowIndex=&title="
processing-icon="#{column.hasCustomIcon ? column.customIconUrl : column.templateIconClass}">
#{column.attributes.printName}
</d-table-column>
</c:when>
<c:when test="#{column.isTemplateColumn()}">
<d-table-column processing="true"
processing-icon="#{column.hasCustomIcon ? column.customIconUrl : column.templateIconClass}">
#{column.attributes.printName}
</d-table-column>
</c:when>
<c:otherwise>
<d-table-column width="#{column.sizeInPx}" alignment="#{column.alignment}" ordering="#{column.ordering}">#{column.attributes.printName}</d-table-column>
</c:otherwise>
</c:choose>
</c:if>
</c:forEach>
</d-table>
which could be simplified even more if we were to need a table with a well determined structure (no dynamic template substitution):
<d-table d-persistence-id="myPersistenceId">
<d-table-source>/api/petsTable</d-table-source>
<d-table-column width="10%" alignment="center" processing="true" processing-icon="http://..url..">Chart</d-table-column>
<d-table-column width="20%" alignment="left" ordering="true">Column1</d-table-column>
<d-table-column width="100px" alignment="center">Column2</d-table-column>
<d-table-column width="20%" alignment="left">Column3</d-table-column>
<d-table-column alignment="center">Column4</d-table-column>
</d-table>
All the functionality corresponding to the above web component html would be separated in another .js file which made use of plugins provided by the jquery datatables plugin. The resulting table should provide the same functionality as the jsf richfaces table component. Mainly it had to support:
Sorting
Filtering
Lazy loading
Auto-size columns
Vertical scrolling
Horizontal scrolling
Single/Multi row selection
Drag&Drop column reordering
Resizable Columns
Session persistent settings
Custom cell renderers
This would allow us to place such table definitions anywhere in our HTML and boom, just like that awesome tables would show up!
In reality…
For those who are familiar with the datatables plugin, you may recall its capacity of being enhanced by providing extensions. So let’s make our columns resizable with the colResize plugin, after all it’s loading a file and initializing an option!
Doesn’t work…but why? Well nothing seems wrong with our setup, so what is going on, maybe the extension is not performing as expected? Let’s get our hands dirty an check the ‘codes’ inside the library:
if (typeof define === 'function' && define.amd) { // Define as an AMD module if possible
define(['jquery', 'datatables'], factory);
} else if (typeof exports === 'object') { // Node/CommonJS
factory(require('jquery'), require('datatables'));
} else if (jQuery && !jQuery.fn.dataTable.ColResize) { // Otherwise simply initialise normally, stopping multiple evaluation
factory(jQuery, jQuery.fn.dataTable);
}
Last lines of code in the library, module registration. First, we don’t use AMD modules anywhere…well internally webpack our awesome module bundler, does expose a define function even if its functionality is different than the standard one, this implementation fails…..silently. Well, we can’t get rid of webpack, so how do we fix this? We just want to call that last ‘if’ to register the module ‘normally’ , ok we will clone the official colResize repository, host it on our Disy github account then modify the library to remove those annoying ifs, push our changes, and instruct our dependency management tool to look for the plugin on our github account rather than on the official npm repository. Easy.
Lesson learned, certain 'old' plugins need registering modifications when used in new complex environments, such as webpack.
It works, in Chrome at least! Let’s try Firefox? Oh we cannot, so far only Chrome implemented web components fully, no problem, we will use the shiny, brand new, still beta web components polyfills to make our Firefox behave as if it already implemented all web components substandards, after all it’s a polyfill purpose! Ok, but which polyfill version do we use? There is 5 of them:
webcomponents-hi.js - Html import
webcomponents-hi-ce.js - Html import & Custom Elements
webcomponents-hi-sd-ce.js - Html import & Shadow DOM & Custom Elements
webcomponents-sd-ce.js - Html Shadow DOM & Custom Elements
webcomponents-lite.js - You guessed, as the ‘lite’ adjective might imply, this one contains everything, plus some more? More? Yes we will get there!
Let’s pick ‘webcomponents-sd-ce.js’ since we don’t really use HTML imports. Aaaand? Ok it doesn’t work because I forgot to include the ‘custom-elements-es5-adapter’ needed if your browser natively supports ES6. Explained in the docs, my bad.
Yes! Firefox is a go!
What about IE11? Well here we need another polyfill than in Firefox since IE11 supports even less things than Firefox. No problem ‘webcomponents-lite.js’ loaded. Hmm still does not seem to work, ahh ‘custom-elements-es5-adapter’ should not be included in this case, ok we will include it conditionally.
<script type="text/javascript">
if (window.customElements) {
document.write("\x3Cscript type='text/javascript' src='#{cw-u:encodeResourceURL("/scripts/webcomponents/custom-elements-es5-adapter.js")}'>\x3C/script>");
}
</script>
Not very elegant, but it does the job. Wow, even in IE11! Nice!
Let’s add some more features to our table component. Row and column highlighting, this should be easy. Indeed, some lines of CSS and a few of Js give us the desired result! Firefox checking…almost, not really, what is going on? Style is not applied correctly, there are css styles leaking in from the outside of our component. How is this possible, it’s supposed to be an isolated (shadowed) component! Well it turns out that the part of the polyfill which is suppose to implement the Shadow DOM specification, to hide the details of our component from the rest of the document and prevent css from leaking in from outside, doesn’t work. The name of this implementation is Shady DOM, interesting name, well they did call it shady (by definition: unreliable, uncertain, dodgy) and the words ‘as is’ and ‘no warranty’ do appear in the license, so I guess it’s fair, they did warn us.
Lesson learned, working with brand new technology can be time consuming due to its lack of maturity
Two days later we discover a bug in a different part of our web application, another JSF component stopped working, could it be related to our new “interactions”, and if yes, how is it possible? Our component is supposed to be isolated. The helpful error message is as follows:
DOMException [SyntaxError: "'#j_idt250:tableCondition' is not a valid selector"
code: 12
nsresult: 0x8053000c
location: http://localhost:8000/trunk/scripts/webcomponents/webcomponents-hi-sd-ce.js:123] webcomponents-hi-sd-ce.js:125:321
Yes, it is the polyfill we loaded which is throwing the error, but the old component is not suppose to use it, indeed but remember the polyfills modify the way the browser works by changing basic functionality, how exactly do we trace the error?
Let’s check the code:
'use strict';var J="undefined"!=typeof window&&window===this?this:"undefined"!=typeof global&&null!=global?global:this,Aa="function"==typeof Object.defineProperties?Object.defineProperty:function(g,q,N){g!=Array.prototype&&g!=Object.prototype&&(g[q]=N.value)};function sb(){sb=function(){};J.Symbol||(J.Symbol=tb)}var tb=function(){var g=0;return function(q){return"jscomp_symbol_"+(q||"")+g++}}();
function ed(){sb();var g=J.Symbol.iterator;g||(g=J.Symbol.iterator=J.Symbol("iterator"));
ahh many 175 characters long lines of minified code! Actually there were some source maps in the directory where yarn downloaded the files, let’s move them over to the place we serve our content from, the browser should pick them up and unminify our code!
Done! And?
import '../node_modules/@webcomponents/webcomponents-platform/webcomponents-platform.js';
import '../node_modules/@webcomponents/template/template.js';
import '../src/promise.js';
import '../node_modules/@webcomponents/html-imports/src/html-imports.js';
import '../src/pre-polyfill.js';
import '../node_modules/@webcomponents/shadydom/src/shadydom.js';
import '../node_modules/@webcomponents/custom-elements/src/custom-elements.js';
import '../node_modules/@webcomponents/shadycss/entrypoints/scoping-shim.js';
import '../src/post-polyfill.js';
import '../src/unresolved.js';
10 lines, that is it? Many 175 loooong lines of unminified code transformed to 10 very short lines?
Even worse, IE would not load a broken down javascript file like this, it just doesn’t know what those import statements are. Ahh this is frustrating, do I need to manually go through all those imports, which call other imports, which call….and check which one might have the method which is causing the error? I think so since the web components polyfill does not provide any unminified version…2 hours, ok found it! Hmm they are patching:
document.getElementById
even assembly people have heard of this method, why is it patching such an important method (and many others as well)? I guess for the sake of encapsulation and the way the Shadow DOM interacts with the rest of the DOM. Is this what is causing the error? Yes it is, can we fix it? It might be tricky, quite complex stuff going on here.
Me: Hey Matthew, check this out, this is the line: patchBuilt... that's causing our error. It's in the import from the import within the other import.
Mat: What line, there is nothing on that line
Me: Yes there is, what do you mean?
Mat: I'm checking the source here on github and I cannot see that line
Me: Well it is in our version of code.....could it be that they updated this in the last 2 days since I downloaded the library?
Yes, they did, indeed a new version which fixes our problem actually! Wow, I feel silly, lesson learned I guess.
Lesson learned, one of the first things a developer should check is updates to the library he/she is using.
Late at home while surfing the web components project setup, I discovered that indeed there is an internal task for developers, called ‘debugify’ which allows you to create full unminified versions of web components, this should work in IE. Not documented, but apreciated.
Lesson learned, nobody writes minified code. If the code is minified, there must be a way to unminify it.
Let’s add more features, it seems datatables supports everything we need, so this should be straightforward. Yes, datatables is quite a helpful plugin, but I believe it was not tested with as many features together as we need because every time we add a new feature an old one breaks. To the praised tree do not go with a sack (to pick up fruits), as they said when I was a child. After some workarounds and hacks, we do manage to accommodate all of our desired options in.
Meanwhile a bug came in, on Firefox charts have stopped working, no error in the console, but the charts don’t show up. Not good.
Time to get our hands dirty and check the code. Minified code again, well performance is important so can’t blame anyone. Checking the website for the unminified version, nowhere to be found. It turns out it’s provided to paying customers only. That’s us, cool, debugging…
The chart library code looks more or less like:
// Add a function to onReadyArray which adds the chart to the div. To be executed when the document is ready.
AmCharts.makeChart = function(div, config, amDelay) {
...
AmCharts.onReadyArray.push(function() {
// Add the chart to the specified div
});
...
}
// When the document is ready
window.removeEventListener("load", AmCharts.handleLoad, true);
// execute the functions from the onReadyArray
AmCharts.handleLoad = function() {
AmCharts.isReady = true;
var onReadyArray = AmCharts.onReadyArray;
for (var i = 0; i < onReadyArray.length; i++) {
var fnc = onReadyArray[i];
if (isNaN(AmCharts.processDelay)) {
fnc();
} else {
setTimeout(fnc, AmCharts.processDelay * i);
}
}
}
So it appends the chart to the provided html element on the window load event, reasonable. Still the code which actually appends the chart to the html document is never executed, why is that since our document eventually loads correctly, i.e. the load event is fired.
Do you remember the previous “plus some more”? It turns out the same “Shady” polyfill changes the way events work in the browser in order to maintain encapsulation within shadowed components. The Shadow DOM standards states that only certain events should pass through the “barrier” between our component and the rest of the HTML document. But one could argue that window events should be allowed to pierce through since so many libraries use this basic load event to make initializations of plugins. Could it be another bug in the polyfill? It is. Since we know what the problem is and we see that a fix is trivial for the polyfill developers, we might as well create a merge request because we’re nice folks and we want to support open source projects.
But how do we fix our problem? We could clone the whole project again to our github account an modify it. Or we could remove the Shadow DOM suport from our code and get rid of this “shady” implementation from the polyfill itself, this does mean that our components will no longer be encapsulated. Are we prepared to take such a step, is it even possible to remove parts of the polyfill and not completely break it?
It turns out there is one other option. Our application is not designed as a single page application, it has different pages which load different libraries, so we could avoid loading the problematic polyfills wherever we need to show a chart…as long as we don’t need to show a table using our component which does require the polyfill of course. Bingo, that’s the case! Problem solved.
Yet another bug. In IE11 things are bogging down. Sometimes, only sometimes, the GUI blocks for approximately 40 seconds when loading a webtable with a filter on it, after 3 hours I managed to localize the problem, as unbelievable as this might be, the culprit is: The same Shady Dom polyfill and its overriding of the browser’s main functions (getElementById and querySelector) and this time there are no updates…I guess enough is enough, let’s remove the Shady Dom from the polyfill, can we? Let’s fork the project on our github account so we can mess with it’s source code. It turns out that the design of these polyfills is quite composable so to remove a specific polyfill is enough to comment out the line which imports it:
//import '../node_modules/@webcomponents/shadydom/src/shadydom.js';
The end.
We’re almost there. The previous solution only works in the polyfill we load in IE, remember in the beginning we said we need to load a polyfill for Firefox and another one for Internet Explorer because IE has fewer things implemented than Firefox. Nevertheless if we exclude the Shady Dom polyfill from the Firefox variant this stops working, I’m guessing it’s because it still needs a part of the dependencies. It would have been great to just remove it from both, but in Firefox we did not encounter the page slow down we encoutnered in IE, so I guess we can leave it in Firefox.
Moving on to another project which involves creating a basic player for linked geographical points. It’s a smaller feature than the web table, but it’s more connected to maps, very interesting for geography enthusiasts such as myself. The only downside to it is that it makes our JSF filters fail when used on the same page as the this new web component.
[JSF AJAX Error] An empty response was received from the server. Check server error logs. Logger.js:81:13
Object { type: "error", status: "emptyResponse", source: <span#j_idt630:j_idt643>, responseCode: 200, responseXML: null, responseText: "<?xml version="1.0" encoding="UTF-8…", description: "An empty response was received from…" } Logger.js:81:13
XML Parsing Error: junk after document element
Location: http://localhost:8075/cad75/pages/map/default/index.xhtml
Line Number 1, Column 163:
That odd looking id seems familiar. Yes, it’s the Shady Dom we were unable to remove from the Firefox polyfill. It prevents some JSF AJAX call which is necessary to do the filtering.
When we were dealing with this, I remember I was tired.
It’s clear we need to get rid of this polyfill. Robert has an idea, instead of just removing it from the Firefox polyfill, which proved not to work before, we could try to only load the CustomElements polyfill, instead of the whole polyfill (CustomElements + ShadyDom + moreStuff) this would produce much smaller bundles and we would get rid of this inmature ShadyDom polyfill. Robert manages to do it, not sure if it was straight forward or there was some blood and sweat involved. I learned something from all of this.
New technology has bugs, newer technology has more bugs, even if it's great technology
We need to modify our components to remove the Shadow DOM option from them. It’s not a huge problem I guess, we were mainly using the Shadow DOM to prevent outside style from leeking into our web components, but as we saw, the Shady polyfill wasn’t doing a perfect job and styles were leaking through anyway. It’s nice to be able to hide implementation details from the user, but if this comes at a cost of big time spent fixing bugs popping all over the code and makes your work frustrating, we can live without it.
Conclusion
It’s been an interesting trip, a bit unusual but I learned a lot, I had very good colleagues by my side and together I believe we improved at least a small part of our frontend application. Despite what you might think I like working with web components. In my previous company I did a presentation for my colleagues about it, I really thought they were the future. I really believe in web components, I think they are a superior technology to our current JSF components. Web components provide better encapsulation than their JSF couterparts. They are smaller, in general, easier to maintain. They provide better reusability. They can be easily combined to create more complex web components. They are or should/will be available natively in the browser! They use newer technology, even if this is not an advantage all by itself, I do believe in general newer and proven technology is better than older technology.
The key word in the previous sentence is ‘proven’. This technology as we saw, it hasn’t been implemented by Firefox or Edge yet, it’s only in Chrome, Opera, Safari. Indeed people from Mozilla seem to advance on the development but they also said they will not support some feature such as HTML imports, will their implementation be production ready anytime soon? I hope so. I also hope that the polyfills will get more mature because in the state we were using them when we started our journey they are not reliable, they are well.. shady. Most problems we dealt with were related to these polyfills or different libraries we used not so much because of the web components standard itself, that is the reason why in Chrome we had next to no bug. Web components v1.0 cannot fail as it’s predecesor Web components v.0. The idea of having native components in the browser, similar to those provided by Angular or React, is beautiful, normal even. Through determination and hard work we managed to make a good experience out of it. It’s a hell of a journey converting a user interface from an outdated technology to something brand new.
The title image WebComponents Logo was published by WebComponents.org.