Prepare for the Backbone.js upgrade in Sugar 7.8



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
Anonymous
  • Comment originally made by Matthew Marum.

    We use Backbone 0.9.10 in Sugar 7.7 and earlier versions so you can refer to that API documentation.

    https://cdn.rawgit.com/jashkenas/backbone/0.9.10/index.html

  • Comment originally made by Mehul.

    Hi Matt

    is there having any documentation of backbone used in sugar 7.7 and above with example .. events,model,collection,router,sync,view ..

  • Comment originally made by Matthew Marum.

    Yes, this is basically what we've done in Sidecar as a work around. When initialized is called then Sidecar will call delegateEvents() again. This does mean that events are being delegated twice which we don't want to continue to do in the future.
  • Comment originally made by Hector.

    Thanks Matt.

    You make a good point that preserving a customized view's event hash might not always be practical. For the delegateEvents() call you mentioned. Would it be safe to call this in the constructor then? In that case it would be a small change to any existing code that currently extends the events hash in the initialize method.

    Thanks again.

  • Comment originally made by Matthew Marum.

    Hi Hector,

    You are right that if you want to extend instead of overriding events then I think it is easiest to use delegateEvents() directly instead of manipulating the events hash and relying on the default Backbone behavior work for you. In general, the events hash is tightly coupled to the HTML DOM that you are using. So if you've made any customizations to the View's Handlebars template, or used jQuery to manipulate DOM for a View, then extending the parent events is not going to be upgrade safe. So we are emphasizing other techniques.

    The delegateEvents() example in the post above does extend the parent controller's events.

    For example,

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

    The call to _.extend() combines the parent controller events and some extra events into a new object. This object is passed directly to delegateEvents().