UNPKG

backbone-proxy_

Version:
337 lines (271 loc) 13.3 kB
// Backbone Proxy v0.1.1 // // https://github.com/biril/backbone-proxy // Licensed and freely distributed under the MIT License // Copyright (c) 2014 Alex Lambiris /*globals exports, define, require, _, Backbone */ // Detect env & export module // -------------------------- (function (root, createModule) { 'use strict'; // A global `define` method with an `amd` property signifies the presence of an AMD loader // (e.g. require.js, curl.js) if (typeof define === 'function' && define.amd) { return define(['underscore', 'backbone', 'exports'], function (_, Backbone, exports) { return createModule(exports, _, Backbone); }); } // A global `exports` object signifies CommonJS-like enviroments that support `module.exports`, // e.g. Node if (typeof exports === 'object') { return createModule(exports, require('underscore'), require('backbone')); } // Otherwise we assume running in a browser - no AMD loader: // Save a reference to previous value of `BackboneProxy` before overwriting it - so that it can // be restored on `noConflict` var previousBackboneProxy = root.BackboneProxy; // Create the BackboneProxy module, attaching `BackboneProxy` to global scope createModule(root.BackboneProxy = {}, _, Backbone); // And add the `noConflict` method. This sets the `BackboneProxy` _global_ to to its previous // value (_once_), returning a reference to `BackboneProxy` (_always_) root.BackboneProxy.noConflict = function () { var BackboneProxy = root.BackboneProxy; root.BackboneProxy = previousBackboneProxy; return (BackboneProxy.noConflict = function () { return BackboneProxy; }).call(); }; // Create module // ------------- }(this, function (BackboneProxy, _, Backbone) { 'use strict'; var // Model built-in events. Note that 'all' is not considered to be one of them builtInEventNames = [ 'add', 'remove', 'reset', 'change', 'destroy', 'request', 'sync', 'error', 'invalid' ], // Names of 'event API' (`Backbone.Event`) methods eventApiMethodNames = [ 'on', 'off', 'trigger', 'once', 'listenTo', 'stopListening', 'listenToOnce' ], // ### SubscriptionCollection // A collection of subscriptions, where each is a `<event, callback, context, proxyCallback>` // 4-tuple. Every `EventEngine` instance maintains a `SubscriptionCollection` instance to // keep track of registered callbacks and their mapping to _proxy_-callbacks // SubscriptionCollection = (function () { function SubscriptionCollection () { this._items = []; } SubscriptionCollection.prototype = { _markToKeep: function (propName, propValue) { if (!propValue) { return; } for (var i = this._items.length - 1; i >= 0; --i) { this._items[i].keep = this._items[i][propName] !== propValue; } }, // Store subscription of given attributes store: function (event, callback, context, proxyCallback) { this._items.push({ event: event, callback: callback, context: context, proxyCallback: proxyCallback }); }, // Unstore subscriptions that match given `event` / `callback` / `context`. None of the // params denote mandatory arguments and those given will be used to _filter_ the // subscriptions to be removed. Invoking the method without any arguments will remove // _all_ subscriptions. This behaviour is in line with that of `Backbone.Model#off` unstore: function (event, callback, context) { var itemsUnstored = [], itemsKept = []; _(this._items).each(function (item) { item.keep = false; }); this._markToKeep('event', event); this._markToKeep('callback', callback); this._markToKeep('context', context); for (var i = this._items.length - 1; i >= 0; --i) { (this._items[i].keep ? itemsKept : itemsUnstored).push(this._items[i]); } this._items = itemsKept; return itemsUnstored; } }; return SubscriptionCollection; }()), // ### Event Engine // A module that builds on top of `Backbone.Events` to support invoking registered callbacks in // response to events triggered on a given `proxied`, on behalf of a given `proxy`. Every // `ModelProxyProto` instance (i.e. every `Proxy.prototype`) contains an Event Engine to // facilitate forwarding events from proxied to proxy // EventEngine = (function () { function EventEngine(proxy, proxied) { this._proxy = proxy; this._proxied = proxied; this._subscriptions = new SubscriptionCollection(); this._isListeningToProxied = false; this._markerContext = {}; } EventEngine.prototype = _({}).extend(Backbone.Events, { // Get a value indicating whether there's currently _any_ listeners registered on the engine _hasEvents: function () { for (var key in this._events) { if (this._events.hasOwnProperty(key)) { return true; } } return false; }, // Listen or stop listening for the 'all' event on `proxied` depending on whether there's // currently _any_ listeners registered on the event engine (equivalently, on the proxy) _manageSubscriptionToProxied: function () { var hasEvents = this._hasEvents(); if (this._isListeningToProxied !== hasEvents) { if (hasEvents) { this._proxied.on('all', _.bind(function () { Backbone.Events.trigger.apply(this, arguments); }, this), this._markerContext); } else { this._proxied.off(null, null, this._markerContext); } this._isListeningToProxied = hasEvents; } }, // For any given subscription (defined by `event` / `callback` / `context`), create a // _proxy-callback_ - a callback that will be invoked by by Backbone's Event module in // place of the original caller-provided callback. Depending on the type of the event the // given subscription refers to, the proxy-callback will take care of appropriately // setting `model` arguments that may be present (when dealing with model built-in events) // as well as the context (if it's not explicitly set by the caller) _createProxyCallback: function (event, callback, context) { // The context is the one that's already specified _or_ set to the proxy context || (context = this._proxy); // If the subscription is for a model built-in event, the proxy-callback will have to // replace the model argument with the proxy (as Backbone's Event module will set it to // the proxied when invoking the callback). Same goes for the context if (_(builtInEventNames).contains(event) || !event.indexOf('change:')) { return function () { arguments[0] = this._proxy; callback.apply(context, arguments); }; } // So the subscription doesn't concern a built-in event. If additionally it's not for the // 'all' event, then we're dealing with a user-defined event. In this case we only have // to make sure that the callback is invoked the appropriate context if (event !== 'all') { return function () { callback.apply(context, arguments); }; } // So The subscription is for the 'all' event: The given callback will run for built-in // events as well as arbitrary, user-defined events. We'll have to check the type of the // event at callback-invocation-time and treat it as one of the two cases. (And yes, if // client-code decides to `trigger()` an event which is named like a built-in but // doesn't carry the expected parameters, things will go sideways) return function (event) { if (_(builtInEventNames).contains(event) || !event.indexOf('change:')) { arguments[1] = this._proxy; } callback.apply(context, arguments); }; }, on: function (event, callback, context) { var proxyCallback = this._createProxyCallback(event, callback, context); this._subscriptions.store(event, callback, context, proxyCallback); Backbone.Events.on.call(this, event, proxyCallback, context); this._manageSubscriptionToProxied(); }, off: function (event, callback, context) { var matchingSubscriptions = this._subscriptions.unstore(event, callback, context); _(matchingSubscriptions).each(function (subscription) { Backbone.Events.off.call(this, subscription.event, subscription.proxyCallback, subscription.context); }, this); this._manageSubscriptionToProxied(); } }); return EventEngine; }()), // ### Model Proxy Prototype // The prototype of Proxy, _per given `proxied` model_. Overrides certain `Backbone.Model` // methods ultimately delegating to the implementations of the given `proxied` // Create a prototype for Proxy, given a `proxied` model createModelProxyProtoForProxied = function (proxied) { function ModelProxyProto() { var createPersistenceMethod; // #### Prototype's event API methods - they all delegate to the internal Event Engine // e.g. `on`, `off` // _(eventApiMethodNames).each(function (methodName) { this[methodName] = function () { this._eventEngine[methodName].apply(this._eventEngine, arguments); return this; }; }, this); // #### Prototype's methods that should always be invoked with `this` set to `proxied` // e.g. `set` // This ensures that // // * all other model methods in the call-graph of the invoked method (e.g. `proxied.set`) // are also invoked with `this` set to `proxied`. // * all properties which may be set on the model as part of the invoked method's code // path are set on `proxied` // // For example, this ensures that the `model` parameter made available to event listeners // attached to `proxied` will be `proxied` - not `proxy` (which would otherwise be the // case). In terms of set properties, consider `validationError` which also needs to be // set on `proxied` - not `proxy`. // Generally, we just need to forward to `proxied` _(['isNew', 'url']).each(function (methodName) { this[methodName] = function () { return proxied[methodName](); }; }, this); // Specifically, for the case of `set`, we need to replace the returned reference with // `this`. To get proper chaining this.set = function () { return proxied.set.apply(proxied, arguments) ? this : false; }; // #### Prototype's persistence methods // i.e. `fetch`, `save` & `destroy` // Create persistence method of `methodName`, where `isWithAttributes` indicates whether // the method excepts attributes (`save`) or just options (`fetch`, `destroy`) createPersistenceMethod = function (methodName, isWithAttributes) { return function () { var opts, success, error, args; opts = arguments[isWithAttributes ? 1 : 0]; opts = opts ? _.clone(opts) : {}; success = opts.success; error = opts.error; success && (opts.success = _.bind(function (model, response, opts) { success(this, response, opts); }, this)); error && (opts.error = _.bind(function (model, response, opts) { error(this, response, opts); }, this)); args = isWithAttributes ? [arguments[0], opts] : [opts]; return proxied[methodName].apply(proxied, args); }; }; this.fetch = createPersistenceMethod('fetch', false); this.destroy = createPersistenceMethod('destroy', false); this.save = createPersistenceMethod('save', true); this._proxied = proxied; } ModelProxyProto.prototype = proxied; return ModelProxyProto; }; // ### BackboneProxy // The BackboneProxy module. Features a single `extend` method by means of which a proxy 'class' // `Proxy` may be created. E.g `UserProxy = BackboneProxy.extend(user)`. Proxy classes may be // instantiated into proxies, e.g. `user = new UserProxy()` // Return the BackboneProxy module return _(BackboneProxy).extend({ extend: function (proxied) { var ctor, ModelProxyProto; ctor = function ModelProxy() { this._eventEngine = new EventEngine(this, proxied); }; ModelProxyProto = createModelProxyProtoForProxied(proxied); ctor.prototype = new ModelProxyProto(); return ctor; } }); }));