Ember – Conventions over Conventions

Today I want to talk about conventions and share something I’ve been doing as I’m working in my app that uses EmberJS for the front-end.

Warning: This post does assume a certain degree of familiarity with ember/ember-cli and I assume that you have all things installed necessary to run/build an ember app. There’s a full repository on github with the complete project, which I’ll reference from here.

We’re going to create a very simple app with components that have i18n, real-time validation, use DDAU (data down, actions up) and some baked-in conventions to make things more “robust”. It will look something like this:

Components with validation and i18n

Components with validation and i18n

The first thing is to create a new project using ember-cli (at the time of writing EmberJS is on version 2.9.1) on the terminal:

ember new blog-post-ember-conventions-steps

Then, since we’re going to use i18n, I recommend the excellent ember-i18n addon and to make things look good we’ll use ember-bootstrap and to make validations we’ll use ember-model-validator. Also, I like to use ember-truth-helpers, so I’m going to install them for later. To install just do the following on the terminal (see this commit for the results):

ember install ember-i18n

ember install ember-model-validator

ember install ember-bootstrap

ember install ember-truth-helpers

To finish installing ember-i18n, we’ll need two locales (so that we can switch between them), so we just type this in terminal (to create the english and portuguese locales, see this commit):

ember generate locale en

ember generate locale pt

With the required addons out of the way it’s time to generate a new route so that we can work on something (see this commit).

ember g route application

ember g controller application

And we’re going to need a model for this whole thing to work, so I’m going to create a person model:

ember g model person

We’re going to include a simple name and age attribute, so we get the following definition:

import DS from 'ember-data';

const {
 Model,
 attr
} = DS;

export default Model.extend({
 name: attr('string'),
 age: attr('number')
});

Component with DDAU

For starters we’re going to create a component to handle text/numeric values which adheres to Data Down Actions Up. In terminal type:

ember g component attribute-text

In our component template we’re going to use a standard bootstrap form definition (I’m omitting a way to bind the value on purpose):

<div class="form-group">
 <label title="{{placeholder}}" 
   class='control-label'
   for='{{attribute}}'>
   {{label}}
 </label>
 <input
   value={{get model attribute}}
   class='form-control'
   placeholder={{placeholder}}
   type='text'
   id={{attribute}}
 >
</div>

To make things dynamic, the value of the input uses the get helper which allows me to get a dynamic property, in this case from the model (in the {{get model attribute}} line).

If I now make my route return a new person instance (so that we have a model to work with)

routes/application.js
import Ember from 'ember';

export default Ember.Route.extend({
 model() {
   return this.store.createRecord('person');
 }
});

And I update my main application template to display two components like this:

<div class="container">
 <h2 id="title">Conventions, Conventions</h2>

 <div class="row">
 <div class="col-sm-6">
 {{attribute-text
   model=model
   attribute='name'
   label='Name'
   placeholder='Insert your name'
 }}
 </div>
 <div class="col-sm-6">
 {{attribute-text
   model=model
   attribute='age'
   label='Age'
   placeholder='Insert your age'
 }}
 </div>
 </div>

 {{outlet}}
</div>


We’ll get a result like the following (see this commit with the result of all changes):

initial-display

Initial Display

Now, it’s a start, but I said we wanted to used DDAU on our component, so we’re going to make just that, we’ll add an oninput handler.

value={{get model attribute}}
oninput={{action this.attrs.onUpdateValue value="target.value"}}
class='form-control'

We’re calling this action ‘onUpdateValue’, so we need to update the application template (and controller) to have these actions.

templates/application.hbs
...
placeholder='Insert your name'
onUpdateValue=(action 'setName')
....
placeholder='Insert your age'
onUpdateValue=(action 'setAge')

And in the controller, things look like this:

import Ember from 'ember';

const {
 Controller,
 get
} = Ember;

export default Controller.extend({

 actions: {
   setName(name) {
     let model = get(this, 'model');
     model.set('name', name);
   },

   setAge(age) {
     let model = get(this, 'model');
     model.set('age', age);
   }
 }
});

Ok, so now we have a DDAU compatible component, which is nice (see this commit).

