binder.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. "use strict";
  2. /**
  3. * @fileOverview allows you to bind a change watcher that looks for get and set operations on an arbitrary
  4. * property of an object at at any depth. This allows you to look for changes or intercept values asynchronously or otherwise.
  5. * @module documents/binder
  6. * @requires async
  7. * @requires documents/probe
  8. * @requires lodash
  9. * @requires promise
  10. */
  11. var Promise = require( 'promise' );
  12. var async = require( "async" );
  13. var probe = require( "./probe" );
  14. var sys = require( "lodash" );
  15. /**
  16. * Identifies the properties that the binder expects
  17. * @type {{getter: null, getterAsync: boolean, setter: null, validator: null, validatorAsync: boolean, setterAsync: boolean}}
  18. * @private
  19. */
  20. var dataBinderOptions = exports.dataBinderOptions = {
  21. getter : null,
  22. getterAsync : false,
  23. setter : null,
  24. validator : null,
  25. validatorAsync : false,
  26. setterAsync : false
  27. };
  28. /**
  29. * You can unbind previously bound objects from here.
  30. *
  31. * @param {string} path The path that was bound using {@link module:documents/binder.bind}
  32. * @param {*} record The object that was bound
  33. */
  34. exports.unbind = function ( path, record ) {
  35. var context = record;
  36. var lastParent = context;
  37. var parts = path.split( probe.delimiter );
  38. var lastPartName = path;
  39. var lastParentName;
  40. sys.each( parts, function ( part ) {
  41. lastParentName = part;
  42. lastParent = context;
  43. context = context[part];
  44. lastPartName = part;
  45. if ( sys.isNull( context ) || sys.isUndefined( context ) ) {
  46. context = {};
  47. }
  48. } );
  49. if ( lastParent === context ) {
  50. deleteBindings( record, lastPartName );
  51. } else {
  52. deleteBindings( lastParent, lastPartName );
  53. }
  54. function deleteBindings( mountPoint, mountName ) {
  55. mountPoint[mountName] = mountPoint["__" + mountName + "__"];
  56. delete mountPoint["__" + mountName + "__"];
  57. }
  58. };
  59. /**
  60. * Bind to a property somewhere in an object. The property is found using dot notation and can be arbitrarily deep.
  61. * @param {string} path The path into the object to locate the property. For instance this could be `"_id"`, `"name.last"`.
  62. * or `"some.really.really.long.path.including.an.array.2.name"`
  63. * @param {object} record Anything you can hang a property off of
  64. * @param {options} options What you wanna do with the doohicky when yoyu bind it.
  65. * @param {function(*):Promise|*=} options.getter This is the method to run when getting the value. When it runs, you will receive
  66. * a single parameter which is the current value as the object understands it. You can return the value directly, just raise an event or
  67. * whatever your little heart demands. However, if you are asynchronous, this will turn your return value into a promise, one of the
  68. * few places this system will embrace promises over node-like error passing and that is mainly because this is a getter so a return value
  69. * is particularly important. *
  70. * @param {*} options.getter.value The current value of the record
  71. * @param {function(err, value)=} options.getter.callback When asynchronous, return you value through this method using node style
  72. * error passing (the promise is handled for you by this method).
  73. * @param {boolean=} options.getterAsync When true (not truthy) the getter is treated asynchronously and returns a promise with your value.
  74. * @param {function(*, *, *)=} options.setter A setter method
  75. * @param {*} options.setter.newVal The new value
  76. * @param {*} options.setter.oldVal The old value
  77. * @param {*} options.setter.record The record hosting the change
  78. * @param {function(*, *, *, function=)=} options.validator If you want a validator to run before settings values, pass this guy in
  79. * @param {*} options.validator.newVal The new value
  80. * @param {*} options.validator.oldVal The old value
  81. * @param {*} options.validator.record The record hosting the change
  82. * @param {function(err)=} options.validator.callback If the validator is asynchronous, then pass your value back here, otherwise pass it back as a return value.
  83. * When you use an asynchronous instance, pass the error in the first value and then the rest of the parameters are yours to play with
  84. * @param {boolean=} options.validatorAsync When true (not truthy) the validator is treated asynchornously and returns a promise with your value.
  85. * @returns {*}
  86. */
  87. exports.bind = function ( path, record, options ) {
  88. options = sys.extend( {}, dataBinderOptions, options );
  89. var context = record;
  90. var lastParent = context;
  91. var parts = path.split( probe.delimiter );
  92. var lastPartName = path;
  93. var lastParentName;
  94. sys.each( parts, function ( part ) {
  95. lastParentName = part;
  96. lastParent = context;
  97. context = context[part];
  98. lastPartName = part;
  99. if ( sys.isNull( context ) || sys.isUndefined( context ) ) {
  100. context = {};
  101. }
  102. } );
  103. if ( lastParent === context ) {
  104. setUpBindings( record, lastPartName );
  105. } else {
  106. setUpBindings( lastParent, lastPartName );
  107. }
  108. function setUpBindings( mountPoint, mountName ) {
  109. mountPoint["__" + mountName + "__"] = mountPoint[mountName];
  110. Object.defineProperty( mountPoint, mountName, {
  111. get : function () {
  112. if ( sys.isFunction( options.getter ) ) {
  113. var promise;
  114. if ( options.getterAsync === true ) {
  115. promise = Promise.denodeify( options.getter );
  116. }
  117. if ( promise ) {
  118. return promise( mountPoint["__" + mountName + "__"] ).then( function ( val ) {
  119. mountPoint["__" + mountName + "__"] = val;
  120. } );
  121. } else {
  122. mountPoint["__" + mountName + "__"] = options.getter( mountPoint["__" + mountName + "__"] );
  123. return mountPoint["__" + mountName + "__"];
  124. }
  125. } else {
  126. return mountPoint["__" + mountName + "__"];
  127. }
  128. },
  129. set : function ( val ) {
  130. async.waterfall( [
  131. function ( done ) {
  132. if ( sys.isFunction( options.validator ) ) {
  133. if ( options.validatorAsync ) {
  134. options.validator( val, mountPoint["__" + mountName + "__"], record, done );
  135. } else {
  136. var res = options.validator( val, mountPoint["__" + mountName + "__"], record );
  137. if ( res === true ) {
  138. done();
  139. } else {
  140. done( res );
  141. }
  142. }
  143. } else {
  144. done();
  145. }
  146. },
  147. function ( done ) {
  148. if ( sys.isFunction( options.setter ) ) {
  149. if ( options.setterAsync === true ) {
  150. options.setter( val, mountPoint["__" + mountName + "__"], record, done );
  151. } else {
  152. done( null, options.setter( val, mountPoint["__" + mountName + "__"], record ) );
  153. }
  154. } else {
  155. done( null, val );
  156. }
  157. }
  158. ], function ( err, newVal ) {
  159. if ( err ) { throw new Error( err ); }
  160. mountPoint["__" + mountName + "__"] = newVal;
  161. } );
  162. }
  163. } );
  164. }
  165. return context;
  166. };