JavaScript πΎ
The team considers itself JS framework agnostic as there is no single framework of preference. For some applications, not having a framework but instead using only ES6 is the right choice, while in other cases React or Vue.JS are brought into the application stack.
Linting
Whenever possible, use and maintain Nimbleβs ESLint shareable config for automated code checks conforming the syntax conventions at Nimble.
Formatting
-
Use soft-tabs with a two-space indent.
-
Prefer single quotes.
let string = "String";
let string = 'String';
-
Use semicolons at the end of each statement.
let string = 'String'
let string = 'String';
-
Use strict equality checks (
===
and!==
) except when comparing against (null
orundefined
).string == 'String'
string === 'String';
-
Use a trailing comma after each item in a multi-line array or object literal, including the last item.
{ a: 'a', b: 'b' }
{ a: 'a', b: 'b', }
-
Prefer ES6 classes over prototypes.
function Animal() { } Animal.prototype.speak = function() { return this; }
class Animal { speak() { return this; } }
-
Use the arrow function notation when using function expressions (as when passing an anonymous function).
// Base class class Request { function fetch() {} }
let records = Request.fetch().then(function() { });
let records = Request.fetch().then(() => { ... });
-
Prefer template strings over string concatenation.
'string text' + expression + 'string text'
`string text ${expression} string text`
-
Prefer array functions like
map
andforEach
overfor
loops.for (var index = 0; index < myArray.length; index++) { console.log(myArray[index]); }
myArray.forEach((value) => { console.log(value); });
-
Use
const
for declaring variables that will never be re-assigned, andlet
otherwise. -
Group the imports and order by built-in, external, and internal modules
import _ from 'lodash'; import fs from 'fs'; import foo from 'src/foo';
import fs from 'fs'; import _ from 'lodash'; import foo from 'src/foo';
Naming
-
Use
lowerCamelCase
for file names.user_avatar.js
userAvatar.js
-
Use
PascalCase
for classes.class userAvatar {} class user_avatar {}
class UserAvatar {}
-
Use
lowerCamelCase
for variables and function.let dummy_variable = 'goof';
let dummyVariable = 'goof';
-
Use
SCREAMING_SNAKE_CASE
for constants.const dummy_variable = 'goof';
const DUMMY_VARIABLE = 'goof';
-
Use
lowerCamelCase
for key names in a constant.const DUMMY_OBJECT_VARIABLE = { PRIMARY_BUTTON: 'blue', SECONDARY_BUTTON: 'white', };
const DUMMY_OBJECT_VARIABLE = { primaryButton: 'blue', secondaryButton: 'white', };
-
Avoid
var
to declare variables, instead prefer usinglet
orconst
.var dummyVariable
let dummyVariable
-
Use
_singleLeadingUnderscore
for private variables and functions.class Animal { // private methods privateMethod() {} }
class Animal { // private methods _privateMethod() {} }
-
Define event handlers methods as βpublicβ methods (thus without a prefix
_
):class Dropdown { _onToggleClick(event) { } }
class Dropdown { onToggleClick(event) { } }
-
Methods must be placed in the following order: static, public then private
class Notification { static render(type, message) { // ... } constructor(elementRef) { // ... } // Public onCloseNotification() { // ... } // Private _privateMethod() { // ... } }
-
Use the following pattern to name event handlers:
on
+element/node
+event type
. The element/node can be omitted if itβs redundant.clickHandler = (event) => {} buttonClick = (event) => {}
onButtonClick = (event) => {} onTouchStart = (event) => {}
When defining and binding custom events, the same pattern applies:
// Given this component and event listener to a custom event const locationFilter = document.querySelector('.location-search__input'); locationFilter.addEventListener(LOCATION_SEARCH_SELECT_LOCATION_SUCCESS, this.onSelectLocationSuccess); // The event handler will be the following: onSelectLocationSuccess = () => { } onLocationSearchSelectLocationSuccess = () => { }
Project Structure
JS-only Applications
This structure applies to JS-only / Node.JS applications. This architecture is inspired both by the accepted standards in the JS community and also by the Ruby on Rails framework πͺ .
app/
βββ assets/
βββ helpers/
βββ initializers/
βββ components/
βββ screens/
βββ index.js
bin/
βββ build.js
βββ setup.js
config/
βββ locales/
βββ index.js
dist/
βββ ...
lib/
βββ middlewares/
server/
βββ index.js
spec/
βββ support/
.eslintrc.json
package.json
-
app/
: Close to 100% of the application code must be in there. Itβs often namedsrc/
but it must also contain non-JS files such asassets/
(which are often stored inpublic
). -
bin/
: CLI scripts e.g. application setup, distribution build or deployment. -
config/
: The configuration files for the application such as environment variables and locales. -
dist/
: The compiled application code and assets for production deployment. Do not commit this directory to the Git repository. -
lib/
: Non-specific application or shared code. -
server/
: The Node.JS web server configuration and boot scripts. -
spec/
: The tests for the application and the config for the test environment (stored insupport/
). -
./
: The root of the styles folder contains the NPM package configuration and other dot files.
If a library or framework e.g. Ember.JS has its own directory structure then use the one provided by the library or framework.
Front-end JS
This structure applies to applications in which JS is used solely on the front-end i.e. in a Ruby on Rails application.
βββ assets
βΒ Β βββ fonts
βΒ Β βΒ Β βββ ...
βΒ Β βββ images
βΒ Β βΒ Β βββ ...
βΒ Β βββ javascript
βΒ Β βΒ Β βββ adapters
βΒ Β βΒ Β βββ components
βΒ Β βΒ Β βββ config
βΒ Β βΒ Β βββ helpers
βΒ Β βΒ Β βββ initializers
βΒ Β βΒ Β βββ lib
βΒ Β βΒ Β βββ polyfills
βΒ Β βΒ Β βββ screens
βΒ Β βββ stylesheets
βΒ Β βΒ Β βββ ...
All JS files must be stored into a single sub-directory named javascript
:
-
adapters/
: Code in charge of making network and/or async tasks. Creating one file per API resource is recommended. -
components/
: Re-usable and stateless user interface components. -
config/
: The configuration files for the application such as environment variables. -
helpers/
: Any utilities used in the project. Creating one file per utility of group of utilities is recommended. -
initializers/
: Code that initializes components used application-wide instead of having to bind each component on each page. Creating one file per component initialized is recommended. -
lib/
: Non-specific application or shared code. -
polyfills/
: Provides browser specific enhancement. Usually, this covers code bridging the gap to a feature not yet available in all browsers and extensions of built-in functionality. -
screens/
: Page specific components which are in charge of coordinating the re-usable components and/or adding functionality that does not fit into a component. These components are also considered as root elements/containers when it comes to event handling.
Adapters
-
Avoid performing remote network calls inside components or screens. Instead, decompose this functionality into adapters classes:
// In a file named payment.js import config from 'config'; import BaseAdapter from './base'; class PaymentAdapter extends BaseAdapter { /** * Create a new payment * * @param {Object} [payment] - payment attributes * @return {Promise} - a promise which will resolve to the payment completion response */ static create(payment) { let requestParams = { payment: { provider_token: payment.providerToken, card_id: payment.cardId } }; return this.prototype.postRequest(`${config['api']['payment']}`, requestParams); } } export default PaymentAdapter;
Then use this adapter into components or screens:
PaymentAdapter.create({providerToken: 'XghYhKJUnkd', cardId: 'etst-xyuhd'})
-
Prefer creating a base adapter class then extend it for each adapter.
-
In the base adapter, setup the generic request configuration (e.g. authorization token, content-type) which will be used by all adapters:
import requestManager from '../lib/requestManager'; const SELECTOR = { csrfToken: 'meta[name="csrf-token"]' }; class BaseAdapter { /** * Generates request headers for authenticated routes. * Appends the csrf token in the header, required by protected Rails routes. * * @return {Object} A request headers. */ static requestHeaders(multipart = false) { const csrfToken = document.querySelector(SELECTOR.csrfToken); const token = csrfToken.getAttribute('content'); const headers = { 'X-CSRF-Token': token, 'Accept': 'application/json' }; return ({ headers }); } /** * Make a GET request. * * @param {string} endpoint - the API endpoint to use. * @param {Object} params - params to be sent. * @return {Promise} A Promise that will resolve into an object or reject * with an error object for its reason. */ getRequest(endpoint, params) { return requestManager('GET', endpoint, params); } }
Components
-
Use a folder structure with an index file (and other files when required):
components/ βββ Button.js
components/ βββ Button/ β βββ index.js
This is both a future-proof measure and a mean to break down components into small meaningful modules:
components/ βββ Button/ β βββ index.js β βββ icon.js
-
All files that related to the component (e.g. template files, configuration) must be located in the same directory:
components/ βββ QuestionList/ β βββ templates β β βββ answer.hbs β β βββ question.hbs β βββ config.js β βββ index.js
-
Usually, the
contructor
method contains 3 private methods_bind
,_setup
and_addEventListeners
:-
_bind
: bind all functions to the local instance scope. Mostly used for binding event handler methods. -
_setup
: bootstrap component actions such as elements initialization. -
_addEventListeners
: groupaddEventListener
of componentβs elements together.
const CLASS_NAME = { closed: 'notification--closed' } class Notification { constructor(elementRef) { this.notification = elementRef; this.dismissButton = document.querySelector('.notification__dismiss-button'); this._bind(); this._setup(); this._addEventListeners(); } // Event Handlers onCloseNotification() { this.notification.classList.add(CLASS_NAME['closed']); } // Private _bind() { this.onCloseNotification = this.onCloseNotification.bind(this); } _setup() { this._setTitleText(); } _addEventListeners() { this.dismissButton.addEventListener('click', this.onCloseNotification); } _setTitleText() { // ... } }
-
Initializers
Each initializer must import a component and a selector to bind the component to:
import Dropdown from '../components/dropdown';
document.querySelectorAll('[data-toggle="dropdown"]').forEach(dropdown => {
new Dropdown(dropdown);
});
Each initializer is then imported by an index file which will be imported from the manifest file application.js
:
βββ initializers
βΒ Β βββ calendar.js
βΒ Β βββ dropdown.js
βΒ Β βββ index.js
βΒ Β βββ modal.js
βΒ Β βββ notification.js
initializers/index.js
has the following content:
import './calendar';
import './dropdown';
import './modal';
import './notification';
Screens
Use a file or folder structure with an index file (and other files when required):
// Single file
screens/
βββ Home.js
screens/
βββ Home/
β βββ index.js
Each screen must import its required components and any needed default selector:
import SelectInput from '../components/selectInput/';
import BookingForm, { DEFAULT_SELECTOR as BOOKING_FORM_SELECTOR } from '../components/bookingForm/';
Define selector for screen and element reference which component is binded to:
const SELECTOR = {
screen: '.home.index',
categorySelect: '.category-select'
}
Example of HomeScreen
that contain following components:
-
BookingForm
which usingDEFAULT_SELECTOR
from its component. -
CategorySelect
which usingSelectInput
component so the selector is needed to be defined in this screen.
import SelectInput from '../components/selectInput/';
import BookingForm, { DEFAULT_SELECTOR as BOOKING_FORM_SELECTOR } from '../components/bookingForm/';
const SELECTOR = {
screen: '.home.index',
categorySelect: '.category-select'
}
class HomeScreen {
constructor() {
this.bookingButton = document.querySelector(BOOKING_FORM_SELECTOR.bookingButton);
// Bind Function
this._clickBookButtonHandler = this._clickBookButtonHandler.bind(this);
this._setup();
this._addEventListeners();
}
// Private Methods
/**
* Setup components
*/
_setup() {
new BookingForm();
new SelectInput(document.querySelector(SELECTOR.categorySelect));
}
/**
* Bind event listeners
*/
_addEventListeners() {
// ...
}
}
// Setup the HomeScreen only on the home page
let isHomePage = document.querySelector(SELECTOR.screen) != null;
if (isHomePage) {
new HomeScreen();
}
Each screen is then imported by an index file which will be imported from the manifest file application.js
:
βββ screens/
βΒ Β βββ Checkout/
βΒ Β β βββ index.js
βΒ Β βββ Home/
βΒ Β β βββ index.js
βΒ Β βββ index.js
screens/index.js
has the following content:
import './Checkout';
import './Home';
Bundle/manifest files
Following this architecture, the manifest files only contain initializers
and screens
(in this respective order):
import './initializers';
import './screens';
Documentation
Follow JSDoc convention to document each function:
/**
* Validate the Facebook account url.
*
* @param {string} url - the Facebook account url.
* @return {boolean} Return true if the url is valid, otherwise, return false.
*/
_validateFacebookUrl(url) {
const urlPattern = /http(s)?:\/\/(www\.)?(facebook|fb)\.com\/[A-z0-9_\-.]+\/?/i;
return urlPattern.test(url);
}
Add a short description of the function followed by a list of parameters.