Using the Custom Elements Design Pattern

PHILOSOPHY

There are more and more tools out there now to build client apps. AngularJS, Ember, Backbone, React, Knockout, etc.. all have their pros and cons.
People in charge of development teams seem to agonize over the decision.

The approaches in this guide were picked with the following criteria:

Code written for Junior Developers

Code is read far more than it’s written. Keeping code easy to read makes it easy to maintain. A junior developer should be able to start pushing code on the first day.

Code broken into small modules that have no dependencies with other modules

Keeping code in decoupled modules allows developers to add or remove code at will without worrying about breaking the codebase. It also allows a developer to maintain a module as its own separate app without necessarily needing to understand how that module relates to the app in general.

The DOM is not the source of truth

Too often code is trying to determine state by parsing the DOM for information. The DOM is unreliable: it can be refactored, elements can be hidden by code, or DOM elements you assume are present didn’t get rendered.

The DOM is simply a view of the state and reacts to changes in the model layer and router.

We use a single global object-model which defines our current state and the DOM simply reacts to that model.

Decoupled architecture.

The front end code is completely decoupled from the backend and only accesses data through a web api. This allows the backend or front end to be swapped out at any time. It also makes available the possibility of creating native apps.

No frameworks.

Frameworks define how the entire app is created. If a need arises to switch frameworks, this usually results in a re-architect of the code.

Libraries are okay.

Libraries like jQuery or moment.js are okay to use because they solve a specific problem. They can easily be removed from the project or swapped with a similar library without causing much downtime.

Javascript should be obvious and well-commented

Creating a massive structure of prototyped objects creates obfuscation and confusion. All javascript code should simply be procedural which is the pattern most junior developers understand. Object-oriented or functional javascript should only be used in rare cases where procedural javascript is not practical.

BUSINESS CASE

Besides making the codebase easier to write, read and maintain, this code design pattern has several business cases:

Rapid Development and Quick Debugging

Because the code is insular and modular using custom elements, developing is much faster because each custom element is a mini-app. There is no need to track dependencies (since there are none between custom elements) so there is very little concern about the x- one break- one problem that occurs in more complex code structures.

Easy to make responsive

Since the front end is decoupled, it’s easier to make responsive.

The code can be wrapped into a web-view native app

Running the code through Cordova to generate native iOs and Android apps should be relatively painless.

Platform designed for new features

The code is designed like legos, so adding new features is quick and easy. Just build a custom element and plug it in.

Getting Started

INSTALLING YOUR ENVIRONMENT

The codebase requires a few tools to be installed in your development environment.

gulpfile.js
This is the Gulp code responsible for compiling and building the codebase.

jscs.json
The rules for JSCS builds

package.json
npm uses this file to keep your development environment up to date.

README.md
A markdown version of this guide

skeleton.js
This is a blank custom element you can use to start a new custom element for the codebase.

this.sublime-project
A SublimeText project file that hides the node_modules folder from the list of files.

node_modules folder
This folder is used by Npm and you shouldn’t need to ever deal with it. It’s probably a good idea to hide it in from your code editor.

develop folder
This folder contains all the code for this project.

develop/assets
This folder contains static assets like fonts and images

develop/js
This contains the global JavaScript files that control the entire app

develop/js/vendors
This folder contains the third-party JavaScript libraries used in the app

develop/sass
This folder contains the global sass files for the entire app

develop/sass/baseline
This folder contains the resets and baseline styles for the entire
app

develop/sass/themes
This folder contains the styles for any instance-specific themes

develop/test
This folder contains the Tape unit testing code

How it Works

CUSTOM ELEMENTS

The codebase is built around custom elements. Custom Elements allow web developers to define new types of HTML elements. The spec is one of several new API primitives landing under the Web Components umbrella, but it's quite possibly the most important. Web Components don't exist without the features unlocked by custom elements:

Define new HTML/DOM elements

Create elements that extend from other elements

Logically bundle together custom functionality into a single tag

Extend the API of existing DOM elements

Custom elements are native to Chrome and Android browsers and a small polyfill in the js/vendors folder expands support to the other browsers including iOs.

Custom elements are small, self-contained units of code. HTML already has several default elements that come with the markup out of the box: textareas, dropdowns, inputs, etc. Each of these come with their own separate code that allows textareas to be resized, and dropdowns to drop down.

What we’re doing with the codebase's custom elements is defining new elements that the browser can use. Just like a textarea, these custom elements have a separate codebase that doesn’t have dependencies on other custom elements. And just like native elements, you can add or remove the custom elements without necessarily breaking the app.

If you open the skeleton.js file in the root, you’ll see that the entire custom element is wrapped in a javascript closure. At the top, there is a default ES6 template. The CustomElement Class defines the custom element, and add three hooks: createdCallback(), attachedCallback() and attributeChangedCallback().

