'use strict'; var Collection = require('./collection'); function hasOwnProperty(e, property) { return Object.prototype.hasOwnProperty.call(e, property.name || property); } function defineCollectionProperty(ref, property, target) { var collection = Collection.extend(target[property.name] || [], ref, property, target); Object.defineProperty(target, property.name, { enumerable: property.enumerable, value: collection }); if (collection.length) { collection.forEach(function(o) { ref.set(o, property.inverse, target); }); } } function defineProperty(ref, property, target) { var inverseProperty = property.inverse; var _value = target[property.name]; Object.defineProperty(target, property.name, { configurable: property.configurable, enumerable: property.enumerable, get: function() { return _value; }, set: function(value) { // return if we already performed all changes if (value === _value) { return; } var old = _value; // temporary set null _value = null; if (old) { ref.unset(old, inverseProperty, target); } // set new value _value = value; // set inverse value ref.set(_value, inverseProperty, target); } }); } /** * Creates a new references object defining two inversly related * attribute descriptors a and b. * *
* When bound to an object using {@link Refs#bind} the references * get activated and ensure that add and remove operations are applied * reversely, too. *
* ** For attributes represented as collections {@link Refs} provides the * {@link RefsCollection#add}, {@link RefsCollection#remove} and {@link RefsCollection#contains} extensions * that must be used to properly hook into the inverse change mechanism. *
* * @class Refs * * @classdesc A bi-directional reference between two attributes. * * @param {Refs.AttributeDescriptor} a property descriptor * @param {Refs.AttributeDescriptor} b property descriptor * * @example * * var refs = Refs({ name: 'wheels', collection: true, enumerable: true }, { name: 'car' }); * * var car = { name: 'toyota' }; * var wheels = [{ pos: 'front-left' }, { pos: 'front-right' }]; * * refs.bind(car, 'wheels'); * * car.wheels // [] * car.wheels.add(wheels[0]); * car.wheels.add(wheels[1]); * * car.wheels // [{ pos: 'front-left' }, { pos: 'front-right' }] * * wheels[0].car // { name: 'toyota' }; * car.wheels.remove(wheels[0]); * * wheels[0].car // undefined */ function Refs(a, b) { if (!(this instanceof Refs)) { return new Refs(a, b); } // link a.inverse = b; b.inverse = a; this.props = {}; this.props[a.name] = a; this.props[b.name] = b; } /** * Binds one side of a bi-directional reference to a * target object. * * @memberOf Refs * * @param {Object} target * @param {String} property */ Refs.prototype.bind = function(target, property) { if (typeof property === 'string') { if (!this.props[property]) { throw new Error('no property <' + property + '> in ref'); } property = this.props[property]; } if (property.collection) { defineCollectionProperty(this, property, target); } else { defineProperty(this, property, target); } }; Refs.prototype.ensureRefsCollection = function(target, property) { var collection = target[property.name]; if (!Collection.isExtended(collection)) { defineCollectionProperty(this, property, target); } return collection; }; Refs.prototype.ensureBound = function(target, property) { if (!hasOwnProperty(target, property)) { this.bind(target, property); } }; Refs.prototype.unset = function(target, property, value) { if (target) { this.ensureBound(target, property); if (property.collection) { this.ensureRefsCollection(target, property).remove(value); } else { target[property.name] = undefined; } } }; Refs.prototype.set = function(target, property, value) { if (target) { this.ensureBound(target, property); if (property.collection) { this.ensureRefsCollection(target, property).add(value); } else { target[property.name] = value; } } }; module.exports = Refs; /** * An attribute descriptor to be used specify an attribute in a {@link Refs} instance * * @typedef {Object} Refs.AttributeDescriptor * @property {String} name * @property {boolean} [collection=false] * @property {boolean} [enumerable=false] */