Now let’s make things more interesting!

Conventions on i18n

Ideally if I need to show the same field of a given model, I would like to have the labels/placeholders all centralized in a localization file, so I’m going to do the following and put the following in my locales/en/translations.js (and the equivalent in /pt/translations.js) and not have to type them in the template every time.

export default {
 'model': {
   'person': {
     'name': {
     'label': 'Name',
     'help': 'This name will be used throughout the application to display your name',
     'placeholder': 'Person\'s complete name'
    },
  'age': {
    'label': 'Age',
    'help': 'This field is used to validate access to some parts of the app, must be 18 or older',
    'placeholder': 'The age (in years)'
    }
  }
 }
};

Now, to make this work I would have to the following transformation (and use the “t” helper from ember-i18n). Check this commit for the changes.

templates/application.hbs
...
{{attribute-text
   model=model
   attribute='name'
   label=(t 'model.person.name.label')
   placeholder=(t 'model.person.name.placeholder')
   onUpdateValue=(action 'setName')
}}

I mean, it’s kind of verbose, having to pass all that model.modelName.attribute.xxx over and over again, so I’m going to leverage conventions here. I know that my strings are in a translations file with the structure being:

model: {
  attribute: {
    label: 'The Label',
    placeholder: 'The Placeholder',
    help: 'The Help text'
  }
}

Let’s update our component to have some additional properties:

import Ember from 'ember';

const {
 Component,
 inject: { service },
 computed,
} = Ember;

export default Component.extend({
 i18n: service(),

 attribute: '',

 getTranslation(property) {
   return this.get('i18n').t(`model.${this.get('modelName')}.${this.get('attribute')}.${property}`);
 },

 modelName: computed.alias('model.constructor.modelName'),

 helpText: computed('i18n.locale', function() {
   return this.getTranslation('help').toString();
 }),

 label: computed('i18n.locale', function() {
   return this.getTranslation('label').toString();
 }),

 hasHelp: computed('helpText', function() {
   return this.get('helpText').length > 0;
 }),

 placeholder: computed('i18n.locale', function() {
   return this.getTranslation('placeholder').toString();
 })

});

We are going to leverage the fact that each DS.Model instance knows the model name (through constuctor.modelName) and create a helper method getTranslation which uses the model name and attribute name to get the translation using that structure we defined in the translations file (model.modelName.attribute).

Since we also pass the attribute name from the application template, we have everything we need. Just need to update the application template and remove the label and placeholder parameters:

{{attribute-text
  model=model
  attribute='name'
  onUpdateValue=(action 'setName')
}}

Also, we can now include the help text that is present in the translations file, in the component template file, like this (relevant commit):