createdCallback()
This callback is fired when this custom element is created. This is usually done when the app loads and the custom element is defined for the browser. In most cases you won’t need to mess with this callback unless you are lazy-loading this custom element after the app is initialized.

attachedCallback()
This callback is fired when you place the custom element into the DOM. This usually contains three functions: dataPlug(), buildOut() and events(). You should try to keep these functions in the callback, but occasionally you’ll need to rearrange the order or move one of the function inside another. In that case, you’ll want to make a comment here explaining where the function you moved is found in custom element. You should not remove any of these functions as developers maintaining the code will expect these to be present. If you’re not using them, you can leave the functions blank or comment them out.

attributeChangedCallback()
If you dynamically add an attribute to the custom element like a data-id or style, this callback will fire. This is only for the custom element itself, not it’s children.

The last line of code in this section attaches the custom element to the browser’s HTML api.

dataPlug()
This function is for data bindings. This is usually where you want to put the initial AJAX calls to fill out any templates used by the custom element.

buildOut()
This is occurs after the initial data bindings and can be used for any animated buildouts on load.

events()
This is where event-bindings would occur.

The last section (which is blank) is for standalone functions used for this custom element.

SCSS
Every custom element should have a SCSS file as well. It should be named the same as its accompanying JavaScript file. The sass file will be namespaced for that custom element.

EQCSS
EQCSS are for element queries and are only included alongside the SCSS files if the custom element is using EQs. More information about element queries can be found here.

HOW TO CREATE A CUSTOM ELEMENT

To create a new custom element:

Copy the skeleton.js and place it in the develop/custom_elements folder, ideally in a separate folder based on URL “pages”. For example, if you’re creating a profile page, you should create a folder called “profile” and put the blank skeleton.js file inside. Rename it to reference your new custom element (like “profile.js”).

Create a blank scss file using the same name as your custom element’s js file. At the top of the scss file put: @import “../../sass/Baseline/sass”; Be sure to check the path to ensure it properly targets the baseline sass file. Next put the name of your custom element with empty brackets to setup the custom element’s namespacing. (new-web-custom-element {})

In the js file, name your custom element where it says "document.registerElement”.

If you need a theme for the custom element, put it in the theme scss found in "develop/sass/themes".

Register your scss file in "develop/sass/style.scss".

Stop and restart gulp to ensure you’ve compiled your new custom element into the codebase.

THE PARENT HTML FILE

This codebase is contained inside a single HTML file found in develop/html. Any custom elements that should be available in every view should be placed here (like a spinner). Other custom elements are rendered in the <main> tags by the client-side router.

SASS

The sass for the entire app is contained in the develop/sass folder. CSS for the custom elements is stored alongside the custom element’s JavaScript file in the develop/custom_elements folder. Let’s take a look at what’s in the main Sass folder:

develop/sass/baseline/base.scss
This is the CSS reset. There isn’t any real reason to mess with the code inside. It simply ensure that any layout biases between browsers is nulled so every browser plays from the same base styles.

develop/sass/baseline/embeded.scss, develop/sass/baseline/perfect-scrollbar.scss
These are the styles for the embed.js or perfect-scrollbar plugins. Feel free to change them as needed, but remember that if you are changing the look or feel, you might want to offload those styles to a theme file instead.

develop/sass/baseline/project.scss
These are the baseline styles specific to the codebase and are the style defaults. This should allow pages to render without a theme and still be somewhat stylish. Put global default styles in here.

develop/sass/baseline/sass.scss
This is where you’d put global sass variables and mix-ins.

develop/sass/style.scss
This is the manifest SCSS and its sole purpose is to import all the other sass files.

UNIT TESTING

The codebase has a simple unit testing framework called Tape installed. More information about Tape can be found here.

GLOBAL JAVASCRIPT

In the develop/javascript file, there are some js files that are used globally:

develop/js/vendor/autoexpand-textarea.js
This library makes textareas automatically grow in height as text is entered. This is custom library for this codebase so there is no open source project to update this file with.

develop/js/vendor/document-register-element.js
This is the custom element polyfill. Most of this code comes from here however the typeof HTMLElement !== ‘function’ conditional (which polyfills iOs Safari) is custom.

develop/js/vendor/jquery.min.js
Umm - jQuery

develop/js/vendor/moment.min.js
moment.js is a library that helps with javascript time and date issues: http://momentjs.com/

develop/js/app.js
This is the main JavaScript file and is the javascript code the starts to build the app. It:
- creates the single global object
- pulls in the initial data via AJAX

develop/js/router.js
This client-side router watches the URL for changes and fires if it changes. It controls which custom elements are loaded and when.

