Matt Marum

Prepare for the Backbone.js upgrade in Sugar 7.8

Blog Post created by Matt Marum Employee on Jul 13, 2016



 

Upgrading our Backbone

 

Have you done some Sidecar programming lately? Then you have been using Backbone. Backbone is the err... backbone of Sidecar. It provides all the base MVC classes which are extended by Sidecar to create the Sugar 7 UI. For example, all Sidecar controllers (Views, Layouts, Fields) extend from the Backbone View class.

 

Ultimately, a solid background in Backbone programming will turn you into a Sidecar wizard in no time.

 



 

But if you are a Backbone aficionado then you might have noticed that Sugar 7.7 and earlier versions uses an old version of Backbone (specifically Backbone 0.9.10). We have been missing out on bug fixes and miscellaneous feature improvements. So for Sugar 7.8 we will be moving to Backbone 1.2.3.  Since Backbone.js has a hard dependency on Underscore.js, we will also upgrade the Underscore library from 1.4.4 to 1.8.3.All Sugar Developers should check out the Backbone changelog and the Underscore changelog to see if their code customizations could be impacted by this long overdue library upgrade.

 

Read on to learn more about some adjustments you need to make to your Sugar code.

 

Changes to Sidecar controller DOM event delegation

 

In Backbone 1.2.0, there was an important change that affects how DOM events are delegated in Backbone Views. Emphasis mine.

Views now always delegate their events in setElement. You can no longer modify the events hash or your view's el property in initialize.

 

This means that modifying this.events in the initialize() function of a Backbone View to register DOM event handlers is no longer supported by Backbone. This is because DOM events set in the events hash (this.events) are delegated before initialize() is even called. However, since this was a common practice for Sugar code and customizations we have altered the default Backbone behavior within Sidecar for the Sugar 7.8 release.Sugar will continue to continue to call delegateEvents() during initialize() in Sugar 7.8 for compatibility but the practice is deprecated since Backbone no longer supports it. Sidecar controllers that modify this.events during initialize() will continue to work until this workaround is removed in an upcoming Sugar release.

 

Here is a simple example of a Sidecar view that uses this deprecated practice.

 

A simple example

./custom/clients/base/views/example/example.js

 

[code language="javascript"]

 

/** This approach is deprecated in Sugar 7.8 release  **/

 

({

 

    events: {...},

 

    initialize: function(options) {

 

        if (...) {

 

            this.events['click'] = function(e){...};

 

        }

 

        this._super('initialize', [options]);

 

    },

 

    ...

 

})

 

 

 

This will not work in a future Sugar release.

 

A Record View use case

 

Let's examine a common Sidecar customization use case.

 

Say we need to extend the out of the box Sugar RecordView controller to launch a wizard user interface on a mouse click.

 

We plan to listen for a special DOM click event but we also do not want to break any existing Record view event listeners.

 

To implement this feature, Sugar Developers commonly used code such as the following:./custom/..../clients/base/views/record/record.js

 

[code language="javascript"]

 

/** This approach is deprecated in Sugar 7.8 release  **/

 

({

 

    extendsFrom: 'RecordView',

 

    initialize: function(options) {

 

        // Extending the RecordView events in a deprecated fashion

 

        this.events = _.extend({}, this.events, {

 

            'click .wizard': '_launchWizard'

 

        });

 

        this._super('initialize', [options]);

 

    },

 

    _launchWizard: function(){

 

      // ... do something ...

 

    }

 

})

 

 

 

To reiterate, the examples above will no longer work in a future Sugar release. Sugar Developers should update any similar code to use alternative approaches listed below.

 

Event Delegation Alternatives

 

Here are some alternatives that you can use for delegating DOM events with your Sidecar controllers.

 

Statically define your events hash

 

Define your events all within the events object hash. Note that when extending controllers that this would override any events defined on a parent controller.

 

[code language="javascript"]

 

({

 

    events: {

 

        'mousedown .title': 'edit',

 

        'click .button': 'save',

 

        'click .open': function(e) { ... }

 

    }

 

    ...

 

})

 

 

 

Use a callback function for dynamic events

 

You can assign the events variable of Backbone controllers a function instead of an object hash.  This function will then be used to determine the event hash used when delegateEvents() is called by Backbone.

 

[code language="javascript"]

 

({

 

    events: function(){

 

        if (...) {

 

            return {'click .one': function(){...}};

 

        } else {

 

            return {'click .two': function(){...}};

 

        }

 

    }

 

    ...

 

})

 

 

 

If you must, then call delegateEvents() function directly

 

You can optionally pass an alternative event hash using this.delegateEvents(events). When unspecified, this.events is used by default. The delegateEvents() function removes any previously delegated events at same time so it is safe to call multiple times.

 

[code language="javascript"]

 