{{#if hasHelp}}
  <span class="help-block">{{helpText}}</span>
{{/if}}

In order to switch between locales we can add two buttons and an action to our controller

templates/application.hbs
<button class='btn btn-default' disabled={{eq i18n.locale 'en'}} onClick={{action 'setLocale' 'en'}}>English</button>
<button class='btn btn-default' disabled={{eq i18n.locale 'pt'}} onClick={{action 'setLocale' 'pt'}}>Português</button>

And an action in the controller

controllers/application.js

const {
  Controller,
  get,
  inject: { service }
} = Ember;

export default Controller.extend({
  i18n: service(),

  actions: {
    ...
    setLocale(value) {
      this.set('i18n.locale', value);
    }
  }
}

And with this (see this commit), we finish the first part regarding conventions. We basically leverage a predefined structure (convention) in the translations file and the fact that each instance of a model knows the name of the model to retrieve all the values associated with a given attribute.

Part 2 – Preventing bugs/typos on attribute names using conventions

One of the things that I find a real problem is doing things like we did before while setting the value on the model:

setName(name) {
 let model = get(this, 'model');
 model.set('name', name);
},

Doing a set operation and passing a string like ‘name’ means that if we make a small typo on that ‘name’, we never find out about it until we go hunting a bug because some operation that depends on this value is having a weird behavior. This is a mistake I’ve done plenty of times, and the way I found to deal with my typos is the following:

I create a file in the utils folder of my app called person-utils.js in which I place the following content:

utils/person-utils.js
export default {
 PERSON: {
  NAME: 'name',
  AGE: 'age'
 }
};

This is basically a list of all attributes of the model Person, but as a JSON object that I can reference.

In my controller I now import this file and create a new method:

controllers/application.js
i18n: service(),
...
setField(field, value) {
 Ember.assert(`Must pass a valid attribute - ${field}`, !isEmpty(field));
 let model = get(this, 'model');
 model.set(field, value);
},

This method will be used to set the value of a field and assert that the field name is valid (as in, not empty), for this to work now we have to change our controller to look like this:

controllers/application.js
import Ember from 'ember';
import PersonUtil from '../utils/person';

const {
 Controller,
 get,
 inject: { service },
 assert,
 isEmpty
} = Ember;

const {
 PERSON
} = PersonUtil;

export default Controller.extend({

 setField(field, value) {
   assert(`Must pass a valid attribute - ${field}`, !isEmpty(field));
   let model = get(this, 'model');
   model.set(field, value);
 },

 actions: {
   setName(name) {
    this.setField(PERSON.NAME, name);
   },

   setAge(age) {
    this.setField(PERSON.AGE, name);
   }
 }
});

By importing the person-utils file with the constant PERSON which contains keys for each attribute, and using the calls to setField with a key from that object, If I make a mistake and do:

this.setField(PERSON.NEME); // This will fail because NEME does not exist

When I try to write on the name field, I’ll get a nice error message in the console, and even a stacktrace telling me where the problem is, like the following example:

Wrong parameter

Wrong parameter

The list of changes can be seen in this commit. This has prevented me a lot of bugs 🙂

Real-time validation

The last step in this tutorial is having validation as you type. In order to do that we must define a set of validations as per ember-model-validator‘s documentation, so we change our model to have the following (age must be an integer over 18 and name a string over 4 characters long):

models/person.js
import Validator from '../mixins/model-validator';

const {
 Model,
 attr
} = DS;

export default Model.extend(Validator, {
 name: attr('string'),
 age: attr('number'),
 validations: {
  name: {
    presence: true,
    length: {
    minimum: 4
   }
  },
  age: {
   presence: true,
    numericality: {
     allowBlank: true,
     onlyInteger: true,
     greaterThanOrEqualTo: 18
   }
  }
 }

Now, we need to update our component to display errors, which per bootstrap’s documentation is adding an “has-error” class to the outer div and a span with class “help-block” to display the messages.

<div class="form-group {{if (gt (get model (concat 'errors.' attribute '.length')) 0) 'has-error'}}">
   <label title="{{placeholder}}" class='control-label' for='{{attribute}}'>
   {{label}}
   </label>
   <input
    value={{get model attribute}}
    oninput={{action this.attrs.onUpdateValue value="target.value"}}
    class='form-control'
    placeholder={{placeholder}}
    type='text'
    id={{attribute}}
  >
 {{#each (get model (concat 'errors.' attribute)) as |error|}}
   <span class="help-block">{{error.message}}</span>
 {{/each}}
 {{#if hasHelp}}
   <span class="help-block">{{helpText}}</span>
 {{/if}}
</div>

We use the “gt” helper for ember-truth-helpers for check if the model has errors on the attribute that is linked to this component and if so, we add the “has-error” class.

With this now we are capable of showing errors, but we’re missing something that triggers the validation process. Since I want to have “real-time” validation, I’m going to use the actions in the controller that update the model, to also validate the field which I’m typing on.

controllers/application.js
validateField(field) {
 this.get('model').validate({ only: [field] });
},

setField(field, value) {
 assert(`Must pass a valid attribute - ${field}`, !isEmpty(field));
 let model = get(this, 'model');
 model.set(field, value);
 this.validateField(field);
},

These changes can be seen in the relevant commit.

EmberJS is a really great framework which leverages a lot of conventions (especially when coupled with the excellent ember-cli tools), by adding a few more of our own conventions on top of this project I think we get even more value.

Feel free to comment and suggest better approaches.

Happy coding!

Leave a Reply

Your email address will not be published. Required fields are marked *