Home

Awesome

Ember.js Style Guide

This document extends the JavaScript Style Guide to provide Ember.js specific guidance.

These guidelines are specific to:

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119

Table of Contents

Conventions

Grammar

Testing

Ember Data

File Structure

Architecture Philosophy


Computed Properties

Templates

Type checking

Comments

Since JSDoc does not yet currently fully support ES6 syntax its conventions do not always reflect the architecture of an Ember application. The style guides and examples in this section serve to remedy these discrepancies.

The ember-cli-jsdoc addon should be used to achieve the support presented in this guide.

JSDoc tags utilizing

Only the following Block Tags are acceptable for use in documenting your code:

Only the following Inline Tags are acceptable for use in documenting your code:

These tags are not currently allowed for use but once JSDoc offers additional support for them aligning with our needs we will begin using them. In the meantime the @augments tag should be used.

General rules
File structure

This commenting structure MUST be used as a template when creating new Component, Controller, Mixin, or Route files. Route files will not contain all of the represented structures. The "File Structure" Section describes what should be placed in each section.

...({

    // -------------------------------------------------------------------------
    // Dependencies


    // -------------------------------------------------------------------------
    // Attributes


    // -------------------------------------------------------------------------
    // Actions


    // -------------------------------------------------------------------------
    // Events


    // -------------------------------------------------------------------------
    // Properties


    // -------------------------------------------------------------------------
    // Observers


    // -------------------------------------------------------------------------
    // Methods

});
Modules
import Ember from 'ember';
import TooltipEnabled from 'sl-ember-components/mixins/sl-tooltip-enabled';

/**
 * Short description
 *
 * A longer description that
 * spans multiple lines
 *
 * @module
 * @augments ember/Component
 * @augments sl-ember-components/mixins/sl-tooltip-enabled
 */
export default Ember.Component.extend( TooltipEnabled, {
    ...
});
Functions
/*
 * @override
 * @function
 * @returns {undefined}
 */
config: function() {...},

/*
 * @function
 * @param {Number} first
 * @param {Number} second
 * @returns {Number}
 */
add: function( first, second ) {...}
Constants
/**
 * Number of months in a year
 *
 * @constant {Number}
 */
numberOfMonths: 12
Properties
/*
 * @override
 * @type {String}
 */
name: 'configuration',

/**
 * String representing the full timezone name
 *
 * @type {String}
 */
timezone: null,

/**
 * Emergency Notification model
 *
 * @type {?Array.<module:app/models/emergency-notification>}
 */
emergencyNotifications: null
Acceptable Deviations

To lessen the burden of creating documentation in scenarios where it does not add any value, it is not required to create full DocBlocks in the following scenarios:

In these scenarios a shortened, single-line comment containing the @type tag is all that is needed.

import Ember from 'ember';
import TooltipEnabled from 'sl-ember-components/mixins/sl-tooltip-enabled';

/**
* @module
* @augments ember/Component
* @augments sl-ember-components/mixins/sl-tooltip-enabled
*/
export default Ember.Component.extend( TooltipEnabled, {

    // -------------------------------------------------------------------------
    // Dependencies

    // -------------------------------------------------------------------------
    // Attributes

    /** @type {String[]} */
    classNames: [
        'alert',
        'sl-alert'
    ],

    /** @type {String[]} */
    classNameBindings: [
        'themeClassName',
        'dismissable:alert-dismissable'
    ],

    /** @type {String} */
    ariaRole: 'alert',

    // -------------------------------------------------------------------------
    // Actions

    /** @type {Object} */
    actions: {

        /**
         * Trigger a bound "dismiss" action when the alert is dismissed
         *
         * @function actions:dismiss
         * @returns {undefined}
         */
        dismiss: function() {
            this.sendAction( 'dismiss' );
        }
    },

    // -------------------------------------------------------------------------
    // Events

    // -------------------------------------------------------------------------
    // Properties

    // -------------------------------------------------------------------------
    // Observers

    // -------------------------------------------------------------------------
    // Methods

});
Example of the tags in use
import Ember from 'ember';
import TooltipEnabled from 'sl-ember-components/mixins/sl-tooltip-enabled';

/**
 * The provided command line arguments
 *
 * @ignore
 * @type {Array.<String, *>}
 */
var argv = require( 'minimist' )( process.argv.slice( 2 ) );

/**
 * @module
 * @augments ember/Component
 * @augments module:app/mixins/the-mixin
 * @augments sl-ember-components/mixins/sl-tooltip-enabled
 */
