Skip to content

Instantly share code, notes, and snippets.

@addyosmani
Last active December 22, 2015 01:19
Show Gist options
  • Save addyosmani/6395466 to your computer and use it in GitHub Desktop.
Save addyosmani/6395466 to your computer and use it in GitHub Desktop.
Just a thing

Large-scale JS application architecture with web components

I once wrote about implementing your own decoupled architecture for building large-scale apps, inspired by some of the work Nicolas Zakas did at Yahoo and I at AOL.

It included suggestions such as breaking down your app into small, highly-reusable components which were easier to maintain. These components would communicate (or react) to each each other by means of an event messaging bus or mediator. A permission model would ensure that components could only speak to parts of the system they were allowed to and facade patterns (abstraction APIs) let you switch out one utility library for another as needed.

A few years have passed since then and luckily standards-based web platform primitives started to appear to help with a similar set of problems. To say that this has been exciting to see evolve would be an under-statement.

Enter stage left, Web Components

We'll soon have custom elements that give us a world where everything is an element (e.g <my-widget>) and expands HTML's existing vocabulary - a world that cares about encapsulation where all of your resources are elements, even those that aren't necessarily visual.

Similar to the patterns we advocated for a few years ago, this vision considers applications as being composed of elements. They can be easily reused anywhere and ideally it won't matter what frameworks you've used to create these elements. They'll just work.

Registering a custom element <x-foo>:

var XFoo = document.registerElement('x-foo');
document.body.appendChild(new XFoo());

Registering a custom element with markup to be rendered:

var XFooProto = Object.create(HTMLElement.prototype);

XFooProto.createdCallback = function() {
  this.innerHTML = "<b>I'm an x-foo-with-markup!</b>";
};

var XFoo = document.registerElement('x-foo-with-markup', {prototype: XFooProto});

/*
Usage:
▾<x-foo-with-markup>
   <b>I'm an x-foo-with-markup!</b>
 </x-foo-with-markup>
*/

Extending elements can also easily be done, by declaring the type of element we wish to extend when registering out element with document.registerElement():

var XFooButtonPrototype = Object.create(HTMLButtonElement.prototype);
XFooButtonPrototype.createdCallback = function() {
  this.textContent = "I'm an x-foo button!";
};

var XFooButton = document.registerElement('x-foo-button', {
  prototype: XFooButtonPrototype,
  extends: 'button'
});

Where in past years we heavily relied on JavaScript templating, we'll soon have declarative HTML templates instead. They'll contain inert chunks of markup which you may want to use later, work with the DOM and be parsed, but won't be rendered. Where we previously relied on hacks to hide our templates in script blocks, these HTML templates will be hidden from the document and you won't be able to traverse into its DOM.

<!-- declare our inert template -->
<template id="mytemplate">
  <img src="" alt="avatar">
  <div class="comment"></div>
</template>

<!-- activate the template -->
var t = document.querySelector('#mytemplate');
// Populate the src at runtime.
t.content.querySelector('img').src = 'avatar.png';

var clone = document.importNode(t.content, true);
document.body.appendChild(clone);

In the original architecture I described, we had to hack together any true sense of encapsulation. The best thing we used to have available to us for this was iframes. The Shadow DOM changes this by giving us a way to properly encapsulate markup ("hidden DOM elements"), give our elements their own style boundaries to avoid styles leaking and offering some of the same capabilities browser vendors like Chrome have used to implement their own custom form elements using just HTML, CSS and JS. Styles in your Shadow DOM get scoped by default and unless you explicitly let them, they won't leak across elements (but can if you want).

Encapsulation of content (e.g text) with Shadow DOM is also trivial:

var XFooProto = Object.create(HTMLElement.prototype);

XFooProto.createdCallback = function() {
  // 1. Attach a shadow root on the element.
  var shadow = this.createShadowRoot();

  // 2. Fill it with markup goodness.
  shadow.innerHTML = "<b>I'm in the element's Shadow DOM!</b>";
};

var XFoo = document.registerElement('x-foo-shadowdom', {prototype: XFooProto});

Where we previously relied on script injection or AMD to dynamically pull in packages, HTML imports will give us a cleaner way to package, distribute and share our custom elements. Imports will allow us to easily consume or extend other custom elements too.

Good patterns will still have their place. Event signalling can still be implemented in the form of custom elements, allowing any element on the page to broadcast or subscribe to events from any other.

Polymer offers a set of polyfills for these web platform features as well as syntactic sugar built upon these technologies. The polyfills cover Shadow DOM, Custom Elements and HTML imports amongst other things. A basic Polymer (custom) element could be written as follows:

<polymer-element name="basic-elem">
  <template>
    <span>Coucou</span>
  </template>
  <script>
  	Polymer('basic-elem');
  </script>
</polymer-element>

and imported via HTML imports as follows:

<html>
	<head>
		<script src="../lib/polymer.min.js"></script>
		<link rel="import" href="elements/basicElem.html">
	</head>
	<body>
		<basic-elem></basic-elem>
	</body>
<html>

we can throw some template tag into there, using data-binding to keep the value of {{name}} in sync:

<polymer-element name="basic-elem">
  <template>
  	<input type="text" placeholder="What's your name ?" value="{{name}}"></input>
  	<br/>
    <span>Your name is {{name}}</span>
  </template>
  <script>
  	Polymer('basic-elem');
  </script>
</polymer-element>

Event handlers for our custom element can then be added in the mix:

<polymer-element name="basic-elem">
  <template>
  	<input type="text" placeholder="What's your name ?" value="{{name}}"></input>
  	<br/>
    <span>Your name is {{name}}</span>
    <button on-click="btnClicked" >click me</button>
    <div on-mouseover="divHover" >hoverMe!</button>
  </template>
  <script>
  	Polymer('basic-elem',{
  		name:'nico',
  		btnClicked : function(){
  			console.log('btnClicked');
  		},
  		divHover:function(){
  			console.log('mouseHover');
  		}
  	});
  	
  </script>
</polymer-element>

and finally take advantage of the component ready and value attribute change events should we wish to do anything with the data later on:

<polymer-element name="basic-elem">
  <template>
  	<input type="text" placeholder="What's your name ?" value="{{name}}"></input>
    <input type="text" placeholder="What's your age ?" value="{{age}}"></input>
    <span>Your name is {{name}}</span>
  </template>
  <script>
 Polymer('basic-elem',{
      name:'nico',
      age:'32',
      ready:function(){
        console.log('basic-elem rocks!')
      },
      nameChanged:function(){
        console.log('nameChanged');
      },
      ageChanged:function(){
        console.log('ageChanged');
      }
  });
  </script>
</polymer-element>

Firing custom events which can be observed by other elements:

<polymer-element name="ouch-button">
  <template>
    <button on-click="{{onClick}}">Send hurt</button> 
  </template>
  <script>
    Polymer('ouch-button', {
      onClick: function() {
        this.fire('ouch', {msg: 'That hurt!'}); // fire(inType, inDetail, inToNode)
      }
    });
  </script>
</polymer-element>

<ouch-button></ouch-button>

<script>
  document.querySelector('ouch-button').addEventListener('ouch', function(e) {
    console.log(e.type, e.detail.msg); // "ouch" "That hurt!"
  });
</script>

Modules

I would be remiss to not mention the terrific work done by Dave Herman and the rest of TC39 bringing ES6 modules to the platform. The patterns for how they will work with Web Components are very much still being fleshed out, however the notion that we my someday not require our own custom architecture, or at least, a significantly thinner wrapper around platform primitives is an exciting future worth looking forward to.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment