observablemixin.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module utils/observablemixin
  7. */
  8. import EmitterMixin from './emittermixin';
  9. import CKEditorError from './ckeditorerror';
  10. import { extend, isObject } from 'lodash-es';
  11. const observablePropertiesSymbol = Symbol( 'observableProperties' );
  12. const boundObservablesSymbol = Symbol( 'boundObservables' );
  13. const boundPropertiesSymbol = Symbol( 'boundProperties' );
  14. /**
  15. * Mixin that injects the "observable properties" and data binding functionality described in the
  16. * {@link ~Observable} interface.
  17. *
  18. * @mixin ObservableMixin
  19. * @mixes module:utils/emittermixin~EmitterMixin
  20. * @implements module:utils/observablemixin~Observable
  21. */
  22. const ObservableMixin = {
  23. /**
  24. * @inheritDoc
  25. */
  26. set( name, value ) {
  27. // If the first parameter is an Object, iterate over its properties.
  28. if ( isObject( name ) ) {
  29. Object.keys( name ).forEach( property => {
  30. this.set( property, name[ property ] );
  31. }, this );
  32. return;
  33. }
  34. initObservable( this );
  35. const properties = this[ observablePropertiesSymbol ];
  36. if ( ( name in this ) && !properties.has( name ) ) {
  37. /**
  38. * Cannot override an existing property.
  39. *
  40. * This error is thrown when trying to {@link ~Observable#set set} an property with
  41. * a name of an already existing property. For example:
  42. *
  43. * let observable = new Model();
  44. * observable.property = 1;
  45. * observable.set( 'property', 2 ); // throws
  46. *
  47. * observable.set( 'property', 1 );
  48. * observable.set( 'property', 2 ); // ok, because this is an existing property.
  49. *
  50. * @error observable-set-cannot-override
  51. */
  52. throw new CKEditorError( 'observable-set-cannot-override: Cannot override an existing property.' );
  53. }
  54. Object.defineProperty( this, name, {
  55. enumerable: true,
  56. configurable: true,
  57. get() {
  58. return properties.get( name );
  59. },
  60. set( value ) {
  61. const oldValue = properties.get( name );
  62. // Fire `set` event before the new value will be set to make it possible
  63. // to override observable property without affecting `change` event.
  64. // See https://github.com/ckeditor/ckeditor5-utils/issues/171.
  65. let newValue = this.fire( 'set:' + name, name, value, oldValue );
  66. if ( newValue === undefined ) {
  67. newValue = value;
  68. }
  69. // Allow undefined as an initial value like A.define( 'x', undefined ) (#132).
  70. // Note: When properties map has no such own property, then its value is undefined.
  71. if ( oldValue !== newValue || !properties.has( name ) ) {
  72. properties.set( name, newValue );
  73. this.fire( 'change:' + name, name, newValue, oldValue );
  74. }
  75. }
  76. } );
  77. this[ name ] = value;
  78. },
  79. /**
  80. * @inheritDoc
  81. */
  82. bind( ...bindProperties ) {
  83. if ( !bindProperties.length || !isStringArray( bindProperties ) ) {
  84. /**
  85. * All properties must be strings.
  86. *
  87. * @error observable-bind-wrong-properties
  88. */
  89. throw new CKEditorError( 'observable-bind-wrong-properties: All properties must be strings.' );
  90. }
  91. if ( ( new Set( bindProperties ) ).size !== bindProperties.length ) {
  92. /**
  93. * Properties must be unique.
  94. *
  95. * @error observable-bind-duplicate-properties
  96. */
  97. throw new CKEditorError( 'observable-bind-duplicate-properties: Properties must be unique.' );
  98. }
  99. initObservable( this );
  100. const boundProperties = this[ boundPropertiesSymbol ];
  101. bindProperties.forEach( propertyName => {
  102. if ( boundProperties.has( propertyName ) ) {
  103. /**
  104. * Cannot bind the same property more that once.
  105. *
  106. * @error observable-bind-rebind
  107. */
  108. throw new CKEditorError( 'observable-bind-rebind: Cannot bind the same property more that once.' );
  109. }
  110. } );
  111. const bindings = new Map();
  112. // @typedef {Object} Binding
  113. // @property {Array} property Property which is bound.
  114. // @property {Array} to Array of observable–property components of the binding (`{ observable: ..., property: .. }`).
  115. // @property {Array} callback A function which processes `to` components.
  116. bindProperties.forEach( a => {
  117. const binding = { property: a, to: [] };
  118. boundProperties.set( a, binding );
  119. bindings.set( a, binding );
  120. } );
  121. // @typedef {Object} BindChain
  122. // @property {Function} to See {@link ~ObservableMixin#_bindTo}.
  123. // @property {Function} toMany See {@link ~ObservableMixin#_bindToMany}.
  124. // @property {module:utils/observablemixin~Observable} _observable The observable which initializes the binding.
  125. // @property {Array} _bindProperties Array of `_observable` properties to be bound.
  126. // @property {Array} _to Array of `to()` observable–properties (`{ observable: toObservable, properties: ...toProperties }`).
  127. // @property {Map} _bindings Stores bindings to be kept in
  128. // {@link ~ObservableMixin#_boundProperties}/{@link ~ObservableMixin#_boundObservables}
  129. // initiated in this binding chain.
  130. return {
  131. to: bindTo,
  132. toMany: bindToMany,
  133. _observable: this,
  134. _bindProperties: bindProperties,
  135. _to: [],
  136. _bindings: bindings
  137. };
  138. },
  139. /**
  140. * @inheritDoc
  141. */
  142. unbind( ...unbindProperties ) {
  143. // Nothing to do here if not inited yet.
  144. if ( !( observablePropertiesSymbol in this ) ) {
  145. return;
  146. }
  147. const boundProperties = this[ boundPropertiesSymbol ];
  148. const boundObservables = this[ boundObservablesSymbol ];
  149. if ( unbindProperties.length ) {
  150. if ( !isStringArray( unbindProperties ) ) {
  151. /**
  152. * Properties must be strings.
  153. *
  154. * @error observable-unbind-wrong-properties
  155. */
  156. throw new CKEditorError( 'observable-unbind-wrong-properties: Properties must be strings.' );
  157. }
  158. unbindProperties.forEach( propertyName => {
  159. const binding = boundProperties.get( propertyName );
  160. // Nothing to do if the binding is not defined
  161. if ( !binding ) {
  162. return;
  163. }
  164. let toObservable, toProperty, toProperties, toPropertyBindings;
  165. binding.to.forEach( to => {
  166. // TODO: ES6 destructuring.
  167. toObservable = to[ 0 ];
  168. toProperty = to[ 1 ];
  169. toProperties = boundObservables.get( toObservable );
  170. toPropertyBindings = toProperties[ toProperty ];
  171. toPropertyBindings.delete( binding );
  172. if ( !toPropertyBindings.size ) {
  173. delete toProperties[ toProperty ];
  174. }
  175. if ( !Object.keys( toProperties ).length ) {
  176. boundObservables.delete( toObservable );
  177. this.stopListening( toObservable, 'change' );
  178. }
  179. } );
  180. boundProperties.delete( propertyName );
  181. } );
  182. } else {
  183. boundObservables.forEach( ( bindings, boundObservable ) => {
  184. this.stopListening( boundObservable, 'change' );
  185. } );
  186. boundObservables.clear();
  187. boundProperties.clear();
  188. }
  189. },
  190. /**
  191. * @inheritDoc
  192. */
  193. decorate( methodName ) {
  194. const originalMethod = this[ methodName ];
  195. if ( !originalMethod ) {
  196. /**
  197. * Cannot decorate an undefined method.
  198. *
  199. * @error observablemixin-cannot-decorate-undefined
  200. * @param {Object} object The object which method should be decorated.
  201. * @param {String} methodName Name of the method which does not exist.
  202. */
  203. throw new CKEditorError(
  204. 'observablemixin-cannot-decorate-undefined: Cannot decorate an undefined method.',
  205. { object: this, methodName }
  206. );
  207. }
  208. this.on( methodName, ( evt, args ) => {
  209. evt.return = originalMethod.apply( this, args );
  210. } );
  211. this[ methodName ] = function( ...args ) {
  212. return this.fire( methodName, args );
  213. };
  214. }
  215. };
  216. extend( ObservableMixin, EmitterMixin );
  217. export default ObservableMixin;
  218. // Init symbol properties needed to for the observable mechanism to work.
  219. //
  220. // @private
  221. // @param {module:utils/observablemixin~ObservableMixin} observable
  222. function initObservable( observable ) {
  223. // Do nothing if already inited.
  224. if ( observablePropertiesSymbol in observable ) {
  225. return;
  226. }
  227. // The internal hash containing the observable's state.
  228. //
  229. // @private
  230. // @type {Map}
  231. Object.defineProperty( observable, observablePropertiesSymbol, {
  232. value: new Map()
  233. } );
  234. // Map containing bindings to external observables. It shares the binding objects
  235. // (`{ observable: A, property: 'a', to: ... }`) with {@link module:utils/observablemixin~ObservableMixin#_boundProperties} and
  236. // it is used to observe external observables to update own properties accordingly.
  237. // See {@link module:utils/observablemixin~ObservableMixin#bind}.
  238. //
  239. // A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
  240. // console.log( A._boundObservables );
  241. //
  242. // Map( {
  243. // B: {
  244. // x: Set( [
  245. // { observable: A, property: 'a', to: [ [ B, 'x' ] ] },
  246. // { observable: A, property: 'c', to: [ [ B, 'x' ] ] }
  247. // ] ),
  248. // y: Set( [
  249. // { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
  250. // ] )
  251. // }
  252. // } )
  253. //
  254. // A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback );
  255. // console.log( A._boundObservables );
  256. //
  257. // Map( {
  258. // B: {
  259. // x: Set( [
  260. // { observable: A, property: 'a', to: [ [ B, 'x' ] ] },
  261. // { observable: A, property: 'c', to: [ [ B, 'x' ] ] }
  262. // ] ),
  263. // y: Set( [
  264. // { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
  265. // ] ),
  266. // z: Set( [
  267. // { observable: A, property: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
  268. // ] )
  269. // },
  270. // C: {
  271. // w: Set( [
  272. // { observable: A, property: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
  273. // ] )
  274. // }
  275. // } )
  276. //
  277. // @private
  278. // @type {Map}
  279. Object.defineProperty( observable, boundObservablesSymbol, {
  280. value: new Map()
  281. } );
  282. // Object that stores which properties of this observable are bound and how. It shares
  283. // the binding objects (`{ observable: A, property: 'a', to: ... }`) with {@link utils.ObservableMixin#_boundObservables}.
  284. // This data structure is a reverse of {@link utils.ObservableMixin#_boundObservables} and it is helpful for
  285. // {@link utils.ObservableMixin#unbind}.
  286. //
  287. // See {@link utils.ObservableMixin#bind}.
  288. //
  289. // A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
  290. // console.log( A._boundProperties );
  291. //
  292. // Map( {
  293. // a: { observable: A, property: 'a', to: [ [ B, 'x' ] ] },
  294. // b: { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
  295. // c: { observable: A, property: 'c', to: [ [ B, 'x' ] ] }
  296. // } )
  297. //
  298. // A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback );
  299. // console.log( A._boundProperties );
  300. //
  301. // Map( {
  302. // a: { observable: A, property: 'a', to: [ [ B, 'x' ] ] },
  303. // b: { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
  304. // c: { observable: A, property: 'c', to: [ [ B, 'x' ] ] },
  305. // d: { observable: A, property: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
  306. // } )
  307. //
  308. // @private
  309. // @type {Map}
  310. Object.defineProperty( observable, boundPropertiesSymbol, {
  311. value: new Map()
  312. } );
  313. }
  314. // A chaining for {@link module:utils/observablemixin~ObservableMixin#bind} providing `.to()` interface.
  315. //
  316. // @private
  317. // @param {...[Observable|String|Function]} args Arguments of the `.to( args )` binding.
  318. function bindTo( ...args ) {
  319. const parsedArgs = parseBindToArgs( ...args );
  320. const bindingsKeys = Array.from( this._bindings.keys() );
  321. const numberOfBindings = bindingsKeys.length;
  322. // Eliminate A.bind( 'x' ).to( B, C )
  323. if ( !parsedArgs.callback && parsedArgs.to.length > 1 ) {
  324. /**
  325. * Binding multiple observables only possible with callback.
  326. *
  327. * @error observable-bind-no-callback
  328. */
  329. throw new CKEditorError( 'observable-bind-to-no-callback: Binding multiple observables only possible with callback.' );
  330. }
  331. // Eliminate A.bind( 'x', 'y' ).to( B, callback )
  332. if ( numberOfBindings > 1 && parsedArgs.callback ) {
  333. /**
  334. * Cannot bind multiple properties and use a callback in one binding.
  335. *
  336. * @error observable-bind-to-extra-callback
  337. */
  338. throw new CKEditorError( 'observable-bind-to-extra-callback: Cannot bind multiple properties and use a callback in one binding.' );
  339. }
  340. parsedArgs.to.forEach( to => {
  341. // Eliminate A.bind( 'x', 'y' ).to( B, 'a' )
  342. if ( to.properties.length && to.properties.length !== numberOfBindings ) {
  343. /**
  344. * The number of properties must match.
  345. *
  346. * @error observable-bind-to-properties-length
  347. */
  348. throw new CKEditorError( 'observable-bind-to-properties-length: The number of properties must match.' );
  349. }
  350. // When no to.properties specified, observing source properties instead i.e.
  351. // A.bind( 'x', 'y' ).to( B ) -> Observe B.x and B.y
  352. if ( !to.properties.length ) {
  353. to.properties = this._bindProperties;
  354. }
  355. } );
  356. this._to = parsedArgs.to;
  357. // Fill {@link BindChain#_bindings} with callback. When the callback is set there's only one binding.
  358. if ( parsedArgs.callback ) {
  359. this._bindings.get( bindingsKeys[ 0 ] ).callback = parsedArgs.callback;
  360. }
  361. attachBindToListeners( this._observable, this._to );
  362. // Update observable._boundProperties and observable._boundObservables.
  363. updateBindToBound( this );
  364. // Set initial values of bound properties.
  365. this._bindProperties.forEach( propertyName => {
  366. updateBoundObservableProperty( this._observable, propertyName );
  367. } );
  368. }
  369. // Binds to an attribute in a set of iterable observables.
  370. //
  371. // @private
  372. // @param {Array.<Observable>} observables
  373. // @param {String} attribute
  374. // @param {Function} callback
  375. function bindToMany( observables, attribute, callback ) {
  376. if ( this._bindings.size > 1 ) {
  377. /**
  378. * Binding one attribute to many observables only possible with one attribute.
  379. *
  380. * @error observable-bind-to-many-not-one-binding
  381. */
  382. throw new CKEditorError( 'observable-bind-to-many-not-one-binding: Cannot bind multiple properties with toMany().' );
  383. }
  384. this.to(
  385. // Bind to #attribute of each observable...
  386. ...getBindingTargets( observables, attribute ),
  387. // ...using given callback to parse attribute values.
  388. callback
  389. );
  390. }
  391. // Returns an array of binding components for
  392. // {@link Observable#bind} from a set of iterable observables.
  393. //
  394. // @param {Array.<Observable>} observables
  395. // @param {String} attribute
  396. // @returns {Array.<String|Observable>}
  397. function getBindingTargets( observables, attribute ) {
  398. const observableAndAttributePairs = observables.map( observable => [ observable, attribute ] );
  399. // Merge pairs to one-dimension array of observables and attributes.
  400. return Array.prototype.concat.apply( [], observableAndAttributePairs );
  401. }
  402. // Check if all entries of the array are of `String` type.
  403. //
  404. // @private
  405. // @param {Array} arr An array to be checked.
  406. // @returns {Boolean}
  407. function isStringArray( arr ) {
  408. return arr.every( a => typeof a == 'string' );
  409. }
  410. // Parses and validates {@link Observable#bind}`.to( args )` arguments and returns
  411. // an object with a parsed structure. For example
  412. //
  413. // A.bind( 'x' ).to( B, 'a', C, 'b', call );
  414. //
  415. // becomes
  416. //
  417. // {
  418. // to: [
  419. // { observable: B, properties: [ 'a' ] },
  420. // { observable: C, properties: [ 'b' ] },
  421. // ],
  422. // callback: call
  423. // }
  424. //
  425. // @private
  426. // @param {...*} args Arguments of {@link Observable#bind}`.to( args )`.
  427. // @returns {Object}
  428. function parseBindToArgs( ...args ) {
  429. // Eliminate A.bind( 'x' ).to()
  430. if ( !args.length ) {
  431. /**
  432. * Invalid argument syntax in `to()`.
  433. *
  434. * @error observable-bind-to-parse-error
  435. */
  436. throw new CKEditorError( 'observable-bind-to-parse-error: Invalid argument syntax in `to()`.' );
  437. }
  438. const parsed = { to: [] };
  439. let lastObservable;
  440. if ( typeof args[ args.length - 1 ] == 'function' ) {
  441. parsed.callback = args.pop();
  442. }
  443. args.forEach( a => {
  444. if ( typeof a == 'string' ) {
  445. lastObservable.properties.push( a );
  446. } else if ( typeof a == 'object' ) {
  447. lastObservable = { observable: a, properties: [] };
  448. parsed.to.push( lastObservable );
  449. } else {
  450. throw new CKEditorError( 'observable-bind-to-parse-error: Invalid argument syntax in `to()`.' );
  451. }
  452. } );
  453. return parsed;
  454. }
  455. // Synchronizes {@link module:utils/observablemixin#_boundObservables} with {@link Binding}.
  456. //
  457. // @private
  458. // @param {Binding} binding A binding to store in {@link Observable#_boundObservables}.
  459. // @param {Observable} toObservable A observable, which is a new component of `binding`.
  460. // @param {String} toPropertyName A name of `toObservable`'s property, a new component of the `binding`.
  461. function updateBoundObservables( observable, binding, toObservable, toPropertyName ) {
  462. const boundObservables = observable[ boundObservablesSymbol ];
  463. const bindingsToObservable = boundObservables.get( toObservable );
  464. const bindings = bindingsToObservable || {};
  465. if ( !bindings[ toPropertyName ] ) {
  466. bindings[ toPropertyName ] = new Set();
  467. }
  468. // Pass the binding to a corresponding Set in `observable._boundObservables`.
  469. bindings[ toPropertyName ].add( binding );
  470. if ( !bindingsToObservable ) {
  471. boundObservables.set( toObservable, bindings );
  472. }
  473. }
  474. // Synchronizes {@link Observable#_boundProperties} and {@link Observable#_boundObservables}
  475. // with {@link BindChain}.
  476. //
  477. // Assuming the following binding being created
  478. //
  479. // A.bind( 'a', 'b' ).to( B, 'x', 'y' );
  480. //
  481. // the following bindings were initialized by {@link Observable#bind} in {@link BindChain#_bindings}:
  482. //
  483. // {
  484. // a: { observable: A, property: 'a', to: [] },
  485. // b: { observable: A, property: 'b', to: [] },
  486. // }
  487. //
  488. // Iterate over all bindings in this chain and fill their `to` properties with
  489. // corresponding to( ... ) arguments (components of the binding), so
  490. //
  491. // {
  492. // a: { observable: A, property: 'a', to: [ B, 'x' ] },
  493. // b: { observable: A, property: 'b', to: [ B, 'y' ] },
  494. // }
  495. //
  496. // Then update the structure of {@link Observable#_boundObservables} with updated
  497. // binding, so it becomes:
  498. //
  499. // Map( {
  500. // B: {
  501. // x: Set( [
  502. // { observable: A, property: 'a', to: [ [ B, 'x' ] ] }
  503. // ] ),
  504. // y: Set( [
  505. // { observable: A, property: 'b', to: [ [ B, 'y' ] ] },
  506. // ] )
  507. // }
  508. // } )
  509. //
  510. // @private
  511. // @param {BindChain} chain The binding initialized by {@link Observable#bind}.
  512. function updateBindToBound( chain ) {
  513. let toProperty;
  514. chain._bindings.forEach( ( binding, propertyName ) => {
  515. // Note: For a binding without a callback, this will run only once
  516. // like in A.bind( 'x', 'y' ).to( B, 'a', 'b' )
  517. // TODO: ES6 destructuring.
  518. chain._to.forEach( to => {
  519. toProperty = to.properties[ binding.callback ? 0 : chain._bindProperties.indexOf( propertyName ) ];
  520. binding.to.push( [ to.observable, toProperty ] );
  521. updateBoundObservables( chain._observable, binding, to.observable, toProperty );
  522. } );
  523. } );
  524. }
  525. // Updates an property of a {@link Observable} with a value
  526. // determined by an entry in {@link Observable#_boundProperties}.
  527. //
  528. // @private
  529. // @param {Observable} observable A observable which property is to be updated.
  530. // @param {String} propertyName An property to be updated.
  531. function updateBoundObservableProperty( observable, propertyName ) {
  532. const boundProperties = observable[ boundPropertiesSymbol ];
  533. const binding = boundProperties.get( propertyName );
  534. let propertyValue;
  535. // When a binding with callback is created like
  536. //
  537. // A.bind( 'a' ).to( B, 'b', C, 'c', callback );
  538. //
  539. // collect B.b and C.c, then pass them to callback to set A.a.
  540. if ( binding.callback ) {
  541. propertyValue = binding.callback.apply( observable, binding.to.map( to => to[ 0 ][ to[ 1 ] ] ) );
  542. } else {
  543. propertyValue = binding.to[ 0 ];
  544. propertyValue = propertyValue[ 0 ][ propertyValue[ 1 ] ];
  545. }
  546. if ( observable.hasOwnProperty( propertyName ) ) {
  547. observable[ propertyName ] = propertyValue;
  548. } else {
  549. observable.set( propertyName, propertyValue );
  550. }
  551. }
  552. // Starts listening to changes in {@link BindChain._to} observables to update
  553. // {@link BindChain._observable} {@link BindChain._bindProperties}. Also sets the
  554. // initial state of {@link BindChain._observable}.
  555. //
  556. // @private
  557. // @param {BindChain} chain The chain initialized by {@link Observable#bind}.
  558. function attachBindToListeners( observable, toBindings ) {
  559. toBindings.forEach( to => {
  560. const boundObservables = observable[ boundObservablesSymbol ];
  561. let bindings;
  562. // If there's already a chain between the observables (`observable` listens to
  563. // `to.observable`), there's no need to create another `change` event listener.
  564. if ( !boundObservables.get( to.observable ) ) {
  565. observable.listenTo( to.observable, 'change', ( evt, propertyName ) => {
  566. bindings = boundObservables.get( to.observable )[ propertyName ];
  567. // Note: to.observable will fire for any property change, react
  568. // to changes of properties which are bound only.
  569. if ( bindings ) {
  570. bindings.forEach( binding => {
  571. updateBoundObservableProperty( observable, binding.property );
  572. } );
  573. }
  574. } );
  575. }
  576. } );
  577. }
  578. /**
  579. * Interface which adds "observable properties" and data binding functionality.
  580. *
  581. * Can be easily implemented by a class by mixing the {@link module:utils/observablemixin~ObservableMixin} mixin.
  582. *
  583. * @interface Observable
  584. * @extends module:utils/emittermixin~Emitter
  585. */
  586. /**
  587. * Fired when a property changed value.
  588. *
  589. * observable.set( 'prop', 1 );
  590. *
  591. * observable.on( 'change:prop', ( evt, propertyName, newValue, oldValue ) => {
  592. * console.log( `${ propertyName } has changed from ${ oldValue } to ${ newValue }` );
  593. * } );
  594. *
  595. * observable.prop = 2; // -> 'prop has changed from 1 to 2'
  596. *
  597. * @event change:{property}
  598. * @param {String} name The property name.
  599. * @param {*} value The new property value.
  600. * @param {*} oldValue The previous property value.
  601. */
  602. /**
  603. * Fired when a property value is going to be set but is not set yet (before the `change` event is fired).
  604. *
  605. * You can control the final value of the property by using
  606. * the {@link module:utils/eventinfo~EventInfo#return event's `return` property}.
  607. *
  608. * observable.set( 'prop', 1 );
  609. *
  610. * observable.on( 'set:prop', ( evt, propertyName, newValue, oldValue ) => {
  611. * console.log( `Value is going to be changed from ${ oldValue } to ${ newValue }` );
  612. * console.log( `Current property value is ${ observable[ propertyName ] }` );
  613. *
  614. * // Let's override the value.
  615. * evt.return = 3;
  616. * } );
  617. *
  618. * observable.on( 'change:prop', ( evt, propertyName, newValue, oldValue ) => {
  619. * console.log( `Value has changed from ${ oldValue } to ${ newValue }` );
  620. * } );
  621. *
  622. * observable.prop = 2; // -> 'Value is going to be changed from 1 to 2'
  623. * // -> 'Current property value is 1'
  624. * // -> 'Value has changed from 1 to 3'
  625. *
  626. * **Note:** Event is fired even when the new value is the same as the old value.
  627. *
  628. * @event set:{property}
  629. * @param {String} name The property name.
  630. * @param {*} value The new property value.
  631. * @param {*} oldValue The previous property value.
  632. */
  633. /**
  634. * Creates and sets the value of an observable property of this object. Such an property becomes a part
  635. * of the state and is be observable.
  636. *
  637. * It accepts also a single object literal containing key/value pairs with properties to be set.
  638. *
  639. * This method throws the `observable-set-cannot-override` error if the observable instance already
  640. * have a property with the given property name. This prevents from mistakenly overriding existing
  641. * properties and methods, but means that `foo.set( 'bar', 1 )` may be slightly slower than `foo.bar = 1`.
  642. *
  643. * @method #set
  644. * @param {String|Object} name The property's name or object with `name=>value` pairs.
  645. * @param {*} [value] The property's value (if `name` was passed in the first parameter).
  646. */
  647. /**
  648. * Binds observable properties to another objects implementing {@link module:utils/observablemixin~Observable}
  649. * interface (like {@link module:ui/model~Model}).
  650. *
  651. * Once bound, the observable will immediately share the current state of properties
  652. * of the observable it is bound to and react to the changes to these properties
  653. * in the future.
  654. *
  655. * **Note**: To release the binding use {@link module:utils/observablemixin~Observable#unbind}.
  656. *
  657. * Using `bind().to()` chain:
  658. *
  659. * A.bind( 'a' ).to( B );
  660. * A.bind( 'a' ).to( B, 'b' );
  661. * A.bind( 'a', 'b' ).to( B, 'c', 'd' );
  662. * A.bind( 'a' ).to( B, 'b', C, 'd', ( b, d ) => b + d );
  663. *
  664. * It is also possible to bind to the same property in a observables collection using `bind().toMany()` chain:
  665. *
  666. * A.bind( 'a' ).toMany( [ B, C, D ], 'x', ( a, b, c ) => a + b + c );
  667. * A.bind( 'a' ).toMany( [ B, C, D ], 'x', ( ...x ) => x.every( x => x ) );
  668. *
  669. * @method #bind
  670. * @param {...String} bindProperties Observable properties that will be bound to another observable(s).
  671. * @returns {Object} The bind chain with the `to()` and `toMany()` methods.
  672. */
  673. /**
  674. * Removes the binding created with {@link #bind}.
  675. *
  676. * A.unbind( 'a' );
  677. * A.unbind();
  678. *
  679. * @method #unbind
  680. * @param {...String} [unbindProperties] Observable properties to be unbound. All the bindings will
  681. * be released if no properties provided.
  682. */
  683. /**
  684. * Turns the given methods of this object into event-based ones. This means that the new method will fire an event
  685. * (named after the method) and the original action will be plugged as a listener to that event.
  686. *
  687. * This is a very simplified method decoration. Itself it doesn't change the behavior of a method (expect adding the event),
  688. * but it allows to modify it later on by listening to the method's event.
  689. *
  690. * For example, in order to cancel the method execution one can stop the event:
  691. *
  692. * class Foo {
  693. * constructor() {
  694. * this.decorate( 'method' );
  695. * }
  696. *
  697. * method() {
  698. * console.log( 'called!' );
  699. * }
  700. * }
  701. *
  702. * const foo = new Foo();
  703. * foo.on( 'method', ( evt ) => {
  704. * evt.stop();
  705. * }, { priority: 'high' } );
  706. *
  707. * foo.method(); // Nothing is logged.
  708. *
  709. *
  710. * Note: we used a high priority listener here to execute this callback before the one which
  711. * calls the original method (which used the default priority).
  712. *
  713. * It's also possible to change the return value:
  714. *
  715. * foo.on( 'method', ( evt ) => {
  716. * evt.return = 'Foo!';
  717. * } );
  718. *
  719. * foo.method(); // -> 'Foo'
  720. *
  721. * Finally, it's possible to access and modify the parameters:
  722. *
  723. * method( a, b ) {
  724. * console.log( `${ a }, ${ b }` );
  725. * }
  726. *
  727. * // ...
  728. *
  729. * foo.on( 'method', ( evt, args ) => {
  730. * args[ 0 ] = 3;
  731. *
  732. * console.log( args[ 1 ] ); // -> 2
  733. * }, { priority: 'high' } );
  734. *
  735. * foo.method( 1, 2 ); // -> '3, 2'
  736. *
  737. * @method #decorate
  738. * @param {String} methodName Name of the method to decorate.
  739. */