export default Ember.Component.extend( TooltipEnabled, {

    // -------------------------------------------------------------------------
    // Dependencies

    // -------------------------------------------------------------------------
    // Attributes

    /** @type {String[]} */
    classNames: [
        'alert'
    ],

    // -------------------------------------------------------------------------
    // Actions

    /** @type {Object} */
    actions: {

        /**
         * Trigger a bound "dismiss" action when the alert is dismissed
         *
         * @function actions:dismiss
         * @returns {undefined}
         */
        dismiss: function() {
            this.sendAction( 'dismiss' );
        }
    },

    // -------------------------------------------------------------------------
    // Events

    // -------------------------------------------------------------------------
    // Properties

    /**
     * Access level
     *
     * @constant
     */
    accessLevel = 1,

    /**
     * @typedef {Object.<String, Boolean>} ActionDivider
     * @property {Boolean} divider Must be set to true
     */

    /**
     * @typedef {Object} ActionOption
     * @property {String} action
     * @property {String} label
     */

    /**
     * Definition for the Actions button
     *
     * @example
     *  [{ divider: true },
     *  { action: 'cancelItem', label: 'Cancel Device' }]
     *
     * @type {Array.<ActionDivider|ActionOption>}
     */
    actionsButton: [
        { divider: true },
        { action: 'cancelItem', label: 'Cancel Device' }
    ],

    /**
     * Whether to make the alert dismissible or not
     *
     * @type {Boolean}
     */
    dismissible: false,

    /**
     * Array of filter objects
     *
     * @type {ember/Array}
     * @default ember/Array
     */
    filters: Ember.A(),

    /**
     * Alias for application loading flag
     *
     * @type {module:app/controllers/application~isLoading}
     */
    isLoading: Ember.computed.alias( 'controllers.application.isLoading' ),

    // -------------------------------------------------------------------------
    // Observers

    /**
     * Initialize children array
     *
     * @function
     * @returns {undefined}
     */
    init() {
        this._super( ...arguments );

        this.set( 'children', [] );
    },

    /**
     * React to route changes
     *
     * @function
     * @returns {undefined}
     */
    reactToRouteChange: Ember.observer(
        'currentRouteName',
        function() {
            ...
        }
    ),

    // -------------------------------------------------------------------------
    // Methods

    /**
     * Example callback definition
     *
     * @callback thisIsACallback
     * @param {Number} responseCode
     * @param {String} responseMessage
     */

    /**
     * Use the callback
     *
     * @function
     * @param {thisIsACallback} callback
     * @returns {undefined}
     */
    usingTheCallback: function( callback ) {
        ...
    },

    /**
     * Returns lowercase path when ember-cli-mirage is not enabled
     *
     * @override
     * @param {String} type
     * @returns {String}
     */
    pathForType: function( type ) {
        type = this._super( ...arguments );
        return config.isEmberCliMirageEnabled ? type : type.toLowerCase();
    },

    /**
     * The generated Bootstrap "theme" style class for the alert
     *
     * @function
     * @returns {String} Defaults to "alert-info"
     */
    themeClassName: function() {
        return 'alert-' + this.get( 'theme' );
    }.property( 'theme' ),

    /**
     * Does some secret-squirrel stuff
     *
     * Also refer to {@link module:app/components/info-button} for more details
     *
     * @private
     * @function
     * @param {ember/Array} input
     * @param {String} [modifier]
     * @throws {ember/Error} If `input` is empty
     * @returns {ember/RSVP.Promise} Resolution of promise contains possible types of String, Number, Boolean
     */
    secretStuff: function( input, modifier ) {
        Ember.assert(
            '`input` must not be empty',
            !Ember.isEmpty( input )
        );

        return Ember.RSVP.Promise();
    },

    /**
     * @abstract
     * @function
     * @returns {undefined}
     */
    showHandler() {}
});

/**
 * @memberof module:addon/components/sl-grid
 * @enum {String}
 * @property {String} LEFT "left"
 * @property {String} RIGHT "right"
 */
export const ColumnAlign = Object.freeze({
    LEFT: 'left',
    RIGHT: 'right'
});

Newlines

// SAMPLE: attributeBindings, classNames, classNameBindings

attributeBindings: [
    'data-target',
    'data-toggle',
    'disabled',
    'type'
],

classNames: [
    'form-group'
],

classNameBindings: [
    'themeClassName',
    'dismissable:alert-dismissable'
]


// SAMPLE: Ember.observer, Ember.computed

updateData: Ember.observer(
    'series',
    function() {
        ...
    }
),

themeClassName: Ember.computed(
    'theme',
    'anotherTheme',
    function() {
        ...
    }
)


// SAMPLE: Test assertions

assert.strictEqual(
    component.get( 'dismissable' ),
    true,
    'Component is dismissable'
);

Be explicit with Ember Data attribute types

Even though Ember Data can be used without using explicit types in attr(), always supply an attribute type to ensure the right data transform is used

// Good
export default DS.Model.extend({
    firstName: DS.attr( 'string' ),
    jerseyNumber: DS.attr( 'number' )
});


// Bad
export default DS.Model.extend({
    firstName: DS.attr(),
    jerseyNumber: DS.attr()
});

File Structure

The contents in a file MUST be organized in the following order for each type that exists:

Within each of the represented structures above the attributes, action, properties, etc MUST be listed alphabetically in their respective sections.

Where should DOM interactions occur in an Ember application?

Where to put actions in an Ember application

Routes

Defining functions to be called when events are triggered

Observers

A good pattern to follow for improved performance is the one presented in the video at https://youtu.be/cp1Jk92ve2s?t=1097 from timestamp 18:18 to 20:15