The component approach

by Wilson Page / @wilsonpage

Break pages into chunks...

everything's a component

Component?

Definition

Encapsulation

Communication

Performance

Styling

Responsive

Component?

"A part that combines with other parts to form something bigger"

<video>

Public API

video.play() video.pause() video.load()

Events

'ended' 'error' 'playing' 'progress' 'waiting'

Markup - HTML

Styling - CSS

Behaviour - JS

Encapsulated

Controllable

Informative

Configurable

Reusable

Definition

You don't need a framework to get started with components


var MyComponent = function() {
  this.el = document.createElement('div');
};

MyComponent.prototype.render = function() {
  this.el.innerHTML = 'hello world';
};

var component = new MyComponent();
component.render();
document.body.appendChild(component.el);

Why use a framework then?

Consistent codebase

Interoperability

Abstract repetition

FruitMachine

github.com/ftlabs/fruitmachine

'A lightweight library for defining and assembling UI components'


var MyComponent = fruitmachine.define({
  name: 'my-component',
  template: function() {
    return 'hello world';
  }
});

// Usage
var component = new MyComponent();
component.render();
component.appendTo(document.body);

Why did we write our own?

'Retro-fittability'

Declarative layouts


{
  "module": "layout-a",
  "children": {
    "slot1": {
      "module": "header",
      "model": {
        "title": "My Web App"
      }
    },
    "slot2": {
      "module": "big-story",
      "model": {
        "title": "Story title",
        "body": "Story body..."
      }
    },
    ...
  }
}

var layout = fruitmachine(layoutJSON);

layout
  .render()
  .appendTo(document.body);

{{{ slot1 }}}
{{{ slot2 }}}

Server-side rendering

Crawlable content

Faster 'time to content'

Provide a <noscript> experience

Client & server share definitions

All views rendered as strings

Client enhances server generated HTML

Encapsulation

'The condition of being enclosed
(as in a capsule)'

Why is encapsulation good?

Promotes reuse

Decoupled from application

Lower barrier to entry

Improve sense of ownership

"The more tied components are to each other, the less reusable they will be; and the more difficult it becomes to make changes to one, without accidentally affecting another"

- Rebecca Murphey, jQuery Fundamentals.

// Component Code
var MyComponent = function() {
  var myElement = document.querySelector('.my-element');
  this.el = document.createElement('div');
  this.el.style.height = myElement.clientHeight + 'px';
};

// Application code
var component = new MyComponent();
document.body.appendChild(component.el);

// Component Code
var MyComponent = function(options) {
  this.el = document.createElement('div');
  this.el.style.height = options.height + 'px';
};

// Application Code
var myElement = document.querySelector('.my-element');
var height = myElement.clientHeight;
var component = new MyComponent({ height: height });
document.body.appendChild(component.el);

Treat each component as a 'mini-app'

Pass/inject outside dependencies

Ignorance is bliss

Communication

API & Events

Your app should control its components, never the reverse.


var app = require('app');

function MyImage(options) {
  this.el = document.createElement('img');
  this.el.src = options.src;
  this.el.addEventListener('click', app.showGallery);
};

// App code
var MyImage = require('my-image');
var image = new MyImage({ src: 'image.jpg' });

Events can save us!


function MyImage(src) {
  var self = this;
  events(this); // <=
  this.el = document.createElement('image');
  this.el.src = options.src;
  this.el.addEventListener('click', function() {
    self.fire('click'); // <=
  });
}

var MyImage = require('my-image');
var image = new MyImage({ src: 'image.jpg' });

image.on('click', showGallery); // <=

Events are awesome!

...but know when they're not appropriate


// Don't describe intention
this.fire('showgallery');

// Describe what happened
this.fire('click');

// ... and let the app decide what to do
image.on('click', showGallery);

API to cause something to happen

Events to signal something has happened

Fire from where the event happened

Bubbling


var image = layout.module('my-image');
if (image) {
  image.on('click', showGallery);
}

FruitMachine introduced event propagation to nested components


layout.on('click', showGallery);

layout.on('click', 'my-image', showGallery);

Performance

With hidden implementation, we forfeit some control

Components have full access to the DOM (which can be dangerous)

