Prepare for the Backbone.js Upgrade in Sugar 7.8

by Sarah Friedlander Garcia on July 13, 2016

This content originally appeared on the SugarCRM Developer Blog and was authored by Matthew Marum.
 


 

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 have you up and running as a Sidecar wizard in no time.

All Sidecar controllers, such as the Record View controller, extends from the Backbone View class

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.

What this means 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 within Sidecar 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

/** 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 we could use custom code such as the following:

./custom/…./clients/base/views/record/record.js

/** This approach is deprecated in Sugar 7.8 release  **/
({
    extendsFrom: 'RecordView',
    initialize: function(options) {
        // Extending the RecordView events
        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.

({
    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 can be used to returns an events hash. This function will then be used to determine events hash, by default, when delegateEvents() is called by Backbone.

({
    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 event hash to this.delegateEvents() otherwise it will use this.events by default. delegateEvents() removes any previously delegated events at same time.

({
    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:

// 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:

// 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!
    }
}
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'));
  • 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, Collection#fetch methods.

For example:

Old way, Backbone < 0.9.10:

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

// 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: 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:

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:

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

Find similar articles in these categories:

PRODUCT: SugarCRM

Sarah Friedlander Garcia
Director of Marketing at UpCurve Cloud
More From This Author »