JavaScript πŸ‘Ύ

Hero image for 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 or undefined).

    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 and forEach over for 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, and let 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 using let or const.

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

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

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 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. 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: group addEventListener 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() {
        // ...
      }
    }
    

It’s NOT compulsory for every component to have all these methods. Only define them if needed.

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 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.