Javascript πΎ
We consider ourselves JS framework agnostic as we do not have a framework of preference. For some applications, not having a framework but instead using only ES 5/6 is the right choice, while in other cases React JS or Vue.JS are brought into the application stack.
Linting
We use and maintain our own ESLint shareable config for automated code checks conforming to our syntax conventions.
Formatting
-
Use soft-tabs with a two-space indent.
-
Prefer single quotes.
// Bad let string = "String"; // Good let string = 'String';
-
Use semicolons at the end of each statement.
// Bad let string = 'String' // Good let string = 'String';
-
Use strict equality checks (
===
and!==
) except when comparing against (null
orundefined
).// Bad string == 'String' // Good string === 'String';
-
Use a trailing comma after each item in a multi-line array or object literal, except for the last item.
// Bad { a: 'a', b: 'b', } // Good { a: 'a', b: 'b' }
-
Prefer ES6 classes over prototypes.
// Bad function Animal() { } Animal.prototype.speak = function() { return this; } // Good class Animal { speak() { return this; } }
-
When you must use function expressions (as when passing an anonymous function), use the arrow function notation.
// Base class class Request { function fetch() {} } // Bad let records = Request.fetch().then(function() { }) // Good let records = Request.fetch().then(() => { ... })
-
Prefer template strings over string concatenation.
// Bad 'string text' + expression + 'string text' // Good `string text ${expression} string text`
-
Prefer array functions like
map
andforEach
overfor
loops.// Bad for (var index = 0; index < myArray.length; index++) { console.log(myArray[index]); } // Good myArray.forEach(function (value) { console.log(value); });
-
Use
const
for declaring variables that will never be re-assigned, andlet
otherwise.
Naming
-
Use
lowerCamelCase
for file names.// Bad user_avatar.js // Good userAvatar.js
-
Use
PascalCase
for classes.// Bad class userAvatar {} class user_avatar {} // Good class UserAvatar {}
-
Use
lowerCamelCase
for variables and function.// Bad let dummy_variable = 'goof'; // Good let dummyVariable = 'goof';
-
Use
SCREAMING_SNAKE_CASE
for constants.// Bad const dummy_variable = 'goof'; // Good const DUMMY_VARIABLE = 'goof';
-
Use
lowerCamelCase
for key names in a constant.// Bad const DUMMY_OBJECT_VARIABLE = { PRIMARY_BUTTON: 'blue', SECONDARY_BUTTON: 'white' } // Good const DUMMY_OBJECT_VARIABLE = { primaryButton: 'blue', secondaryButton: 'white' }
-
Avoid
var
to declare variables, instead prefer usinglet
orconst
.// Bad var dummy_variable // Good let dummy_variable
-
Use
_singleLeadingUnderscore
for private variables and functions.// Bad class Animal { // private methods privateMethod() {} } // Good class Animal { // private methods _privateMethod() {} }
-
Define event handlers methods as βpublicβ methods (thus without a prefix
_
):// Bad class Dropdown { _onToggleClick(event) { } } // Good 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.// Bad clickHandler = (event) => {} buttonClick = (event) => {} // Good 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 = () => { } onLocationSeacchSelectLocationSuccess = () => { }
Project Structure
JS-only Applications
This structure applies to JS-only / Node.JS applications. Our architecture is inspired both by the accepted standards in the JS community but 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. We usually create one file per API resource. -
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. We usually create one file per utility of group of utilities. -
initializers/
: Code that initializes components used application-wide instead of having to bind each component on each page. We usually create one file per component initialized. -
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):
// Bad components/ βββ Button.js // Good 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 we need to define selector 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.js
βΒ Β β βββ 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
We 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);
}
We add a short description of the function followed by a list of parameters.