({

 

    extendsFrom: 'RecordView',

 

    oneEvents: {...},

 

    twoEvents: {...},

 

    isOne = true,

 

    toggleOneTwo: function(){

 

        if (this.isOne) {

 

            this.delegateEvents(_.extend({}, this.events, this.oneEvents));

 

        } else {

 

            this.delegateEvents(_.extend({}, this.events, this.twoEvents));

 

        }

 

        this.isOne = !this.isOne;

 

    }

 

    ...

 

})

 

 

 

Other important Backbone changes

  • Backbone.js no longer attaches options to the Backbone.View instance by default (as of 1.2.0). Sugar Developers should know we plan to deprecate this.options on Sidecar controllers in a future Sugar release.
  • This upgrade may also break customizations of Sidecar routes that expect URL parameters to be concatenated to the first argument passed to the Backbone router's callback. Sugar Developers should change the signature of their router callbacks to specify the additional argument for URL parameters.

 

For example:Old way:

 

[code language="javascript"]

 

// in a sugar7.js equivalent file

 

{

 

    name: 'search',

 

    route: 'search(/)(:termAndParams)',

 

    callback: function(termAndParams) {

 

        // termAndParams => "?module=Accounts&foo=bar"

 

        // commence ugly URL parsing...

 

    }

 

}

 

New way:

 

[code language="javascript"]

 

// in a sugar7.js equivalent file

 

{

 

    name: 'search',

 

    route: 'search(/)(:term)',

 

    callback: function(term, urlParams) {

 

        // term => "this is a search term"

 

        // urlParams => "module=Accounts&foo=bar"

 

        // no more ugly URL parsing!

 

    }

 

}

 

 

  • Potential Breaking Change: Sugar customizations that override the sync method on any instances of Backbone.Model and Backbone.Collection should should be updated to match Backbone's new signatures for the internal success/error callbacks for Model#fetch, Model#destroy, Model#save, and Collection#fetch methods.

 

For example:Old way, Backbone < 0.9.10:

 

[code language="javascript"]

 

    // in a custom sidecar controller:

 

    sync: function(method, model, options) {

 

        // custom sync method

 

        ...

 

    options.success = _.bind(function(model, data, options) {

 

        this.collection.reset(model, data, options);

 

    }, this);

 

// in Backbone.js's Collection#fetch method...

 

    fetch: function(options) {

 

        options = options ? _.clone(options) : {};

 

        if (options.parse === void 0) options.parse = true;

 

        var success = options.success;

 

        // *** Note: 'collection', 'resp', 'options' are passed ***

 

        options.success = function(collection, resp, options) {

 

            var method = options.update ? 'update' : 'reset';

 

            collection[method](resp, options);

 

            if (success) success(collection, resp, options);

 

        };

 

        return this.sync('read', this, options);

 

    },

 

New way, Backbone > 1.x:

 

[code language="javascript"]

 

    // in a custom sidecar controller:

 

    sync: function(method, model, options) {

 

        // custom sync method

 

        ...

 

    // *** Only data should now be passed here ***

 

    options.success = _.bind(function(data) {

 

        this.collection.reset(data);

 

    }, this);

 

    // in Backbone.js's Collection#fetch method...

 

    fetch: function(options) {

 

        options = _.extend({parse: true}, options);

 

        var success = options.success;

 

        var collection = this;

 

        // Note: the success callback is now only passed 'resp'

 

        options.success = function(resp) {

 

            var method = options.reset ? 'reset' : 'set';

 

            collection[method](resp, options);

 

            if (success) success.call(options.context, collection, resp, options);

 

            collection.trigger('sync', collection, resp, options);

 

        };

 

        wrapError(this, options);

 

        return this.sync('read', this, options);

 

    },

 

 

  • Potential Breaking Change: The method signature for error callbacks for Model#fetch, Model#destroy, Model#save, and Collection#fetch has changed. The Backbone.Model or Backbone.Collection is passed as the first parameter and the second parameter is now the HttpError XHR object.

 

For example:Old way:

 

[code language="javascript"]

 

    this.model.save({}, {

 

        error: function(error) {

 

            ...

 

        }

 

    });

 

New Way:

 

[code language="javascript"]

 

    this.model.save({}, {

 

        error: (model, error) {

 

            ...

 

        }

 

    });

 

 

  • Potential Breaking Change: Sugar customizations that set the id property directly on a Backbone.Model will not work with Backbone Collections. Sugar Developers should always use Backbone's internal APIs/methods, meaning they should be using model.set('id', ...) instead.

 

For example:

 

[code language="javascript"]

 

var model = app.data.createBean('Accounts', {id: 'foo'});

 

var collection = app.data.createBeanCollection('Accounts');

 

collection.add(model);

 

model.id = 'bar';

 

console.log(collection.get('bar'));

 

 

Output >> undefined

 

Use model.set('id', 'bar'); instead:

 

[code language="javascript"]

 

var model = app.data.createBean('Accounts', {id: 'foo'});

 

var collection = app.data.createBeanCollection('Accounts');

 

collection.add(model);

 

model.set('id', 'bar');

 

console.log(collection.get('bar'));

 

 

Output >> model

Outcomes