Clean ember addon component customization with ember-spread

When developing Ember.js-applications it's common to tap into the huge addon ecosystem that the Ember.js-community provides.

In my consulting projects I often see problems in client projects though that result due to the challenges that developers face when functionality (especially components) provided by addons need to be customized.

In this blog-post I want to give a quick glance into how to customize addon-components in a low friction way that is very flexible and easy to maintain in the long-run when it's necessary to wrap an addon-component and simply extending it isn't enough.

The scenario

Imagine you want to build an application that looks very Material-Design like. You already decided to use an existing material-library because you don't want to build a huge material-library from scratch but you still want to be able to customize the style that the library you chose provides to give your application a unique look.

A great way to bring material design into your Ember.js-application is by using the most excellent ember-paper-addon by Miguel Andrade. But ember-paper comes with a default material-design-look that your design-departement needs to customize.

Being a responsible developer you already had a thorough look at the ember-paper-addon and it seems very well implemented and maintained. There's no need to write a form-library from scratch when using ember-paper. You could but if you did you would most likely come up with something that's very similiar to ember-paper and nobody has time for that.

aintnobodygottime

When you look a little bit closer at the designs your design departement provided to you though, you realize that they decided to customize the way errors are displayed on input elements. Instead of the default way to display errors in ember-paper-inputs by displaying the error message below the user input your design departement wants to show a tooltip that contains the error message.

What ember-paper gives you:
default

What the design departement wants to have:
custom

Ok so what's the best way to customize paper-input?

Customizing paper-input

Ember-paper's paper-input comes with a build-in way to display error-messages but that's not how the design-department imagined things so we need to customize the display of error-messages.

You take a look at the paper-input-template and ember-paper gives you the option to hide its default error-messages by passing hideAllMessages to the paper-input-component. But you don't want to hide all errors you just want to display them differently than ember-paper does.

Fortunately for you the maintainers of ember-paper know what they are doing and have provided a way to customize paper-input by passing a block to the component (Thx Miguel! 🍻).

<!-- app/templates/components/custom-input.hbs -->

<!-- zomg this is awful and unmaintainable in the future -->  
{{#paper-input value=formObject.name onChange=(action (mut formObject.name)) errors=formObject.errors.name hideAllMessages=true as |i|}}
  {{#if i.isInvalidAndTouched}}
    {{custom-error-component errors=i.validationErrorMessages}}
  {{/if}}
{{/paper-input}}

Great success! You are now able to wrap that up into a wrapper-component of your own and everything is cool right? Not really.

Problems with the naive approach

The problem with this approach is that you now have to copy the entire interface that paper-input provides and pass all possible properties that paper-input can handle in your custom-input-template. Otherwise you run into a situation where your custom-input-component does not follow the same interface as ember-paper. You will miss out on new features or god forbid you have to come back to the component every time you realize that you are missing one feature of paper-input and you need to make it work in your custom 'component-fork'.

To actually make this work you now have to dive deep into the ember-paper-repository find all the properties that can be passed to paper-input and make sure that the same properties can be passed to custom-input as well because you need to pass them through. To make things worse you will have to do this for each component you want to use from ember-paper. This of course will be a pain to maintain and a possible source of problems in the future because with every upgrade of ember-paper you have to make sure your custom components still match the correct interface.

What you actually want is a clean way to pass all the properties that you are passing to custom-input down to paper-input in a simple way that you don't have to change any time upstream-ember-paper changes.

Javascript itself provides a clean way to do this - the spread-operator. What you want to do with your custom-input is basically to spread all properties passed to custom-input to paper-input.

Though Javascript provides a way to do this unfortunately neither Handlebars (Ember's templating library) nor Ember.js itself provide a built-in way to spread properties. There's an issue and PR open on the Handlebars-repo about this but at the time of writing this post there's no built-in way to do this in Ember.js.

We have to find a way around this limitation and with the ember-spread-addon we actually can.

Ember-spread to the rescue

The ember-spread-addon makes it possible to pass an options-hash to a component which will then be spread-out as if you passed the spread-out properties directly to the component:

<!-- the following two calls result in the same component state -->

{{paper-input options=(hash label="Name" value=formObject.name)}}

{{paper-input label="Name" value=formObject.name}}

To actually make this work with the paper-input-component we need to extend it and add the SpreadMixin-Mixin that ember-spread provides to it:

// app/components/paper-input.js
import Input from 'ember-paper/components/input';  
import SpreadMixin from 'ember-spread';

export default Input.extend(SpreadMixin);  

So now that we have the ability to spread the options-property passed to paper-input how do we actually pass the correct options to it?

We simply need to build up the correct option-hash in the custom-input-component and we can use Ember.Component's-lifecycle-hooks to do so:

// app/components/custom-input.js
import Ember from 'ember';

const { Component } = Ember;  
const { keys }      = Object;

export default Component.extend({  
  didReceiveAttrs({ newAttrs }) {
    this._super(...arguments);
    this.set('options', this.buildOptionsBasedOnAttrs(newAttrs));
  },

  buildOptionsBasedOnAttrs(attrs) {
    let attrsKeys = keys(attrs);

    return this.getProperties(attrsKeys);
  }
});

With the above implementation we create an options-hash that mirrors the properties passed to custom-input and will use these options together with ember-spread to pass the same properties to paper-input.

<!-- app/templates/components/custom-input.hbs -->  
{{#paper-input options=options as |i|}}
  {{#if i.isInvalidAndTouched}}
    {{custom-error-component errors=i.validationErrorMessages}}
  {{/if}}
{{/paper-input}}

Conclusion

Though we have to extend paper-input to use SpreadMixin in our own application I found this to be a pretty clean solution for wrapping addon-components in a sane way without the need to mirror component-interfaces in your own application and the maintainability issues that would come with that approach in the long-run.

To make it easier to do the same for all other addon-components we can bundle up this approach as a Mixin of course and use that in other wrapper-components that are custom to our application.

I hope this was helpful, thanks for reading and as always just drop me a line on twitter (@LevelbossMike) or ping me on the Ember.js-slack if you have questions. I am also available for consulting work and would be happy to help you and your company with your Ember.js-projects.

comments powered by Disqus