Upcoming and OnDemand Webinars View full list

Creating Ember Blueprints

Todd Gandee

Thousands of years ago before the dawn of… wait, sorry. In 2012, Yeoman was introduced at Google I/O. Yeoman was an easy way to generate baseline html projects, paving the way for command-line-interface(CLI) tools like Ember-CLI. And one of best features of Ember-CLI is the built-in command ember generate or ember g, which adds files, directories, test files and important lines of code to your project.

Most tutorials for Ember will tell you to generate files when you are first starting out, and for good reason. Ember Blueprints create basic modules extending the correct Ember objects and put the file in the correct directory. Most blueprints will create corresponding test files. When you are new to Ember, these steps save a lot of time.

Even as you work with Ember more frequently, the ~36 built-in blueprints will help you save some time on smaller projects. For bigger projects or tasks you find yourself doing over and over, Ember allows you to build your own. In this post, we will walk through the basics of creating your own blueprint, adding some command-line prompts and creating a custom file structure for your blueprint.

Name Links
Ember-CLI API https://ember-cli.com/api/
Blueprints https://ember-cli.com/api/classes/Blueprint.html
UI https://ember-cli.com/api/classes/UI.html

Create a New Ember App

For this example, you can create a new Ember application or skip this step to add the blueprint directly to your existing project:

ember new my-first-blueprint
cd my-first-blueprint

Once inside your application directory, use the generate command to create a new blueprint:

Create Blueprint

ember g blueprint my-special-blueprint

This command will add a new directory /blueprints/my-special-blueprint to the project. It will also add a file called index.js. This is a node file to be run when your blueprint is called.

Creating a new directory

Try running:

ember g my-special-blueprint
> installing my-special-blueprint

> The `ember generate <entity-name>` command requires an entity name to be specified. For more details, use `ember help`.

The output will be an error telling you that blueprints require a name option. Let’s dive into the inner workings of how blueprints set this value.

Three Main Callbacks

normalizeEntityName

The command returned an error asking for an entity-name argument. This argument can be set when calling the command. Also, it can be edited or created from the index.js file with normalizeEntityName method hook. Add the following to the blueprints/my-special-blueprint/index.js:

module.exports = {
  description: 'My Special Blueprint',
  normalizeEntityName: function(entityName){
    return entityName || "special-entity-name";
  }
};

Run the generate command again, ember g my-special-blueprint. This time there are no errors, and we’re finally making progress. Adding this function actually did nothing for the application. Instead, adding this function set the entityName property to the value passed in from the command line, or “special-entity-name”, on the object that is passed around during the lifecycle of calling ember g my-special-blueprint.

fileMapTokens

To see this blueprint’s object value in use, let’s jump to the next hook in the process, fileMapTokens. Tokens are not for taking the subway today; they are for naming files and directories dynamically in your blueprint. Let’s have the blueprint install a style file in your app’s styles directory. We want the style file’s name to match the generated entity’s normalized name, so we’ll need to rename it before it gets installed in place. This is a job for fileMapTokens. Let’s see how! In the filesystem, add the following directories and .scss file to the /blueprints/my-special-blueprint directory:

blueprints
  my-special-blueprint
    files
      app
        styles
          __styleToken__.scss

In the file __styleToken__.scss, add the following code:

// -----------------------------
// Style Module
// -----------------------------

Now, back in the /blueprints/my-special-blueprint/index.js the fileMapTokens() hook needs to be added. To properly name the style file with the token __styleToken__, the blueprint will need a function to return a value to replace the file’s name token. Add the following to the index.js file:

module.exports = {
  description: 'My Special Blueprint',
  normalizeEntityName: function(){
    return "special-entity-name";
  },
  fileMapTokens: function(options) {
    // Return custom tokens to be replaced in your files
    return {
      __styleToken__: function(options) {
        console.log(options.dasherizedModuleName);
        return "_" + options.dasherizedModuleName;
      }
    }
  }
};

The naming of the file and the fileMapToken key, __styleToken__, with double underscores before and after name might seem a bit weird. It is common convention rather than a requirement. Naming the file and fileMapToken styleToken without underscores will work. Having the double underscores will signal a name that will be replaced rather than a static file that will be copied to the user’s app directory.

Once again, run ember g my-special-blueprint to see the output of the blueprint. This time the terminal console should print:

