Using the Custom Elements Design Pattern
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.
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.
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.
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.
INSTALLING YOUR ENVIRONMENT
The codebase requires a few tools to be installed in your development environment.
WINDOWS ONLY: Install Ruby and then run
gem install sass
npm install -g gulp babel npm-check-updates astrum slimerjs casperjs.
Clone the project repo.
ncu -aand follow instructions (if any)
THE CODE STRUCTURE
Opening the project code, you’ll see several files and a two folders. Let’s look at each one:
.eslintrc These are the rules used by ESLint
.gitignore Prevents local npm installation from being pushed to the git repo
.scss-lint.yml Scss Lint’s rules file. SCSS Lint is used by your code editor to lint your sass code.
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/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
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.
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.
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. (
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.
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.
The codebase has a simple unit testing framework called Tape installed. More information about Tape can be found here.
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/perfect-scrollbar.min.js Creates a nice apple-like scrollbar for content: https://noraesae.github.io/perfect-scrollbar/
develop/js/vendor/watch.js Allows you to watch an object (usually the single global object) for changes. https://github.com/melanke/Watch.JS/
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.
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:
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.
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.
assetSources: any static assets (fonts, images, svgs) are listed here.
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.
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.
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.