Skip to content
Blueprint

Ember Blueprints: Why They're Awesome

Brit-turned-Queenstowner and keen coder Jonathan on why he's a fan of Ember Blueprints. Note: This is part one of a. . . (yet-to-be-decided-how-many-parts) series on this subject.

The longer title for this blog would be Ember Blueprints – More Productivity, Less Duplication, Fewer Mistakes, Less Cost.

There are numerous client-side Javascript frameworks to chose from these days, each with their own advantages and disadvantages. Finding the right one for your organisation is going to involve looking at a lot of factors, from hiring, to number of projects, to speed of development, and much more. Here at Media Suite we predominantly work with Ember, and it’s a choice we’re very happy with.  

This is the first part of a series on building a real-world Ember Blueprint to create crud screens based off Ember models and how it can be distributed as an Ember Add-On. While this post is about very Ember specific functionality, it’s simple enough that anyone with front-end experience can probably see what all the moving the parts are doing.

To Err Is Human

To give you a sense of scope, our team of around 15 developers typically work through five to eight Ember applications a year, in addition to maintaining existing ones. The applications tend towards data entry and display, with lots of forms, maps, and graphs. They’re designed to be easy to use and great to look at, but at their heart, it’s basic CRUD (Create Read Update Delete) functionality.

A lot of common patterns are re-used. For example, we frequently have View, Edit and Add pages for a given business object – e.g. a Person. In Ember, and in many frameworks, this is a formally a “model”.

Writing and wiring together a collection of routes, templates and components in Ember is not difficult, but there are a good number of moving parts. It is also vital that each instance of this add/edit/new functionality is consistent, using the same nomenclature, structure and organisation. When we churn through many projects, and offer support across a much wider number, we should play to Ember’s strengths and minimise the cognitive burden of moving from one to another.

Unfortunately, when different people do the same thing, they tend to do it in subtly different ways. It’s also very easy to introduce trivial bugs when wiring together many modules, especially if you’re cut-and-pasting from previous examples. Code review can, should, and does catch many of these instances – but that takes time to complete, and we believe you can drive development time down while increasing quality.

To Generate Programmatically, Divine

Backend frameworks have a great line in auto-generation of behaviours and code. Our backend framework, Loopback auto-generates endpoints for defined models, vastly reducing the time it takes to get a project up and running. Django, a Python web framework, is able to automatically create HTML input forms based on your model definitions leaving you simply to style them and add more application specific behaviours.

With automatic code generation as a starting point, we know that every developer will begin with the same structure for a common pattern. This provides consistency.

Since the code generation has also carried out the leg-work, hours of cut-and-pasting from the last time a similar feature was used is saved. It also removes the head-scratching when you don’t wire it together correctly. Auto-generation of code reduces development effort and increases quality.

Ember and Angular do offer Blueprints – the ability to automatically generate code for common structures. These are great, but limited to very common use-cases. Fortunately Ember offers the ability to author custom Blueprints, so developers are free to create auto-generation of code for anything that is repeatable enough to be worthwhile.

(Note: Angular’s CLI was initially a clone of the Ember CLI, but custom Blueprints and Add-Ons are not yet supported.  Expect to see it in time, though it won’t be until at least version 2.)

Docs on BluePrints

Contrary to the excellent Ember documentation, the documentation for authoring Blueprints leaves a little to be desired.  It’s pretty common to spelunk into the existing Blueprints for clues.

The existing Blueprints are in a number of different places, but the most up to date versions are here: https://github.com/emberjs/ember.js/tree/master/blueprints

The introduction guide can be found here: https://ember-cli.com/extending/#guide

The actual documentation for the API can be found here: https://ember-cli.com/api/classes/Blueprint.html

The latter is a great resource once you already have a good idea at what you need to do, but a little lacking for your initial foray.

The first step is to use the Ember CLI to generate a new Blueprint.

> ember generate blueprint model-crud
installing blueprint
create blueprints/model-crud/index.js

This will provide us with a directory structure like this:

Folder structure

The index.js file is the engine behind the Blueprint.  When ember generate model-crud person is executed on the command line, it’s going to get called and be responsible for ensuring all the correct files with the correct content are written out to the filesystem.

Adding In Blueprint Templates

Files added into the files folder (configurable via the filesPath hook) of the Blueprint will be written into the project in the matching location. A picture says a thousand words, so…

Folder structure showing the blueprint files

Everything is pretty straightforward until the  __modelToken__ and similar file names, which require a bit more explanation.

fileMapTokens Hook

