0.1.1 • Published 10 years ago

backbone-proxy_ v0.1.1

Weekly downloads
4
License
MIT
Repository
github
Last release
10 years ago

Backbone Proxy

Build Status NPM version Bower version

Model proxies for Backbone. Notably useful in applications where sharing a single model instance among many components (e.g. views) with different concerns is a common pattern. In such cases, the need for multiple components to reference the same model-encapsulated state prohibits the specialization of model-behaviour. Additionaly it leads to a design where 'all code is created equal', i.e. all components have the exact same priviledge-level in respect to data-access. BackboneProxy faciliates the creation of model-proxies that may be specialized in terms of behaviour while referencing the same, shared state.

For example, you can create:

Proxies that log attribute changes

// Create a UserProxy class. Instances will proxy the given user model
// Note that user is a model _instance_ - not a class
var UserProxy = BackboneProxy.extend(user);

// Instantiate a logging proxy - a proxy that logs all invocations of .set
var userWithLog = new UserProxy();
userWithLog.set = function (key, val) {
  console.log('setting ' + key + ' to ' + val + ' on user');

  // Delegate to UserProxy implementation
  UserProxy.prototype.set.apply(this, arguments);
};

// Will not log
user.set('name', 'Norman');

// Will log 'setting name to Mairy on user'
userWithLog.set('name', 'Mairy');

// Will log 'user name is Mairy'
console.log('user name is ' + user.get('name'));

Readonly proxies

// Create a UserProxy class
var UserProxy = BackboneProxy.extend(user);

// Instantiate a readonly proxy - a proxy that throws on any invocation of .set
var userReadonly = new UserProxy();
userReadonly.set = function () {
  throw 'cannot set attributes on readonly user model';
};

// Attributes cannot be set on userReadonly
// However attribute changes can be listened for
userReadonly.on('change:name', function () {
  alert('user name was set to ' + userReadonly.get('name'));
});

// Will throw
userReadonly.set('name', 'Mairy');

// Will alert 'user name was set to Mairy'
user.set('name', 'Mairy');

Vew-specific proxies

// Create a UserProxy class
var UserProxy = BackboneProxy.extend(user);

// Instantiate a proxy to pass to view1
var userProxy1 = new UserProxy();
userProxy1.set = function (key, val, opts) {

  // Handle both key/value and {key: value} - style arguments
  var attrs;
  if (typeof key === 'object') { attrs = key; opts = val; }
  else { (attrs = {})[key] = val; }

  // Automatically update lastUpdatedBy to 'view1' on every invocation of set
  attrs.lastUpdatedBy = 'view1';

  // Delegate to UserProxy implementation
  UserProxy.prototype.set.call(this, attrs, opts);
};

// Instantiate a proxy to pass to view2. Similar to userProxy1 -
//  will automatically set the lastUpdatedBy attr to 'view2'
var userProxy2 = new UserProxy();
userProxy2.set = function (key, val, opts) {
  var attrs;
  if (typeof key === 'object') { attrs = key; opts = val; }
  else { (attrs = {})[key] = val; }

  attrs.lastUpdatedBy = 'view2';

  UserProxy.prototype.set.call(this, attrs, opts);
};

var view1 = new SomeView({ model: userProxy1 });
var view2 = new SomeOtherView({ model: userProxy2 });

// Will log modifications of user object '..by view1' / '..by view2'
user.on('change', function () {
  console.log('user modified by ' + user.get('lastUpdatedBy'));
});

Set up

Backbone proxy may be used as an exported global, a CommonJS module or an AMD module depending on the current environment:

  • In projects targetting browsers, without an AMD module loader, include backbone-proxy.js after backbone.js:

    ...
    <script type="text/javascript" src="backbone.js"></script>
    <script type="text/javascript" src="backbone-proxy.js"></script>
    ...

    This will export the BackboneProxy global.

  • require when working with CommonJS (e.g. Node). Assuming BackboneProxy is npm installed:

    var BackboneProxy = require('backbone-proxy');
  • Or list as a dependency when working with an AMD loader (e.g. RequireJS):

    // Your module
    define(['backbone-proxy'], function (BackboneProxy) {
      // ...
    });

    Note that the AMD definition of BackboneProxy depends on backbone and underscore so some loader setup will be required. For non-AMD compliant versions of Backbone (< 1.1.1) or Undescore (< 1.6.0), James Burke's amdjs forks may be used instead, along with the necessary paths configuration

    require.config({
      baseUrl: 'myapp/',
      paths: {
        'underscore': 'mylibs/underscore',
        'backbone': 'mylibs/backbone'
      }
    });

    or you may prefer to just shim them.

At the time of this writing, BackboneProxy has only been tested against Backbone 1.2.1

Usage

BackboneProxy exposes a single extend method as the means of creating a Proxy 'class' for any given model (or model proxy):

// Create Proxy class for given model
var Proxy = BackboneProxy.extend(model);

// Instantiate any number of proxies
var someProxy = new Proxy();
var someOtherProxy = new Proxy();

// Yes, you can proxy a proxy
var ProxyProxy = BackboneProxy.extend(someProxy);
var someProxyProxy = new ProxyProxy();