$ ember g my-special-blueprint
installing my-special-blueprint
special-entity-name
  create app/styles/_special-entity-name.scss

This time text was printed to the command-line and a file was created in the directory app/styles/ with the name _special-entity-name.scss. The fileMapTokens function can return multiple naming tokens for any directory as well as files. If you name your directory __someDirectoryToken__ you can create a file token callback:


fileMapTokens: function(options) {
  // Return custom tokens to be replaced in your files
  return {
    __someDirectoryToken__: function(options) {
      return options.dasherizedModuleName + "-directory";
    }
  }
}

Creating file map tokens allow you create dynamically generated files and nested directories in your project.

locals

The last built-in hook to discuss is locals. This function will be used to return objects to template files created when executing the blueprint. The file created above has the following contents:

// -----------------------------
// Style Module
// -----------------------------

When blueprints install template files, they get run through an Embedded JavaScript (EJS) interpreter, first. This runs any code embedded between <%= and %>, like <%= “I am a string!” %> and puts its output in place of the code snippet tag. Use this to inject information into the style file installed by our blueprint, like so:

// -----------------------------
// Style Module <%= classifiedModuleName %>
// File Location: /styles/<%= entity.styleModule.moduleName %>
// Made for Ember App: <%= dasherizedPackageName %>
// -----------------------------

Running the blueprint command again will yield an error. When creating the template the object entity has not been defined. This is where locals comes in. In the blueprints/my-special-blueprint/index.js file add the following:


fileMapTokens: function(options) {
  . . .
},
locals: function(options) {
  var fileName = options.entity.name;
  options.entity.styleModule = { moduleName: "_" + fileName + ".scss" };
  return options;
}

Finally, run the blueprint command ember g my-special-blueprint in the terminal. There will be a prompt to overwrite the file, type “Y” to overwrite. Once complete, open the newly created file in your app/styles directory. It should have the following contents:

// -----------------------------
// Style Module SpecialEntityName
// File Location: /styles/_special-entity-name.scss
// Made for Ember App: my-first-blueprint
// -----------------------------

The object entity held the key styleModule.moduleName. The template also used values passed in with the options object for the keys classifiedModuleName and dasherizedPackageName. These are commonly used strings passed around in the callback arguments for blueprints. In summary, when creating files with blueprints, you have access to commonly used strings created from the blueprint name, and you have the ability to dynamically create string values in the blueprint index.js file with the method locals.

Beyond the Structured Callbacks

The main methods of creating blueprints are displayed above: normalizeEntityName, fileMapTokens, locals. There are also bookend methods that allow you to freeform the scaffolding process with beforeInstall and afterInstall. These methods take the same options object passed to the other callback methods and they should return that object or a promise. These functions are a good place to add Bower, NPM packages or other Ember Addons to the project, prompt the user with questions, generate other blueprints and do any file clean-up needed.

Adding Modules with Blueprints

Ember-CLI relies on NPM and Bower to install module packages. When creating blueprints, external packages can be essential to the usage of scripts or components. Ember-CLI provides methods to add one or many Addons or packages from Bower or NPM with good descriptive names: addAddonToProject, addAddonsToProject, addBowerPackageToProject, addBowerPackagesToProject, addPackageToProject, addPackagesToProject.

Kind of Package Add One (name, target) Add Many [{name:,target?:}]
Ember CLI Addon addAddonToProject addAddonsToProject
Bower Package addBowerPackageToProject addBowerPackagesToProject
NPM Package addPackageToProject addPackagesToProject

The singular method names accept 2 arguments “name” and “target”, like “jQuery” and “~1.11.1” where name is the registered name of the package and target is the version, tag or github release. The methods for multiple packages have a “s” in the name for noun, “Addons” or “Packages” and accept an array of objects each with a key for “name” and an optional key for the target. Each of these methods returns a promise making them good candidates for the return statement for either beforeInstall or afterInstall.

Try out adding a package within the function beforeInstall:

locals: function(options) {
  . . .
},
beforeInstall: function() {
  return this.addAddonToProject("ember-moment");
}

Finally, run ember g my-special-blueprint to see the Addon added using generate. While installing you will see the command-line console print these lines:

install addon ember-moment
Installed packages for tooling via npm.
installing ember-cli-moment-shim
Installed addon package.
Installed addon package.
  identical app/styles/_special-entity-name.scss

