Aurelia Material Design Lite

This article will present how to wrap Material Design Lite to use it within Aurelia views.

Context

From the official website, Aurelia is an open-source next gen JavaScript client framework for mobile, desktop and web that leverages simple conventions to empower your creativity.
It could be another JavaScript framework, accentuating the JavaScript framework fatigue but Aurelia has an interesting approach and its community is growing.

Material Design Lite let us add a Material Design look and feel to our websites. It doesn’t rely on any JavaScript frameworks and aims to optimize for cross-device use, gracefully degrade in older browsers, and offer an experience that is immediately accessible.

Material Design Lite will automatically register and render all MDL elements marked with MDL classes upon page load. Unfortunately in the case where we are creating DOM elements dynamically we need to register new elements using a javascript API!.

<div id="container"/>  
<script>  
  var button = document.createElement('button');
  var textNode = document.createTextNode('Click Me!');
  button.appendChild(textNode);
  button.className = 'mdl-button mdl-js-button mdl-js-ripple-effect';
  componentHandler.upgradeElement(button);
  document.getElementById('container').appendChild(button);
</script>  

All Arelia views are generated dynamically, so for all MDL elements inside those views we have to call componentHandler.upgradeElement(element). It is therefore impossible to use MDL directly with Aurelia.

The goal of this blog post is to deliver a plugin for Aurelia that allow the usage of MDL elements transparently within Aurelia's views.

Design

Aurelia offers 2 solutions (maybe 3 with global-behavior) to satisfy our need:

CustomElement

See Aurelia documentation for an explanation of this concept.

The most obvious at first glance, we'll map each MDL element to one or many Aurelia's custom elements, so instead of writing:

<button class="mdl-button mdl-js-button mdl-button--raised">Button</button>  

we will write:

<mdl-button class="mdl-button--raised">Button</button>  

Sounds good, but how to translate:

<button class="mdl-button mdl-js-button mdl-button--fab">  
  <i class="material-icons">add</i>
</button>  

We may design a mdl-button-icon to generate a slightly different html template:

<mdl-button-icon class="mdl-button--raised">add</button>  

Then how to wrap:

<a href="#" class="mdl-button mdl-js-button mdl-js-ripple-effect">Do it</a>  

It's clearly not the good solution!

CustomAttribute

See Aurelia documentation for an explanation of this concept.

Thanks to Aurelia's CustomAttribute, we'll be able to mimic MDL elements. Previous examples will be written as following:

<a href="#" mdl="button" class="mdl-js-ripple-effect">Do it</a>  
<button mdl="button" class="mdl-button--fab">  
  <i class="material-icons">add</i>
</button>  
<button mdl="button" class="mdl-button--raised">Button</button>  
Let's code

We defined the way to use MDL within our views.

Implementation has to cover 2 points:

  • how to register dynamically MDL elements
  • the creation of our CustomAttribute class

First, write our html code (app.html):

<div mdl="textfield" class="mdl-textfield--expandable">  
   <label mdl="button" class="mdl-button--icon" for="search">
      <i class="material-icons">search</i>
   </label>
   <div class="mdl-textfield__expandable-holder">
      <input class="mdl-textfield__input" type="text" id="search" />
      <label class="mdl-textfield__label" for="search">Enter your query...</label>
      </div>
   </div>
   <button mdl="button" class="mdl-js-ripple-effect mdl-button--icon" id="hdrbtn">
      <i class="material-icons">more_vert</i>
   </button>
   <ul mdl="menu" class="mdl-js-ripple-effect mdl-menu--bottom-right" for="hdrbtn">
      <li class="mdl-menu__item">About</li>
      <li class="mdl-menu__item">Contact</li>
      <li class="mdl-menu__item">Legal information</li>
   </ul>
</div>  

This code comes from here

All MDL elements have one dedicated class element-<type> and an optionnal one element-js-<type>. We will replace them by a custom attribute mdl="<type>" so we don't have to use them in our view and inject thoses classes dynamically later. All other classes should be used as we will do it directly with MDL.

Second, let's create our custom attribute:

import {inject, customAttribute} from 'aurelia-framework';  
import {componentHandler} from 'google/material-design-lite'; //(1)

@inject(Element) //(2)
@customAttribute('mdl')
export class MDLCustomAttribute {

    constructor(element) {
       this.element = element;
    }

    attached() { // (3)
    }
}

(1): we need this library to register MDL elements (it's out of this blog's scope to explain how to install external modules within an Aurelia project).

(2): MDLCustomAttribute#constructor will receive the current DOM element to be registrered to MDL

(3): this method will be invoked when the custom element is attached to the DOM

Third, for all elements associated with a custom attribute, we have to inject mdl classes and register them associated to their MDL Javascript classes (it's a little bit more complex because we have to manage ripple animation, but the rule is pretty simple, all children elements including the parent with a mdl-js-ripple-effectclass will be registreted as a MaterialRipple Javascript class).

import {inject, customAttribute} from 'aurelia-framework';  
import {componentHandler} from 'google/material-design-lite';  
import $ from 'jquery'; //(1)

let mdlTypes = { //(2)  
    button: {
        html: ["mdl-button", "mdl-js-button"]
      , js: ['MaterialButton']
      , fct: [manageRipple]

    }
  , textfield: {
       js: ['MaterialTextfield']
     , html: ["mdl-textfield", "mdl-js-textfield"]
  }
  , menu: {
       js: ['MaterialMenu']
     , html: ["mdl-menu", "mdl-js-menu"]
     , fct: [manageRipple]
  }
}

function manageRipple(element){//(3)  
  let classes = $(element).attr('class');
  if(classes.split(' ').indexOf('mdl-js-ripple-effect') != -1) componentHandler.upgradeElement(element, "MaterialRipple");
  let elts = $(element).find(".mdl-js-ripple-effect").get();
  for(let elt of elts) componentHandler.upgradeElement(elt, "MaterialRipple");
}


function upgradeElement(element, type){ //(4)  
  let {fct=[], html, js=[]} = (mdlTypes[type] || {});
  if(html) $(element).addClass(html.join(' '));
  for(let type of js) componentHandler.upgradeElement(element, type);
  for(let f of fct) f(element); 
}

@inject(Element)
@customAttribute('mdl')
export class MDLCustomAttribute {  
    constructor(element) {
       this.element = element;
    }
    attached() {
      upgradeElement(this.element, this.value);
    }
}

(1): we need jquery to inject classes (2): for each element's type (only a subset here), we define Javascript classes to associate to the element, HTML classes to add and processes that are required to finalize registration (3): managing ripple animation is the only process to complete (4): whole registration method

And that's all...

You can find the code above on github and a running demo here.