above: Twitter Flight Framework Logo

I recently did a blog post on RetailMeNot's blog. Re-printed below for posterity.


Mixin’ It Up With FlightJS

As RetailMeNot started to develop more rich interactions for our site, we found ourselves needing to include the same functionality and patterns in different components or objects without duplicating the code, lest we end up in the wild west of inheritance. Enter Twitter’s FlightJS framework.

FlightJS is something that we already make use of a great deal at RetailMeNot. One of the key features of FlightJS is the .mixin() pattern. Using mixin() enables us to associate a piece of functionality with any component, without having to worry about how said functionality fits into some class hierarchy.

Guidelines

At RetailMeNot, we’ve been using FlightJS since February 2014, and what we consider "best practices" has been continually evolving as the framework — and our understanding of it has matured. We currently use the following set of guidelines while working on FlightJS code:

  • Separate your concerns into single-purposed functions. This helps with unit testing and AB testing, as it becomes easy to override small bits of functionality. Such concerns may include: dom updating, analytics, ajax requests, internal state management
  • Keep functions small, if you find yourself writing a function that’s more than 9–10 lines of code, you’re probably doing too much.
  • Look for opportunities to mix functionality into a component. Use component.mixin, and the advice API. This helps with not forking huge js files, and with identifying common functionality between components.
  • Look for repeated code, I guarantee there is almost always a way to not repeat it.
  • Avoid calling one function from another when possible, instead use event communication (internal or external), and the advice API.

Missteps

When we first moved from FlightJS, we blindly ported most of our JS code into flight components. In hindsight this was probably unnecessary, given that Flight plays well with non-Flight JS. This was a mistake that we are still paying for.

The beauty of FlightJS is that it encourages and enforces separation of concerns using a combination of functional mixins and aspect oriented programming paradigms. Unfortunately, the library doesn’t stop you from putting too much code in any single file/function.

Our older JS code tended towards putting too much functionality in our functions, which our FlightJS components inherited when we ported over to FlightJS. Because of this, we started out of the gate with FlightJS by breaking many of the above guidelines. This made it difficult to leverage other aspects of the library, such as the advice API or component.mixin. As a result, our initially ported code is no easier to maintain then if we had never started using FlightJS.

We are refactoring our older FlightJS components to make them better-aligned with our guidelines. This will ensure the maintainability and testability of our components moving forward.

Lack of component.mixin prior to FlightJS 1.2.0

Before upgrading to FlightJS version 1.2.0, which added component.mixin(), we had to include all of a component’s desired functionality when we returned the result of calling component(). But what if we needed a component that is in all ways identical to the original component, except that it fires different analytics upon the user taking some action? Well, prior to the flight 1.2 upgrade, we were forced to split the bulk of out component’s implementation into a mixin of it’s own, for later inclusion in a particular component. This limitation combined with the fact that our functions were not small and single-purposed, forced us into some strange paradigms. Let’s examine them:

Code for one of our components might typically look something like this:

component.js

define([
    'flight/lib/component'
], function(
    component
){
    return component( componentCode );
    function componentCode() {
        // Define default attributes for the component
        this.attributes({ /*...*/ });
        // Once the user has successfully completed an action, update the DOM accordingly
        this.updateDomOnSuccess = function () { /*...*/ };
        // Fire analytics indicating that the user has successfully completed an action
        this.doSuccessAnalytics = function () { /*...*/ };
        // Set up listeners to make the component do things
        this.after( 'initialize', function () { /*...*/ });
    }
});

But what if we want to start using this component in two different places on the site, and in one context we want to run a different doSuccessAnalytics() function?

Prior to the FlightJS 1.2 upgrade, we would do it this way:

The "Override Functions in the attachTo" Approach

component.js, with abstracted analytics

define([
    'flight/lib/component'
], function(
    component
){
    return component( componentCode );
    function componentCode() {
        // Define default attributes for the component
        this.attributes({
            // default analytics implementation for component
            successAnalyticsImplementation**:** function () { /*...*/ }
        });
        // Once the user has successfully completed an action, update the DOM accordingly
        this.updateDomOnSuccess = function () { /*...*/ };
        // Fire analytics indicating that the user has successfully completed an action
        this.doSuccessAnalytics = function () {
            if (this.attr.successAnalyticsImplementation){
                this.attr.successAnalyticsImplementation();
            }
        };
        // Set up listeners to make the component do things
        this.after( 'initialize', function () { /*...*/ });
    }
});

