Javascript πŸ‘Ύ

Hero image for 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 or undefined).
// 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 and forEach over for 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, and let 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';
  • Avoid var to declare variables, instead prefer using let or const.
// 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) {
    this.notification = elementRef;

    this.onCloseNotification = this.onCloseNotification.bind(this);

    this._addEventListeners();
  }

  // Event Handlers

  onCloseNotification() {
    this.notification.classList.add(CLASS_NAME['CLOSED']);
  }

  // private

  _addEventListeners() {
    // ...
  }
}

When it comes to unit testing, event handlers are key methods that requires to be tested, hence the need to make them public.

  • 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) => {}

// 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 can 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 named src/ but it must also contain non-JS files such as assets/ (which are often stored in public).

  • 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 in support/).

  • ./: 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 requestManager from '../lib/requestManager';

class PaymentAdapter {
  /**
   * 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 requestManager('POST', `${config['api']['payment']}`, requestParams);
  }
}

export default PaymentAdapter;

Then use this adapter into components or screens:

PaymentAdapter.create({providerToken: 'XghYhKJUnkd', cardId: 'etst-xyuhd'})

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

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 using DEFAULT_SELECTOR from its component.
  • CategorySelect which using SelectInput 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';