refs.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. 'use strict';
  2. var Collection = require('./collection');
  3. function hasOwnProperty(e, property) {
  4. return Object.prototype.hasOwnProperty.call(e, property.name || property);
  5. }
  6. function defineCollectionProperty(ref, property, target) {
  7. var collection = Collection.extend(target[property.name] || [], ref, property, target);
  8. Object.defineProperty(target, property.name, {
  9. enumerable: property.enumerable,
  10. value: collection
  11. });
  12. if (collection.length) {
  13. collection.forEach(function(o) {
  14. ref.set(o, property.inverse, target);
  15. });
  16. }
  17. }
  18. function defineProperty(ref, property, target) {
  19. var inverseProperty = property.inverse;
  20. var _value = target[property.name];
  21. Object.defineProperty(target, property.name, {
  22. configurable: property.configurable,
  23. enumerable: property.enumerable,
  24. get: function() {
  25. return _value;
  26. },
  27. set: function(value) {
  28. // return if we already performed all changes
  29. if (value === _value) {
  30. return;
  31. }
  32. var old = _value;
  33. // temporary set null
  34. _value = null;
  35. if (old) {
  36. ref.unset(old, inverseProperty, target);
  37. }
  38. // set new value
  39. _value = value;
  40. // set inverse value
  41. ref.set(_value, inverseProperty, target);
  42. }
  43. });
  44. }
  45. /**
  46. * Creates a new references object defining two inversly related
  47. * attribute descriptors a and b.
  48. *
  49. * <p>
  50. * When bound to an object using {@link Refs#bind} the references
  51. * get activated and ensure that add and remove operations are applied
  52. * reversely, too.
  53. * </p>
  54. *
  55. * <p>
  56. * For attributes represented as collections {@link Refs} provides the
  57. * {@link RefsCollection#add}, {@link RefsCollection#remove} and {@link RefsCollection#contains} extensions
  58. * that must be used to properly hook into the inverse change mechanism.
  59. * </p>
  60. *
  61. * @class Refs
  62. *
  63. * @classdesc A bi-directional reference between two attributes.
  64. *
  65. * @param {Refs.AttributeDescriptor} a property descriptor
  66. * @param {Refs.AttributeDescriptor} b property descriptor
  67. *
  68. * @example
  69. *
  70. * var refs = Refs({ name: 'wheels', collection: true, enumerable: true }, { name: 'car' });
  71. *
  72. * var car = { name: 'toyota' };
  73. * var wheels = [{ pos: 'front-left' }, { pos: 'front-right' }];
  74. *
  75. * refs.bind(car, 'wheels');
  76. *
  77. * car.wheels // []
  78. * car.wheels.add(wheels[0]);
  79. * car.wheels.add(wheels[1]);
  80. *
  81. * car.wheels // [{ pos: 'front-left' }, { pos: 'front-right' }]
  82. *
  83. * wheels[0].car // { name: 'toyota' };
  84. * car.wheels.remove(wheels[0]);
  85. *
  86. * wheels[0].car // undefined
  87. */
  88. function Refs(a, b) {
  89. if (!(this instanceof Refs)) {
  90. return new Refs(a, b);
  91. }
  92. // link
  93. a.inverse = b;
  94. b.inverse = a;
  95. this.props = {};
  96. this.props[a.name] = a;
  97. this.props[b.name] = b;
  98. }
  99. /**
  100. * Binds one side of a bi-directional reference to a
  101. * target object.
  102. *
  103. * @memberOf Refs
  104. *
  105. * @param {Object} target
  106. * @param {String} property
  107. */
  108. Refs.prototype.bind = function(target, property) {
  109. if (typeof property === 'string') {
  110. if (!this.props[property]) {
  111. throw new Error('no property <' + property + '> in ref');
  112. }
  113. property = this.props[property];
  114. }
  115. if (property.collection) {
  116. defineCollectionProperty(this, property, target);
  117. } else {
  118. defineProperty(this, property, target);
  119. }
  120. };
  121. Refs.prototype.ensureRefsCollection = function(target, property) {
  122. var collection = target[property.name];
  123. if (!Collection.isExtended(collection)) {
  124. defineCollectionProperty(this, property, target);
  125. }
  126. return collection;
  127. };
  128. Refs.prototype.ensureBound = function(target, property) {
  129. if (!hasOwnProperty(target, property)) {
  130. this.bind(target, property);
  131. }
  132. };
  133. Refs.prototype.unset = function(target, property, value) {
  134. if (target) {
  135. this.ensureBound(target, property);
  136. if (property.collection) {
  137. this.ensureRefsCollection(target, property).remove(value);
  138. } else {
  139. target[property.name] = undefined;
  140. }
  141. }
  142. };
  143. Refs.prototype.set = function(target, property, value) {
  144. if (target) {
  145. this.ensureBound(target, property);
  146. if (property.collection) {
  147. this.ensureRefsCollection(target, property).add(value);
  148. } else {
  149. target[property.name] = value;
  150. }
  151. }
  152. };
  153. module.exports = Refs;
  154. /**
  155. * An attribute descriptor to be used specify an attribute in a {@link Refs} instance
  156. *
  157. * @typedef {Object} Refs.AttributeDescriptor
  158. * @property {String} name
  159. * @property {boolean} [collection=false]
  160. * @property {boolean} [enumerable=false]
  161. */