For any given proxied/proxy models that have a proxied-to-proxy relationship, the following apply:

Any attribute set on proxied will be set on proxy and vice versa (the same applies for unset and clear):

proxied.set({ name: 'Betty' });
console.log(proxy.get('name')); // Will log 'Betty'

proxy.set({ name: 'Charles' });
console.log(proxied.get('name')); // Will log 'Charles'

Built-in model events (add, remove, reset, change, destroy, request, sync, error, invalid) triggered in response to actions performed on proxied will also be triggered on proxy. And vice versa:

proxy.on('change:name', function (model) {
  console.log('name set to ' + model.get('name'))
});
proxied.set({ name: 'Betty' }); // Will log 'name set to Betty'

proxied.on('sync', function () {
  console.log('model synced');
});
proxy.fetch(); // Will log 'model synced'

User-defined events triggered on proxied will also be triggered on proxy. The opposite is not true:

proxied.on('boo', function () {
  console.log('a scare on proxied');
});
proxy.on('boo', function () {
  console.log('a scare on proxy');
});

proxied.trigger('boo'); // Will log 'a scare on proxied' & 'a scare on proxy'
proxy.trigger('boo'); // Will only log 'a scare on proxy'

Additions and removals of event listeners are, generally speaking, 'scoped' to each model. That is to say, event listeners may be safely added on, and - primarily - removed from the proxied (/proxy) without affecting event listeners on the proxy (/proxied). This holds when removing listeners by callback or context. For example, when removing listeners by callback:

var onModelChange = function (model) {
  console.log('model changed');
};

proxied.on('change', onModelChange);
proxy.on('change', onModelChange);

proxied.set({ name: 'Betty' }); // Will log 'model changed' twice

// Will only remove the listener previously added on proxied
proxied.off(null, onModelChange);

proxied.set({ name: 'Charles' }); // Will log 'model changed' once

Removing listeners by event name works similarly:

proxied.on('change:name', function () {
  console.log('caught on proxied');
});
proxy.on('change:name', function () {
  console.log('caught on proxy');
});

proxied.set({ name: 'Betty' }); // Will log 'caught on proxied' & 'caught on proxy'

// Will only remove the listener previously added on proxied
proxied.off('change:name');

proxied.set({ name: 'Charles' }); // Will only log 'caught on proxy'

However, removing listeners registered for the 'all' event presents a special case as it will interfere with BackboneProxy's internal event-forwarding. Essentially, you should avoid .off('all') for models which are proxied - it will disable event notifications on the proxy:

proxy.on('change:name', function () {
  console.log('changed name');
});
proxy.on('change:age', function () {
  console.log('changed age');
});

// Will log 'changed name' & 'changed age'
proxied.set({
  name: 'Betty',
  age: 29
});

// Bad move
proxied.off('all');

// Will log nothing
proxied.set({
  name: 'Charles',
  age: 31
});

The model argument passed to a listener will always be set to the model to which the listener was added. The same is true for the context (as long as it's not explicitly set):

var onModelChanged = function (model) {
    model.dump(); // Or alternatively, this.dump();
  };

proxied.dump = function () {
  console.log('proxied changed: ' + JSON.stringify(this.changedAttributes()));
};
proxy.dump = function () {
  console.log('proxy changed: ' + JSON.stringify(this.changedAttributes()));
};

proxied.on('change', onModelChanged);
proxy.on('change', onModelChanged);

proxied.set({ name: 'Betty' }); // Will log 'proxied changed: ..' & 'proxy changed: ..'

proxy.off(null, onModelChanged);
proxy.set({ name: 'Charles' }); // Will log 'proxied changed: ..'

Backbone's 'overridables', i.e. properties and methods that affect model behaviour when set (namely collection, idAttribute, sync, parse, validate, url, urlRoot and toJSON) should be set on the proxied model. That is to say, the root proxied Backbone.Model. Setting them on a proxied model is not meant to (and will generally not) produce the intended result. As an example:

// Setting a validate method on a proxy ..
proxy.validate = function (attrs) {
  if (attrs.name === 'Betty') {
    return 'Betty is not a valid name';
  }
};

// .. will not work: This will log 'validation error: none'
proxy.set({ name: 'Betty' }, { validate: true });
console.log('validation error: ' + (proxy.validationError || 'none'));

// Setting a validate method on the proxied ..
proxied.validate = function (attrs) {
  if (attrs.name === 'Charles') {
    return 'Charles is not a valid name';
  }
};

// .. will produce the intended result:
//  This will log 'validation error: Charles is not a valid name'
proxy.set({ name: 'Charles' }, { validate: true });
console.log('validation error: ' + (proxy.validationError || 'none'));

Aside from the prior examples, the annotated version of the source is available as a reference.

Contributing ( / Testing )

Contributions are obviously appreciated. In lieu of a formal styleguide, take care to maintain the existing coding style. Please make sure your changes test out green prior to pull requests. The QUnit test suite may be run in a browser (test/index.html) or on the command line, by running make test or npm test. The command line version runs on Node and depends on node-qunit (npm install to fetch it before testing). A coverage report is also available.

License

Licensed and freely distributed under the MIT License (LICENSE.txt).

Copyright (c) 2014 Alex Lambiris