Now Available React Programming: The Big Nerd Ranch Guide
Front-End ReactBased on our React Essentials course, this book uses hands-on examples to guide you step by step through building a starter app and a complete,...
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 |
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:
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.
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.
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
.
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.
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
.
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.
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.
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.
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.
Based on our React Essentials course, this book uses hands-on examples to guide you step by step through building a starter app and a complete,...
Svelte is a great front-end Javascript framework that offers a unique approach to the complexity of front-end systems. It claims to differentiate itself from...
Large organizations with multiple software development departments may find themselves supporting multiple web frameworks across the organization. This can make it challenging to keep...