Layout is expensive.
Do it as little as possible.


componentA.innerHTML = 'Component A'; // <= Write
componentA.clientHeight; // <= Read (reflow)

componentB.innerHTML = 'Component B'; // <= Write
componentB.clientHeight; // <= Read (reflow)

componentC.innerHTML = 'Component C'; // <= Write
componentC.clientHeight; // <= Read (reflow)

'Layout thrashing'

How can we prevent it...?

FastDOM

github.com/wilsonpage/fastdom

FastDOM avoids layout thrashing, by batching DOM reads & writes.


fastdom.write(function() {
  componentA.innerHTML = 'Component A'; // <= Write
  fastdom.read(function() {
    componentA.clientHeight; // <= Read
  });
});

fastdom.write(function() {
  componentB.innerHTML = 'Component B'; // <= Write
  fastdom.read(function() {
    componentB.clientHeight; // <= Read
  });
});

'Layout' now occurs only once per frame

Layout boundaries

wilsonpage.co.uk/introducing-layout-boundaries

300% improvement in layout performance

Improved animation frame-rate

How can I make one?

Not be display inline or inline-block

Not have a percentage height value.

Not have an implicit or auto height value.

Not have an implicit or auto width value.

Have an explicit overflow value (scroll, auto or hidden).

Not be a descendant of a <table> element.


.my-component {
  width: 100%;
  height: 100px;
  overflow: hidden;
}

github.com/paullewis/boundarizr

Try a tool like FastDOM to harmonize thrashing components

Experiment with 'layout boundaries' to improve post-render layout performance

Styling

CSS is tricky to tame

'Componentising' makes things easier

Maximise portability

Minimise leakage

Pure discipline


Headline

Body content


article { /* styles */ }
h1 { /* styles */ }
p { /* styles */ }

Headline

Body content


.my-component { /* styles */ }
.my-component .title { /* styles */ }
.my-component .body { /* styles */ }

.title { color: red; }

Headline

Body content


.my-component {}
.my-component_title {}
.my-component_body {}

Responsive

In a responsive application, components need to change their appearance

@media covers styling,
but sometimes we need more...

What about behaviours?

Same markup

Different appearances

Different behaviours


var ArticleList = fruitmachine.define({
  name: 'article-list',
  breakpoints: {
    'column': {
      setup: function() {},
      teardown: function() {}
    },
    'row': {
      setup: function() {},
      teardown: function() {}
    },
     'small': {
      setup: function() {},
      teardown: function() {}
    }
  }
});

Detecting breakpoints

adactio.com/journal/5429


@media(orientation: landscape) {
  .article-list:after { content: 'column' }
}

@media(orientation: portrait) {
  .article-list:after { content: 'row' }
}

window.addEventListener('resize', function() {
  var style = getComputedStyle(articleList.el, ':after');
  style.content; //=> 'column'|'row'
});

We could now map JS breakpoints to CSS breakpoints

window.getComputedStyle()
...kinda expensive!

window.matchMedia


var media = window.matchMedia('(min-width: 500px)');

media.addListener(function(data) {
  alert('matches: ' + data.matches);
});

var ArticleList = fruitmachine.define({
  name: 'article-list',
  media: {
    'orientation: landscape': 'column',
    'orientation: portrait': 'row',
    ...
  },
  states: {
    'column': {
      setup: function() {},
      teardown: function() {}
    },
    'row': {
      ...
    }
  }
});

.my-component.column {
  /* 'column' styling */
}

.my-component.row {
  /* 'row' styling */
}

Can you re-use the same component with different breakpoints?


var peach = new Peach({
  media: {
    'orientation: landscape': 'column'
  }
});

Conclude

Tight scope to promotes reuse and reduces tangled code-bases

Use configuration to maximise usage

Expose a public interface

Use events to react to changes

Timeline your apps to keep an eye out costly DOM work

Smart CSS selectors for rock solid styling

Look to existing components (like <video>) for inspiration

window.matchMedia for 'behavioural' breakpoints

"The secret to building large apps is never build large apps. Break your applications into small pieces. Then, assemble those testable, bite-sized pieces into your big application"

- Justin Meyer, JavaScriptMVC

@wilsonpage