- General Structure [ Properties ] [ Multi-property ]
- Destructuring [ Objects ] [ Get/Set ]
- Data [ Data Binding ]
- Computed Properties [ Brace Expansion ]
- CSS [ Usage ]
- Actions [ Location ]
- Error Handling [ Overall Application Errors ]
- Testing
- Definition of Ready
- Addons [ Versioning ]
Why do we structure our Javascript code? This allows us to follow a consistent coding conventions. Paired with a tool to enforce code structure, such as Eslint, it will provide a way for us to make sure that our code follow a specific specified order.
-
1.1 Controller/Component Property Order: follow this order for properties and methods
- Properties
- Put ember specific properties (e.g.,
classNames
,tagNames
) before custom properties (e.g.,someSpecificAshProp : true
)
- Put ember specific properties (e.g.,
- Component lifecycle hooks (in order) see order in documentation
- Custom methods
actions
go last
- Properties
-
1.2 Model Property Order: follow this order for defining attributes and relationships:
- Attributes
- Relationships
- Single line function
- Multi line function
//Bad
firstName: attr('string'),
fullName: belongsTo('person'),
lastName: attr('string')
//Good
firstName: attr('string'),
lastName: attr('string'),
fullName: belongsTo('person')
-
1.3 Default Unassigned Property
-
When declaring an unassigned property we should default it to
null
instead ofundefined
. -
Why? Because defining a property as
undefined
has the meaning of we didn't define it. By setting it tonull
you are explicitly telling the app that the property has been declared.
-
// bad
foo: undefined
//good
foo: null
-
Why? Better readability.
-
All components written in handlebars that have at least one property should be written in a multi-line format.
-
Closing handlebars
}}
should be on a new line when writing in a multi-line format
-
// bad
{{my-component CreamSoda='nope' Pepsi='nope'}}
{{Bryan-component theRightWay='nope'}}
{{my-component
Sprite='nope'
Fanta='nope'}}
//good
{{my-component
Coke='yes'
SierraMist='yes'
}}
{{Everyone-component
theRightWay='yes'
}}
Extract multiple values from data stored in objects and arrays.
Why? Destructuring allows you to import only which classes you need and then extract (or destructure) only the properties that you need. This makes our modules more efficient.
In Ember 2.16 and above the recommended way to access framework code in Ember applications is via the JavaScript modules API. This makes Ember feel less overwhelming to new users and start faster. These effects are felt because of replacing the Ember global with a first-class system for importing just the parts of the framework you need. JavaScript modules make the framework easier to document, make the distinction between public and private API much easier to maintain, and provide opportunities for performance work such as tree-shaking.
//Bad
import Ember from 'ember';
export default Ember.Component.extend({
session: Ember.inject.service(),
title: 'ASH Development'
});
//Good
import Component from '@ember/component';
import { inject as service } from '@ember/service';
export default Component.extend({
session: service(),
title: 'ASH Development'
});
Destructuring get
and set
will allow you pass in a POJO, rather than being limited to just the current object with the this
keyword.
//Bad
this.set('isDestructured', false);
this.get('isDestructured'); //false
//Good
import { get, set } from '@ember/object';
set(this, 'isDestructured', true);
get(this, 'isDestructured'); //true
set(someObject, 'isUpdated', true);
get(someObject, 'isUpdated'); //true
Any new code written by ASH should only contain one-way data-binding. Third-party addons using two-way data-binding is ok, but be cautious and conscious of the side effects it can have.
Why? Two-way data-binding can have unexpected side effects; data flowing one way keeps things more predictable. For example: if you pass the same data to 2 components and they both are allowed to change it. DDAU allows the parent to arbitrate those changes and resolve conflicts. Otherwise component A can make changes you are not expecting in component B. This is not just a matter of 2 compoents, you can make a change to a child that propagates through the whole app.
//my-cheesy-component.js
//The below code would update the parent and any other component the parentData object was passed to
set(this, 'model.cheese', 'gouda')
//my-cheesy-component.js
actions: {
quesoChanger(e) {
e.preventDefault();
get(this, 'changeCheese')(get(this, 'cheese'));
}
}
//parent component.js, router, or controller
actions: {
newCheesePlease(cheeseType){
set(this, 'model.cheese', cheeseType);
}
}
Twiddle Demo: click here
When a computed property depends on multiple properties of the same object, specify the properties using brace expansion.
Why? Using brace expansion in computed properties makes our code more organized and easier to read, as it organizes dependent keys.
// Bad
fullname: computed('user.firstname', 'user.lastname', function() {
const { firstname, lastname } = get(this, 'user');
return `${firstname} ${lastname}`;
})
// Good
fullname: computed('user.{firstname,lastname}', function() {
const { firstname, lastname } = get(this, 'user');
return `${firstname} ${lastname}`;
})
CSS is permitted (and encouraged) in apps and addons under certain circumstances
Why? Flow, interaction and breakpoints generally belong to the component and not the domain (host site). Properties such as colors, fonts styles, etc. should belong to host site, so that each site can have its own identity. Moving CSS into component files will also cut down on the size of domain CSS bundles and help mitigate the issue of shipping a lot of CSS that belongs to components not in use on that site.
- Box Model
- e.g.,
padding
,height
,margin
,border-width
- Display
- e.g.,
display:flex
,flex-direction: column
- Animations and Transitions
- Certain Font Styles
font-weight
only
Use discretion when adding colors to apps/addons
- Neutral/non-branding colors that aren't likely to change across websites are OKAY to add to that app's/addon's app styles
- Talk to UX if there is any ambiguity
Site-specific Colors
- Do not add site specific colors or site specific variables that are not resuable
Text styles
- e.g.,
line-height
,font-family
these are set on a per site basis
// Bad
// app/style/appName.scss
.chats {
padding: 1rem;
display: flex;
background: $color1;
.chatMsg {
font-family: $font1;
font-weight: 700;
text-transform: uppercase;
background: $color2;
color: $color1;
transition: all 1s ease-in-out;
}
}
// Good
// base/social.scss
.chats {
background: $color1;
.chatMsg {
font-family: $font1;
font-weight: 700;
text-transform: uppercase;
background: $color2;
color: $color1;
}
}
// app/style/appName.scss
.chats {
padding: 1rem;
display: flex;
.chatMsg {
transition: all 1s ease-in-out;
}
}
- Form Actions should be placed on the form element
//Bad
<form>
<input id="firstName" type="text" />
<input id="lastName" type="text" />
<button type="submit" {{action 'SubmitForm'}}> Submit</button>
</form>
//Good
<form {{action 'SubmitForm' on='submit'}}>
<input id="firstName" type="text" />
<input id="lastName" type="text" />
<button type="submit"> Submit</button>
</form>
- Button actions that are not in a
<form>
should be placed on the<button>
itself
//Bad
<div class="container" {{action 'showHide'}}>
<button type="submit">Submit</button>
</div>
//Good
<div class="container">
<button type="submit" {{action 'showHide'}}>Submit</button>
</div>
Every app should contain a base error function within the application route.
Why? Developers are not always perfect, this will insure that even missed errors from other components, controllers or routes will be handled at the application level. Uncaught errors lead to a bad user experiences.
//Example code for route/application.js
import Route from '@ember/routing/route';
import { get, set } from '@ember/object';
export default Route.extend({
genericError: 'Hmm, something went wrong.',
actions: {
error(error){
//grabbing app container for a place to put the error so it will show to the user
const app = document.getElementById('app-container');
//getting the content for the error to show the user
if(typeof error === 'string') {
//if its a string set the errorToShow property to that string
set(this, 'errorToShow', error);
} else if (error && error.message) {
//if it has an error.message log the message to the console for debugging
console.error(error.message);
//if it is not a string set the errorToShow property to the genericError
set(this, 'errorToShow', get(this, 'genericError'))
} else {
//if it is not a string set the errorToShow property to the genericError
set(this, 'errorToShow', get(this, 'genericError'))
}
//adds the error to the app container for users to see error message, so they are not left with a blank app container
app.innerHTML = `<div class='error'>${get(this, 'errorToShow')}</div>`
}
}
});
Why? To allow us to establish and hold to a standard of code coverage in all of our apps with meaningful test writing and the ability to gate deployments when those standards are not met.
We use two devDependencies to compile our test scripts.
cross-env
- For properly setting the NODE_ENV on Windows test environmentsember-cli-code-coverage
- For testing the percentage of code coverage with Istanbul
The scripts
section in your package.json file should include the following...
"scripts": {
"test": "cross-env COVERAGE=true ember test",
"test-server": "cross-env COVERAGE=true ember test --server"
}
As of Ember CLI 2.12, ember comes installed with ember-cli-eslint
. This will output lint errors in the local server command line, as well as display errors in unit, integration, and acceptance tests.
Any content that can be updated should have a loading indicator.
Use the ash-loader
addon for this.
A scenario for when the API returns a server error should be considered. Create a 404 template using the ash-four-oh-four
addon.
Determine where the app could break and catch errors to keep the user informed.
As new logic is added to the app, the appropriate tests should be set up to ensure that future updates don't interfere with your current work.
Be sure to utilize the ember-cli-code-coverage
addon and set up the appropriate npm tests as outlined above.
ASH Front End Principles denote that branch coverage should be a minimum of 75% total test coverage, and 50% for tests not including acceptance tests.
ember-a11y-testing
should be installed, configured, and added to unit, integration, and acceptance tests.
If the app makes API calls, ember-cli-mirage
should be installed and configured to match the real API.
Test the app in every browser that we support.
If Mirage is being used, and the real API is available, test with both sets of data in each browser.
Current Supported Browsers: ie11, firefox, safari (desktop and mobile), and chrome
Configure stg.ashui build with Mirage data
Configure production build with API data
Create a build definition for the app that will:
- set npm registry path
- run
npm install
oryarn
- run
bower install
- run
npm test
- run
ember build --environment=preview --output-path=preview
- run
ember build --environment=production
- copy files to the drop location
- publish files to stg.ashui and the build location
- publish Code Coverage results
- notify the appropriate Slack channels
npm test
(in the build definition) will catch errors and reject build
- We define
1.0.0
release as the first version ready to be used by the end user. It may not be feature complete, but it should bring some benefit to our customers.1.0.0
release should include tests and be bug free according to the developer. As soon as the package is ready to be used in production, it needs to be on version1.0.0
.
We follow Semantic Versioning standards for versioning our addons:
-
Major version should be used when making incompatible API changes.
-
Minor version should be used when adding new functionality in a backward compatible manner.
Note: When updating
ember-cli
, use minor update.
- Patch version should be used when making backward compatible bug fixes, documentation updates, small enhancements (e.g., performance) that do not add new feature, or dependency updates.