This read-out shows your Addon was installed then the styles/_special-entity-name.scss file was created. Next, add a package in afterInstall:

beforeInstall: function() {
  return this.addAddonToProject("ember-moment");
},
afterInstall: function(){
  return this.addBowerPackageToProject("bootstrap", "~3.3.7");
}

The console now reads:

installing my-special-blueprint
  install addon ember-moment
Installed packages for tooling via npm.
installing ember-moment
  install addon ember-cli-moment-shim
Installed packages for tooling via npm.
installing ember-cli-moment-shim
Installed addon package.
Installed addon package.
  identical app/styles/_special-entity-name.scss
  install bower package bootstrap
  cached https://github.com/twbs/bootstrap.git#3.3.7
Installed browser packages via Bower.

Bootstrap was installed via Bower after the file was created. Considering the .add__Package__ToProject methods return a promise, you can chain .then() calls to trigger other function calls after installs. This is a good place to call other methods like insertIntoFile or lookupBlueprint. Those methods are for another post about blueprints. If you are looking to nest blueprint calls, calling a built-in blueprint like ember g route or ember g component, lookupBlueprint().install() is the method you are looking for. Using insertIntoFile is the way to add text to existing files. Look for a future post to see those methods in depth.

Prompting the User

The last topic is prompting the user to answer questions about the scaffold process. The Ember-CLI object that handles the command-line user interface is aptly named UI. The method in this object is ui.prompt(). Ember-CLI extends the NPM module Inquirer.js for this method. It accepts a question object with variety of keys. The first 3 keys are required: type, name, message. The default prompt type is “input,” assigned as a string, and I found it was a required argument. (So much for defaults!) Start with a basic question with the following format to see where the user input is retrieved. Add the following to the blueprint’s index.js file:

  afterInstall: function(){
    var self = this;
    return this.ui.prompt({
      type: "input",
      name: "framework",
      message: "Do you want to install Bootstrap?"
    }).then(function(data) {
      console.dir(data);
      if(data.framework === "Y") {
          return self.addBowerPackageToProject("bootstrap", "~3.3.7");
      }
      return null;
    });
  }

Run the blueprint to see the prompt. In the callback function for then() the data argument is printed as { framework: 'Y' }. At this point, there is a conditional checking for the match on the string “Y”. This basic example of user input shows the possibilities of prompting the user. This isn’t very useful for the case of adding a style framework to any project via a blueprint.

Next, create a prompt with choices:


  afterInstall: function(){
    var self = this;
    return this.ui.prompt({
      type: "list",
      name: "framework",
      message: "Which framework would you like to install?",
      choices: [
        {name: "SCSS - Bootstrap-Sass", value: {name: "bootstrap-sass"}},
        {name: "CSS - Bootstrap 3.3", value: {name: "bootstrap", target: "~3.3.7"}},
        {name: "SCSS - Bootstrap 4-alpha", value: {name: "bootstrap", target: "4.0.0-alpha.3"}},
        {name: "none", value: null}
      ]
    }).then(function(data) {
      console.dir(data);
      if(data.framework) {
          return self.addBowerPackageToProject(data.framework.name, data.framework.target);
      }
    });
  }

What you just wrote, or copied, was a lot. These prompt calls can get bulky if you want to program in complex choices for scaffolding. Let’s walk through all that you typed. First, you declared a variable self to be used in the promise callback. You will see this in a number of examples as a common practice. Next, change the question type to a “list”. The list type needs choices, which can be added as an array or a function that returns a choices array. The array can contain strings for simple answers or an object with 2 keys: name and value. You have added values that are also objects to be parsed to load specific style frameworks modules from Bower. Using console.dir() or console.log() has been the best way to debug the data passed around all the promise callbacks.

What’s Next?

This has been an introduction into creating a blueprint with the built-in hooks and utilizing some of the blueprint methods for adding libraries and prompting the user for choices. normalizeEntityName, fileMapTokens and locals are the main functions to control how your files are created in your app tree structure and the text that will fill those files. beforeInstall and afterInstall are the wrapper functions to do anything else in the blueprint like install dependencies, like add text to existing files and any other pre-processing action your blueprint needs to be effective.

Look for a future post about nesting other built-in blueprint installs, removing files and adding text to existing files.

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project