The file names that look like __someToken__ are placeholders that we want to replace when the Blueprint is executed.  In this example, we’d like replace the __modelToken__ with the name of the dasherised entity name, e.g. __modelToken__ becomes person, so we have a components/person folder structure.

The fileMapTokens hook in the index.js file is where these file ‘tokens’ are defined.  There’s nothing special about the double-underscore syntax, but it is a convention, and Ember is all about consistent convention. The code that accompanies this looks like:

fileMapTokens: function() {
// Return custom tokens to be replaced in your files
return {
__modelToken__: function (options) {
return options.dasherizedModuleName;
},
__editModelToken__: function (options) {
return `edit_${options.dasherizedModuleName}`
},
__viewModelToken__: function (options) {
return `view_${options.dasherizedModuleName}`
}
},

The return value from this function should be an object, where the key value is the file name, or section of file name, to be replaced. The value is a string, or a function returning a string, with which to replace it.

The dasherizedModuleName is, as  might be expected, the dasherized version of the string passed into the ember generate command.  For example: ember generate model-crud SystemUser will provide an options.dasherizedModuleName value of system-user.

locals Hook

The filenames have now been written with the correct name, but the content of the files is still required. The nice thing is, this step is also not too complicated once you understand what is going on.

Each file is an EJS (Embedded Javascript) template, except the output doesn’t need to be HTML, but instead will most likely be *.hbs and *.js files.  Actually any file type is allowed, the parser will simply evaluate any content within <% %> as Javascript and output other text without modification.

The shortcut syntax of <%=variableName%> will inject the value of variableName directly into the template.

Here’s the New model Route Javascript file as an example:

import Ember from 'ember'
<%=newMixinImport%>
const {get} = Ember

export default Ember.Route.extend(<%=newEditMixinName%>, {
model () {
return {<%=camelizedModel%>: get(this, 'store').createRecord('<%=modelToken%>')}
},
cancelTransition () {
this.transitionTo('/')
}
})

Which in turn, when executed with ember generate model-crud person will output the following:

import Ember from 'ember'
import NewOrEditPersonMixin from '../mixins/routes/new_edit_person'
const {get} = Ember

export default Ember.Route.extend(NewOrEditPersonMixin, {
model () {
return {person: get(this, 'store').createRecord('person')}
},
cancelTransition () {
this.transitionTo('/')
}
})

The variables newMixinImport, newEditMixinName, camelizedModel and modelToken have been replaced in the file with their values.

The locals hook inside the index.js file is where we can set these values.  Below is the code required to populate the template we’ve just seen.

// const S = require('string')
locals: function (options) {
const camelizedModel = S(options.entity.name).camelize().s
return {
newMixinImport: `import NewOrEdit${capitilizedModel}Mixin from '../mixins/routes/new_edit_${options.entity.name}'`,
newEditMixinName: `NewOrEdit${capitilizedModel}Mixin`,
modelToken: options.entity.name,
camelizedModel

The locals hook must return either an object, or a Promise resolving to an object, of key/value pairs where the key is the variable name as used in the template and the value is the string that will be injected.

A More Detailed Template Example

The new Route was a very simple example, but we can look at a more complicated example below.  This is the afterModel hook from the Mixin, used by Add and Edit Routes, that fetches all of the options for the belongsTo relationships, in order to correctly populate the drop-down lists.

afterModel (model) {
// Need to fetch all the related models for belongsTo
<%
const belongsTo = components
.filter(component => component.type === 'belongsTo')
%>
return RSVP.hash({<%
belongsTo.forEach((component, index) => {%>
<%=component.options%>: get(this, 'store').findAll('<%=component.relatedModel%>')<%= index !== belongsTo.length - 1 ? ',' : '' %>
<%})%>
}).then(({<%=belongsTo.map(component => component.options).join(',')%>}) => {
<%belongsTo.forEach((component) => {%>
set(model, '<%=component.options%>', <%=component.options%>)
<%})%>
return model
})
},

Which outputs the rather more simple:

afterModel (model) {
// Need to fetch all the related models for belongsTo

return RSVP.hash({
permissions: get(this, 'store').findAll('permission')

}).then(({permissions}) => {

set(model, 'permissions', permissions)

return model
})
},

availableOptions Explained

It may be you would like to add additional command line arguments to your Blueprint, which you can do using the availableOptions property on the index.js file.  For instance, if we want the model name to be different to the entity name, our command line execution could look like ember generate model-crud SystemUser –model=person.

If we execute that command without letting the Ember CLI know we want to use the –model argument, we are admonished that The option ‘–model’ is not registered with the generate command. Run ember generate --help for a list of supported options.”

The following setting will quieten Ember CLI and allow us to access the model value inside the locals hook directly on the options argument of that function.

I.e. options.model === “person” // true

availableOptions: [
{
name: 'model',
type: String,
default: ''
}
],

It’s worth noting that this availableOptions method is not in the documentation, but examples of it’s use can be seen the “route” Blueprint here

Order of Execution for Index.js Hooks

The documentation does not cover in which order the lifecycle hooks of the Blueprint fire.  Following a little digging, I’ve outlined it below.  The definition for each hook can be found in the  documentation, but a few additional notes wouldn’t go amiss.

filesPath

The path to the blueprint’s files.  The default structure is covered in more detail above.  Is called repeatedly throughout execution.

normalizeEntityName

By default, will produce a dasherized version of the string passed into the ember generate command. Is a great place to provide a default.

E.g. ember generate model-crud <thisWillBeDasherized>

locals

Returns an object or promise containing the variables that can be used in the Blueprint’s templates.

fileMapTokens

Returns an object containing the variables that can be used in template filenames.

beforeInstall

The final option to trigger something before the files are generated and written.  For example, can run other operations here in order to prepare the ground for the Blueprint.

At this point the Blueprint generates the files and writes them to the filesystem

afterInstall

Fires after the files have been written, and is a good opportunity to inform the user of any additional action they may need to take.

There are also beforeUninstall and afterUninstall hooks.  However, it is unclear from documentation and experimentation under what circumstances they fire for Blueprints.

Parsing Models is Hard

The core piece of this Blueprint is to look through an Ember model, understand what it’s attributes are, and than make sensible default decisions about how to represent that as a form.

However, I have a confession; trying to parse the model file as Javascript is hard and I was unable to do it within a reasonable timeframe.  The reason being that the Blueprint executes in a CommonJS Node environment, and the Ember model is an ES6 module.  Even using an ES6 parsing library, it’s not possible to resolve all of the model’s imported modules from within the Blueprint’s execution context.

The next approach was to try and import the generated Javascript file from the dist folder, in this example dist/my-first-blueprint.js. This is an AMD structure for modules, and none of the readily available AMD libraries appear to allow easy consumption of this file either.

Presumably there is code added to dist/vendor.js that allows the Ember Resolver to find the correctly referenced module.  However, this is where my knowledge of the Broccoli build chain and Ember internals fail me.   

The final option is to parse the Javascript file as text using pattern matching.  With the combinations of mixins, different root classes and other variances, this is definitely non-trivial and is something that has been deferred to a separate Blueprint.

In the meantime, here at Media Suite we automatically generate most of our models directly from the database schema.  We will now simply output an accompanying config json file for each model that this Blueprint process can more easily consume.  In addition, this gives us the option to specify a bit more detail about the model’s attributes and how we would like it to display – such as which field to render into a drop-down for belongsTo and hasMany relationships.

Pulling It All Together

The full code for this Blueprint can be found here.

It covers the rough spec for the Model json config, and how the belongsTo relationship is handled within the templates.

A Word on hasMany and belongsTo

Typically we implement a belongsTo relationship as a searchable, drop-down list where the user can pick a single value, though in some cases it could be more appropriate to use radio buttons.  hasMany would be either the same type of drop-down but with multi-select capabilities, or checkboxes.

The UI side of this is relatively straight-forward – the json config file for each model allows us to specify which fields of the related model are relevant for display.  The saving side for hasMany is touch more complicated, as you need to save the related model, not the primary model which is being modified.  This becomes more complicated again if it is a many-to-many relationship, where you may well need to save the relationship model.

This level of uncertainty means that for time-being, hasMany relationships remain on the todo list for this Blueprint.

The full file listing for this Blueprint looks like this:

Full folder structure of ember blueprint

And This Is Just the Beginning

All this work gives us a pretty nice starting point, but that’s all it really is.  Right now, this Blueprint is limited to the project we wrote it in, and we can’t actually test it until we use it with a live project and a real back-end serving data.

In the follow-up post, I’ll take this code, move it into an Ember Add-On that can be installed by any project, and provide a test suite to demonstrate it behaves as it should. Read part two here: Ember Blueprints part 2: putting Ember Blueprints to the test

Media Suite
is now
MadeCurious.

All things change, and we change with them. But we're still here to help you build the right thing.

If you came looking for Media Suite, you've found us, we are now MadeCurious.

Media Suite MadeCurious.