JavaScript Conventions

Use $.Deferred() over native Promise()

Native Promise objects are not yet fully supported by the browsers we need to cover. Until that happens, use jQuery’s Deferred Object to execute asynchronous actions. Implement success/rejection handlers using Promise-like patterns.

For example:

// Deferrals and promises are synonmous.
const promises = [p1, p2, p3]
$.when(...promises).then(onSuccess, onReject)

Stick closely to the native Promise pattern that jQuery’s Deferred object replicates fairly closely:

  • Use $.when(...promises) as a substitute for Promise.all(promises) where promises is an Array list of deferred actions.
  • Use deferred.then(success, reject) as a substitute for promise.then(success, reject)
  • Use deferred.always(onFinally) as a substitute for promise.finally(onFinally)
  • Use $.Deferred().resolve(value) as a substitute for Promise.resolve(value)
  • Use $.Deferred().reject(value) as a substitute for Promise.reject(value)

Defining class private methods

Since we’re using ESM and exporting our class definitions, we rely on the module (or file) scope to contain private read-only methods that then referenced by class instances to replicate “private” behavior.

Define the private method in the global scope:

function privateMethod (arg1, arg2) {
  // Do something
  return 'something'
}

In the class definition, call the global function and bind the calling context if the function needs access to the class instance (i.e. this).

class SomeClass () {
  publicMethod () {
    // Use .call if passing context with comma-separated args.
    privateMethod.call(this, arg1, arg2)

    // Use .bind to pass context with private method function.
    this.listenTo(this.$el, 'click', privateMethod.bind(this, arg1, arg2))
  }
}

Native iterators vs Underscore

ES6 expanded its native iterators over collections, some that overlap with functions of Underscore’s utility library. We generally favor native APIs over third-party ones. To help discern when to use which, the following examples will help with the decision.

const ingredients = [
  { name: 'cilantro', type: herb },
  { name: 'green onions', type: herb },
  { name: beets, type: vegetable },
  { name: 'lemongrass', type: herb }
])

Use underscore iterator functions:

const ingredientsByType = _.groupBy(ingredients, (ingredient) => ingredient.get(type))
// => {
//   herb: [
//     { name: 'cilantro', type: ‘herb’ },
//     { name: 'green onions', type: ‘herb’ },
//     { name: 'lemongrass', type: ‘herb’ }
//   ],
//   vegetable: [{ name: ‘beets’, type: ‘vegetable’ }]
// }

Use native iterators:

const veggies = ingredients.filter((ingredients) => vegetable === ingredients.type)
// => [{ name: ‘beets’, type: ‘vegetable’ }]

Writing classes

Mixins

Mixins are declared as a simple Object containing function properties. This object is then exported out for consumers to import and use to extend the Class or Function prototype as needed.

Defining mixins

Below is an example of a mixin that defines two methods getFullName and getAge which can be “mixed in” or extend an existing Object prototype.

// mixin.js
export default {
  getFullName () {
    return this.fname + this.lname
  },

  getAge () {
    return 2018 - this.birthYear
  }
}
// app.js
import default from './mixin.js'

class SomeClass { ... }

Object.assign(SomeClass.prototype, default) // <-- Mixin

Example

// get_total.js 
export default {
  getTotal: function () { // calculate and return total }
}
// reorder.js
export default {
  reOrder: function (product_id) { // some api request to notify to restock }
}
// yourClassModule.js
import GetTotalMixin from '../mixins/get_total.js'
import ReOrderMixin from '../mixins/reorder.js'

const product = Backbone.Model({ amount: 2400 })

class Cart extends Backbone.Collection
Object.assign(Cart.prototype, GetTotalMixin) // <-- Mixin
const cart = new Cart([product])

class Inventory extends Backbone.Collection
Object.assign(Inventory.prototype, GetTotalMixin, ReOrderMixin) // <-- Mixin
const inventory = new Inventory([product])