Backbone Components

Render 1477041 640 600x338

Build once, use anywhere. Components are highly reusable and composable discrete pieces of functionality. It typically requires a data model and follows patterns that other modern frameworks use so that our fundamental way of thinking doesn’t change when we do ultimately upgrade to a newer view framework.

The following are guidelines to use when developing components in BackboneJS. This is in addition to general JavaScript and Backbone view conventions.

Highly reactive, loosely coupled

Components should reflect the visual state of a model. UI changes should always be driven by changes to attributes in the model.

function updateState () {
  // Update some UI
}

function updateAnotherState () {
  // Update some UI
}

class Component extends Backbone.View {
  initialize (options = {}) {
    this.listenTo(this.model, 'change:attribute1', () => {
      updateState()
    })

    this.listenTo(this.model, 'change:attribute2', () => {
      updateAnotherState()
    })
  }

  render () {
    updateState()
    updateAnotherState()

    return this
  }
}

To keep the event bindings easy to follow, don’t chain methods to sequence functionality. Instead, always bind to separate attributes instead so that UI changes can be easily traced back to the event.

// BAD
// This is super brittle because a single attribute change can cause 
// a cascade of UI changes unknowingly
function updateState () {
  updateAnotherState()
}

function updateAnotherState () {
  updateThirdState()
}

class Component extends Backbone.View {
  initialize (options = {}) {
    this.listenTo(this.model, 'change', () => {
      updateState()
    })
  }

  render () {
    updateState()

    return this
  }
}

Model driven

All UI state is bound to model data. There are 2 types of models that should be used.

Resource model

These are Backbone models that are bound directly to a resource on the backend. Calling fetch() on them will perform a GET request and update the attributes and a save() will perform a POST request to save a new model or a PUT to update a resource on the server.

Use this model if your view maps directly to an attribute without needing to do any computation.

State model

When your view needs to update its state based on a computed value from multiple resource model attributes, use a state model pattern. This allows you to unit test the crap out of this state independent of the view to assert that the computations are correct.

The attributes of the state model should represent the states of the view.

class ComponentState extends Backbone.Model {

  // The default states of this component
  get defaults() {
    return {
      showTitle: true,
      showBody: true,
      showFooter: true
    }
  }

  initialize (attrs = {}, options = {}) {
    this.model = options.model

    this.listenTo(this.model, 'change:attribute1', this.computeState)
    this.listenTo(this.model, 'change:attribute2', this.computeState)
  }

  // Compute a state based on multiple concerns
  computeState () {
    if (this.model.get('attribute1') && !this.model.get('attribute2')) {
      this.set({ showTitle: true })
    }
  }
}

class Component extends Backbone.View {
  initialize (options = {}) {
    // Instantiate a new state model
    this.state = new ComponentState({}, {model: this.model})

    // This behavior is now easily testable
    this.listenTo(this.state, 'change:showTitle', this.toggleTitle)
  }
}

Public API

Methods

Always think critically when adding a public method to a component. Since the component is model driven, public methods should only be needed to programmatically toggle a state change. The advantages of this is that we don’t need to incur a major semantic version update when changing the internals since no public interfaces have changed.

// Example of a valid public interface
class Dropdown extends Backbone.View {
  ...

  // Allow a consumer to programmatically select an item
  selectItem (item) {

  }
}

Events

Events allow a consumer to listen to an action that has occurred. This is purely for 1 way communication. You should only be bubbling events out, never listening to an event to be triggered on itself.

class Dropdown extends Backbone.View {
  initialize () {
    // BAD
    // The consumer should call a focus() method
    // instead of triggering an event
    this.on("focus", ...)
  }

  selectItem (item) {
    ...

    // GOOD
    // Notify listeners that an item has been selected
    this.trigger("select", item)
  }
}

Supports skeleton state

Skeleton state allows us to render components with a resource model that hasn’t been fetched yet. This allows us to avoid chaining promises to render a view only after a model has been fetched.


// BAD
// The consumer has to decide how to inform the user that
// the data is currently being loaded ala spinner or something else
this.model = new ResourceModel()
this.model
  .fetch()
  .then(() => {
    this.view = new Component({model: this.model})
  })

A component should be able to render itself with an appropriate pending state with placeholders where actual data should be when it detects that the model it is given does not have the appropriate data. Typically checking that a model has an id is sufficient enough to know whether the model has been hydrated or not.

// GOOD
this.model = new ResourceModel()
this.model.fetch()

this.view = new Component({model: this.model})
$('body').append(this.view.render().$el)

This simplifies what the consumer of this component has to be concerned and makes our app appear more responsive.

Subcomponent option passing

Often times components are compositions of other components. If you need to allow configuring the subcomponents, pass the options for the subcomponent as a separate attribute.

class Component extends Backbone.View {
  initialize (options = {}) {
    // Always set defaults
    Util.deepDefaults(options, {
      option1: 'val2',
      option2: 'val2',
      subcomponent: {
        option1: 'val2',
        option2: 'val2'
      }
    }

    // Pass in the subcomponent options
    this.subcomponent = new SubComponent(options.subcomponent)
  }
}

// Options with subcomponent options
options = {
  option1: "val",
  option2: "val",
  subcomponent: {
    option1: "val",
    option2: "val"
  }

this.component = new Component(options)

Garbage collection

Remember to always remove sub views so that events are all properly unbound, otherwise you may see phantom behaviors due to events that are still bound.

class Component extends Backbone.View {
  ...

  remove() {
    this.subComponent1.remove()
    this.subComponent2.remove()

    super.remove()
  }
}

DOM Event scoping

Only in extremely rare situations would you need to listen to document or window events. Always try to architect around it so that you are listening to a local dom event instead so that the component doesn’t introduce adverse behavior that may affect the consumer unknowingly.

class Component extends Backbone.View {
  initialize () {
    // BAD
    window.addEventListener(...)
  }

  remove () {
    // If you must, don't forget to remove the event listener
    window.removeEventListener(...)

    super.remove()
  }
}

BEM

Classes internal to the component should be immutable so that styles do not break.

class Component extends Backbone.View {
  get className() {
    // Root block class
    return 'component-my-component'
  }

  initialize () {
    ...
    this.subcomponent = new Component()

    // Subcomponents retain their block class to preserve it's own css
    // but can be referenced by an element class
    this.subcomponent.$el.addClass('component-my-component__my-element')
  }
}