Skip to content

Latest commit

 

History

History
executable file
·
811 lines (634 loc) · 20.8 KB

README.md

File metadata and controls

executable file
·
811 lines (634 loc) · 20.8 KB

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

  • MUST always return a value
    • At minimum a null value
    • Other values are acceptable as appropriate

Templates

  • MUST use double quotes in all template syntax (e.g. attributes, property values, closure actions, etc) for non-bound values
    • MAY use single quotes if passing a value that contains a double quote, such as with {{the-component label='The "First" One'}}, though it is RECOMMENDED to use HTML entities or bound values.

Type checking

  • MUST be performed via the use of Ember.typeOf for every type except for Symbol.
  • Symbol MUST be checked via typeof, due to this Ember.js issue.

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
  • @type and @param

    • types MUST be their most generic form (e.g. Array vs ember.Array, Object vs ember.Object)
    • a * refers to native JavaScript types
    • more specific ember.* types MUST be used when warranted
  • @param

    • MUST use the JSDoc syntax for optional parameters and default values
    • MUST NOT use the Google Closure Compiler syntax
  • @override

    • This tag indicates that a symbol overrides a symbol with the same name in a parent class. Given this definition it could be argued that properties such as classNameBindings, tagName, and others would fall within this category and that beforeModel, setupController, and similar certainly do. Even with limited experience with Ember.js you will quickly learn to recognize these properties and methods and realize that they are overriding their parental definitions. Therefore, to reduce the tediousness of having to declare this tag in all of these places then, the following rules are to be observed:
      • This tag MUST NOT be used in Route or Component objects
      • This tag MUST be used as appropriate in all other locations (i.e. Ember Data application adapter)
  • It is RECOMMENDED to alphabetize the contents of the @param, @type, and @returns tags when multiple types are represented.

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
  • MUST have a DocBlock immediately preceding the definition
  • A short description...
    • ... MUST be present if there are no other DocBlocks in the file
    • ... MAY be included in all other scenarios as appropriate
  • MAY contain a long description
  • MUST contain a @module tag, listed as the first one
    • MUST NOT be followed by a value
  • MAY contain any of these tags:
    • @augments
    • @example
    • @see
    • @link
  • MUST NOT contain any other tags
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
  • MUST have a DocBlock immediately preceding the definition
  • SHOULD contain a short description
  • MAY contain a long description
  • MUST contain
    • @function
    • @override IF overriding a previously-defined function
    • @returns
  • MAY contain any of these tags:
    • @abstract
    • @callback
    • @example
    • @listens
    • @param
    • @private
    • @protected
    • @see
    • @throws
  • MUST NOT contain any other tags
/*
 * @override
 * @function
 * @returns {undefined}
 */
config: function() {...},

/*
 * @function
 * @param {Number} first
 * @param {Number} second
 * @returns {Number}
 */
add: function( first, second ) {...}
Constants
  • MUST have a DocBlock immediately preceding the definition
  • SHOULD contain a short description
  • MAY contain a long description
  • MUST contain
    • @constant
  • SHOULD contain
    • @default if a non-simple type
  • MAY contain
    • @example
    • @link
    • @see
  • MUST NOT contain any other tags
/**
 * Number of months in a year
 *
 * @constant {Number}
 */
numberOfMonths: 12
Properties
  • MUST have a DocBlock immediately preceding the definition
  • SHOULD contain a short description
  • MAY contain a long description
  • MUST contain
    • @type
  • SHOULD contain
    • @default if a non-simple type
  • MAY contain
    • @example
    • @link
    • @private
    • @protected
    • @see
  • MUST NOT contain any other tags
/*
 * @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:

  • Within...
    • Ember Components
    • Ember Controllers
  • For...
    • dependencies
    • attributes
    • actions hash (NOT the individual actions!)

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

  • Newlines MUST be employed in the following scenarios:
    • When defining:
      • attributeBindings
      • classNames
      • classNameBindings
    • When using:
      • Ember.observer
      • Ember.on
      • Ember.computed
      • Ember.assert
      • In assert statements in tests
// 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'
);
  • The requirement to use soft tabs set to 4 spaces is intended to be applied to JavaScript files but should also be applied to JSON files whenever possible. A scenario where this is not possible though, for example, is in the package.json file. Whenever the -save or --save-dev flags are used the file is re-created with 2 spaces so there is no need to try to maintain 4 spaces.

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:

  • Dependencies
    • services
  • Attributes
    • arrangedContent
    • attributeBindings
    • classNames
    • classNameBindings
    • defaultLayout / layout / layoutName
    • queryParams
    • sortBy
    • tagName
    • template / templateName
  • Actions
  • Events
  • Properties
  • Observers
  • Methods

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?

  • SHOULD be contained in the Component whenever possible. This is the closest layer to where the DOM was likely created.
  • If the interaction needs to be shared across Components or if there is an application-specific implementation required, then routes SHOULD be used.

Where to put actions in an Ember application

  • As close to where things are "happening". This is most usually the Component.
  • If the action needs to be shared with other Components or Routes, place it in the lowest-level of shared access between the items requiring shared access.

Routes

  • Manage application state
  • Can contain application-specific DOM knowledge and interaction, though Components should be the first place such needs should be implemented

Defining functions to be called when events are triggered

  • From Proper Use Of Ember on():
    • on() SHOULD NOT be used except for in cases of very rare exception
    • this._super( ...arguments ) MUST always be called
    • In init() the call to this._super( ...arguments ) should be before using this for anything else.

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