GLOBAL OBJECT MODEL

The first thing the codebase does on load is create a global Global object. This is object is for data storage only and works like a mini database in memory. The Global object should not contain any functions or constructors. It should not be extended through prototypal inheritance. It is a global data store and that’s it.

Some elements can watch the Global object or one of it’s branches using watch.js so as the Global object can be updated in realtime through long polling or web sockets, it can fire events to whatever is watching it.

There are several branches to the Global object:

Global.client
Stores a list of attributes specific to the current session like information about the instance or where the user is in the app

Global.member
Stores a list of attributes about the current user

Global.timestamps
A list of timestamps pertaining to when certain data is loaded. This can be used to check whether or not it’s time to re-check that data again from the backend.

The Global object should be stored in local storage at regular intervals. This (coupled with Global.timestamps) will reduce server load and speed up the app. When the app loads in the browser the Global object in local storage can be the initial data so the page loads immediately and then it can retrieve any updates it needs.

The Global object is the source of truth for the app. The DOM is unreliable since, at any time, it can change, or be re-factored, or become obscured. Global is where you would store data you intend to reference later.

Workflow

GULP AND NPM

The build tool we use in Gulp with npm. Once it’s installed you run it by typing gulp into your command line positioned in the root directory. Each day you’ll want to make sure your tools are up to date so in the command line run:

ncu -a

This uses the npm-check-updates package to see if there are any updates available for your node packages. If there are, follow the instructions to update them. For the most part, this is all that’s really required to maintain the tools.

Gulp

The gulpfile.js has several parts:

jsSources: put any js files in this array that you want to load after the vendor files. The files are loaded in order.

vendorSource: put any third party js files in this array. The files are loaded in order.

sassSources: This should only be a single style.scss file, but you could append a theme sass file afterwards if you want to separate that from the main sass files.

htmlSources: pretty much the main HTML file, but others can be added, if necessary.

eqcssSources: grabs all the .eqcss files in the custom_elements folder.

Running gulp in the command line will compile these sources, run validation and setup a livereload environment for you on localhost:8080. Gulp will watch for changes to the js and sass files and recompile everything on the fly. Vendor.js, raw html and static assets aren’t watched so when you add these to the project you must start gulp and then restart it so it will compile these additions.

DESIGNING WITH CUSTOM ELEMENTS

When you first start working with custom elements, there’s a tendency to want to write dependencies between them. Don’t. You want the custom elements to function as separate mini-codebases. If you absolutely need to have a dependency, make it a loose coupling where the code first checks if the dependency is present before executing its code. The code should also run well without the dependency present.

In the example code, there is a loose dependency between the discussion-input custom element and the discussion-area custom element. The discussion-input, upon received text, will try to attach the text to the discussion-area if it’s present. If it’s not, the discussion-input still functions normally (saving the data and resetting the tool) without breaking the code. This allows the discussion-input custom element to be used anywhere in the codebase we need a discussion-like input.

If there needs to be a hard dependency, try using the Global object as a middleman between the dependencies. In the example code, Global.discussions.currentdiscussionID stores which discussion the user is currently in. Since some code is dependent on knowing the current discussion id, having it stored in the Global object prevents needing to check the DOM or other custom elements for that information.

JAVASCRIPT AND ES6

This codebase is written using ES6 syntax that is transpiled down to ES5 via the Babel plugin. While it’s not mandatory that the js should strictly adhere to ES6, it’s a good idea to use the latest version of the language to provide some measure of future-proofing.

Babel runs automatically when you run gulp.

Something else to note about how js works in this codebase: when using selectors, we don’t attach events to CSS classes or IDs. Instead we use a custom attribute called “js-hook”. This custom attribute works like a CSS class, but decouples javascript from the styling of elements. This allows easy front-end code refactoring where class names can be changes and elements re-ordered without breaking the js. It also tells anyone reading the HTML where the js is hooked in so they can be aware of this fact when refactoring.

Animating elements in the DOM should be done by adding and removing CSS classes. Animation is part of the presentation layer and shouldn’t be controlled by js.

SASS AND CSS

The sass in this codebase works normally as any other project. We use sass to namespace the CSS under the custom element, with sub-styles nested under the custom element. This prevents pollution.

When creating a modifier class, prepend the class name with “is-” (.is-hidden, .is-collapsed). These modifier classes will be used by the js code to alter the element’s state. Using is- also helps with CSS refactoring because it’s obvious which styles are default and which are modifiers.

Themes are always loaded last in the DOM and are used to override the default styles. In general, you want to put the default styles in the custom elements that aren’t used for aesthetics. Styles like color, background-color, font-family, font-size, etc are best placed in the theme file to reduce the need for using !important later.