With the above set of changes, we now have the ability to override the analytics function by passing a different implementation in via the .attachTo call for our component’s different context. But is this much better? All we have done is push concern of analytics for a component up to the page level, where these components are being attached. This introduces the potential issue of failing to fire analytics when we add our components to new pages (by omitting an analytics implementation).

To rectify the aforementioned issue, we shifted to the following pattern:

The "Move the Bulk of your Component’s Implementation Into a Mixin" Approach

mixins/with-component-core.js

define([
    'flight/lib/component'
], function(
    component
){
    return componentCode;
    function componentCode() {
        // Define default attributes for the component
        this.attributes({ /*...*/ });
        // Once the user has successfully completed an action, update the DOM accordingly
        this.updateDomOnSuccess = function () { /*...*/ };
        // Fire analytics indicating that the user has successfully completed an action
        this.doSuccessAnalytics = function () {
            // default analytics implementation for component
        };
        // Set up listeners to make the component do things
        this.after( 'initialize', function () { /*...*/ });
    }
});

This pulls the bulk our components implementation into a mixin, to be shared between different implementations of the component as follows:

component.js

define([
    'mixins/with-component-core'
    'flight/lib/component'
], function(
    componentCore
    component
){
    return component( componentCore );
});

tests/component.js — with variation in analytics

define([
    'mixins/with-component-core'
    'flight/lib/component'
], function(
    componentCore
    component
){
    return component( componentCore, withVariationInAnalytics );
    function withVariationInAnalytics() {
        // Fire analytics indicating that the user has successfully completed an action
        this.doSuccessAnalytics = function () {
            // overriding analytics implementation for our test
        }
    };
});

Following this approach, we can now provide a default analytics implementation, override it at the component level, and not have to worry about remembering to include it when we add the component to a new context. However, we were forced to move the majority of our component’s implementation into a mixin, one that can essentially stand alone as a component, which totally defeats the purpose of the mixin pattern.

Enter FlightJS 1.2, which introduced component.mixin(). We can now solve this problem as follows:

The "Flight 1.2’s component.mixin Function is the Answer to All of Our Problems" Approach

component.js

define([
    'flight/lib/component'
], function(
    component
){
    return component( componentCode );
    function componentCode() {
        // Define default attributes for the component
        this.attributes({ /*...*/ });
        // Once the user has successfully completed an action, update the DOM accordingly
        this.updateDomOnSuccess = function () { /*...*/ };
        // Fire analytics indicating that the user has successfully completed an action
        this.doSuccessAnalytics = function () {
            // default analytics implementation for component
        };
        // Set up listeners to make the component do things
        this.after( 'initialize', function () { /*...*/ });
    }
});

tests/component.js — with variation in analytics

define([
    'component' // path to component.js
], function(
    originalComponent
){
    return originalComponent.mixin( analyticsOverride );
    function analyticsOverride() {
        // Fire analytics indicating that the user has successfully completed an action
        this.doSuccessAnalytics = function () {
        // overriding analytics implementation for our test
        }
    };
});

This new pattern solves the aforementioned issues, and comes with the following benefits:

  • We can implement the component as it is intended to behave in the majority of cases.
  • We can provide variations of the existing component without having to touch our original component.
  • As a bonus, it is much more semantically clear as to what we are trying to achieve for the purpose of our test.

By following the guidelines above, and leveraging the power of FlightJS and its component.mixin() function, it becomes easy to write testable client-side JavaScript that encapsulates functionality in a clear, concise manner. As a result the reading, reasoning about, and extending the code becomes much more straight-forward because our component definitions are more expressive and easy to follow. We hope you find the mixin() pattern as valuable as we do.

Originally published at www.retailmenot.com on November 18, 2014. Now available at engineering.ziffmedia.com. Forever available on Luke's Blog