observablemixin.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. import EmitterMixin from './emittermixin.js';
  7. import CKEditorError from './ckeditorerror.js';
  8. import extend from './lib/lodash/extend.js';
  9. import isObject from './lib/lodash/isObject.js';
  10. const attributesSymbol = Symbol( 'attributes' );
  11. const boundObservablesSymbol = Symbol( 'boundObservables' );
  12. const boundAttributesSymbol = Symbol( 'boundAttributes' );
  13. /**
  14. * Mixin that injects the "observable attributes" and data binding functionality.
  15. * Used mainly in the {@link core.ui.Model} class.
  16. *
  17. * @mixin core.ObservableMixin
  18. * @mixes core.EmitterMixin
  19. * @implements core.Observable
  20. */
  21. const ObservableMixin = {
  22. /**
  23. * Creates and sets the value of an observable attribute of this object. Such an attribute becomes a part
  24. * of the state and is be observable.
  25. *
  26. * It accepts also a single object literal containing key/value pairs with attributes to be set.
  27. *
  28. * This method throws the {@link observable-set-cannot-override} error if the observable instance already
  29. * have a property with a given attribute name. This prevents from mistakenly overriding existing
  30. * properties and methods, but means that `foo.set( 'bar', 1 )` may be slightly slower than `foo.bar = 1`.
  31. *
  32. * @method core.ObservableMixin#set
  33. * @param {String} name The attributes name.
  34. * @param {*} value The attributes value.
  35. */
  36. set( name, value ) {
  37. // If the first parameter is an Object, iterate over its properties.
  38. if ( isObject( name ) ) {
  39. Object.keys( name ).forEach( ( attr ) => {
  40. this.set( attr, name[ attr ] );
  41. }, this );
  42. return;
  43. }
  44. initObservable( this );
  45. const attributes = this[ attributesSymbol ];
  46. if ( ( name in this ) && !attributes.has( name ) ) {
  47. /**
  48. * Cannot override an existing property.
  49. *
  50. * This error is thrown when trying to {@link core.ui.Observable#set set} an attribute with
  51. * a name of an already existing property. For example:
  52. *
  53. * let observable = new Model();
  54. * observable.property = 1;
  55. * observable.set( 'property', 2 ); // throws
  56. *
  57. * observable.set( 'attr', 1 );
  58. * observable.set( 'attr', 2 ); // ok, because this is an existing attribute.
  59. *
  60. * @error observable-set-cannot-override
  61. */
  62. throw new CKEditorError( 'observable-set-cannot-override: Cannot override an existing property.' );
  63. }
  64. Object.defineProperty( this, name, {
  65. enumerable: true,
  66. configurable: true,
  67. get() {
  68. return attributes.get( name );
  69. },
  70. set( value ) {
  71. const oldValue = attributes.get( name );
  72. // Allow undefined as an initial value like A.define( 'x', undefined ) (#132).
  73. // Note: When attributes map has no such own property, then its value is undefined.
  74. if ( oldValue !== value || !attributes.has( name ) ) {
  75. attributes.set( name, value );
  76. this.fire( 'change', name, value, oldValue );
  77. this.fire( 'change:' + name, value, oldValue );
  78. }
  79. }
  80. } );
  81. this[ name ] = value;
  82. },
  83. /**
  84. * Binds observable attributes to another objects implementing {@link ObservableMixin}
  85. * interface (like {@link core.ui.Model}).
  86. *
  87. * Once bound, the observable will immediately share the current state of attributes
  88. * of the observable it is bound to and react to the changes to these attributes
  89. * in the future.
  90. *
  91. * **Note**: To release the binding use {@link core.ObservableMixin#unbind}.
  92. *
  93. * A.bind( 'a' ).to( B );
  94. * A.bind( 'a' ).to( B, 'b' );
  95. * A.bind( 'a', 'b' ).to( B, 'c', 'd' );
  96. * A.bind( 'a' ).to( B, 'b', C, 'd', ( b, d ) => b + d );
  97. *
  98. * @method core.ObservableMixin#bind
  99. * @param {String...} bindAttrs Observable attributes that will be bound to another observable(s).
  100. * @returns {core.BindChain}
  101. */
  102. bind( ...bindAttrs ) {
  103. if ( !bindAttrs.length || !isStringArray( bindAttrs ) ) {
  104. /**
  105. * All attributes must be strings.
  106. *
  107. * @error observable-bind-wrong-attrs
  108. */
  109. throw new CKEditorError( 'observable-bind-wrong-attrs: All attributes must be strings.' );
  110. }
  111. if ( ( new Set( bindAttrs ) ).size !== bindAttrs.length ) {
  112. /**
  113. * Attributes must be unique.
  114. *
  115. * @error observable-bind-duplicate-attrs
  116. */
  117. throw new CKEditorError( 'observable-bind-duplicate-attrs: Attributes must be unique.' );
  118. }
  119. initObservable( this );
  120. const boundAttributes = this[ boundAttributesSymbol ];
  121. bindAttrs.forEach( attrName => {
  122. if ( boundAttributes.has( attrName ) ) {
  123. /**
  124. * Cannot bind the same attribute more that once.
  125. *
  126. * @error observable-bind-rebind
  127. */
  128. throw new CKEditorError( 'observable-bind-rebind: Cannot bind the same attribute more that once.' );
  129. }
  130. } );
  131. const bindings = new Map();
  132. /**
  133. * @typedef core.Binding
  134. * @type Object
  135. * @property {Array} attr Attribute which is bound.
  136. * @property {Array} to Array of observable–attribute components of the binding (`{ observable: ..., attr: .. }`).
  137. * @property {Array} callback A function which processes `to` components.
  138. */
  139. bindAttrs.forEach( a => {
  140. const binding = { attr: a, to: [] };
  141. boundAttributes.set( a, binding );
  142. bindings.set( a, binding );
  143. } );
  144. /**
  145. * @typedef core.BindChain
  146. * @type Object
  147. * @property {Function} to See {@link core.ObservableMixin#_bindTo}.
  148. * @property {Observable} _observable The observable which initializes the binding.
  149. * @property {Array} _bindAttrs Array of `_observable` attributes to be bound.
  150. * @property {Array} _to Array of `to()` observable–attributes (`{ observable: toObservable, attrs: ...toAttrs }`).
  151. * @property {Map} _bindings Stores bindings to be kept in
  152. * {@link core.ObservableMixin#_boundAttributes}/{@link core.ObservableMixin#_boundObservables}
  153. * initiated in this binding chain.
  154. */
  155. return {
  156. to: bindTo,
  157. _observable: this,
  158. _bindAttrs: bindAttrs,
  159. _to: [],
  160. _bindings: bindings
  161. };
  162. },
  163. /**
  164. * Removes the binding created with {@link core.ObservableMixin#bind}.
  165. *
  166. * A.unbind( 'a' );
  167. * A.unbind();
  168. *
  169. * @method core.ObservableMixin#unbind
  170. * @param {String} [unbindAttrs] Observable attributes to be unbound. All the bindings will
  171. * be released if no attributes provided.
  172. */
  173. unbind( ...unbindAttrs ) {
  174. // Nothing to do here if not inited yet.
  175. if ( !( attributesSymbol in this ) ) {
  176. return;
  177. }
  178. const boundAttributes = this[ boundAttributesSymbol ];
  179. const boundObservables = this[ boundObservablesSymbol ];
  180. if ( unbindAttrs.length ) {
  181. if ( !isStringArray( unbindAttrs ) ) {
  182. /**
  183. * Attributes must be strings.
  184. *
  185. * @error observable-unbind-wrong-attrs
  186. */
  187. throw new CKEditorError( 'observable-unbind-wrong-attrs: Attributes must be strings.' );
  188. }
  189. unbindAttrs.forEach( attrName => {
  190. const binding = boundAttributes.get( attrName );
  191. let toObservable, toAttr, toAttrs, toAttrBindings;
  192. binding.to.forEach( to => {
  193. // TODO: ES6 destructuring.
  194. toObservable = to[ 0 ];
  195. toAttr = to[ 1 ];
  196. toAttrs = boundObservables.get( toObservable );
  197. toAttrBindings = toAttrs[ toAttr ];
  198. toAttrBindings.delete( binding );
  199. if ( !toAttrBindings.size ) {
  200. delete toAttrs[ toAttr ];
  201. }
  202. if ( !Object.keys( toAttrs ).length ) {
  203. boundObservables.delete( toObservable );
  204. this.stopListening( toObservable, 'change' );
  205. }
  206. } );
  207. boundAttributes.delete( attrName );
  208. } );
  209. } else {
  210. boundObservables.forEach( ( bindings, boundObservable ) => {
  211. this.stopListening( boundObservable, 'change' );
  212. } );
  213. boundObservables.clear();
  214. boundAttributes.clear();
  215. }
  216. }
  217. };
  218. export default ObservableMixin;
  219. // Init symbol properties needed to for the observable mechanism to work.
  220. //
  221. // @private
  222. // @param {ObservableMixin} observable
  223. function initObservable( observable ) {
  224. // Do nothing if already inited.
  225. if ( attributesSymbol in observable ) {
  226. return;
  227. }
  228. // The internal hash containing the observable's state.
  229. //
  230. // @private
  231. // @type {Map}
  232. Object.defineProperty( observable, attributesSymbol, {
  233. value: new Map()
  234. } );
  235. // Map containing bindings to external observables. It shares the binding objects
  236. // (`{ observable: A, attr: 'a', to: ... }`) with {@link core.ObservableMixin#_boundAttributes} and
  237. // it is used to observe external observables to update own attributes accordingly.
  238. // See {@link core.ObservableMixin#bind}.
  239. //
  240. // A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
  241. // console.log( A._boundObservables );
  242. //
  243. // Map( {
  244. // B: {
  245. // x: Set( [
  246. // { observable: A, attr: 'a', to: [ [ B, 'x' ] ] },
  247. // { observable: A, attr: 'c', to: [ [ B, 'x' ] ] }
  248. // ] ),
  249. // y: Set( [
  250. // { observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
  251. // ] )
  252. // }
  253. // } )
  254. //
  255. // A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback );
  256. // console.log( A._boundObservables );
  257. //
  258. // Map( {
  259. // B: {
  260. // x: Set( [
  261. // { observable: A, attr: 'a', to: [ [ B, 'x' ] ] },
  262. // { observable: A, attr: 'c', to: [ [ B, 'x' ] ] }
  263. // ] ),
  264. // y: Set( [
  265. // { observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
  266. // ] ),
  267. // z: Set( [
  268. // { observable: A, attr: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
  269. // ] )
  270. // },
  271. // C: {
  272. // w: Set( [
  273. // { observable: A, attr: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
  274. // ] )
  275. // }
  276. // } )
  277. //
  278. // @private
  279. // @type {Map}
  280. Object.defineProperty( observable, boundObservablesSymbol, {
  281. value: new Map()
  282. } );
  283. // Object that stores which attributes of this observable are bound and how. It shares
  284. // the binding objects (`{ observable: A, attr: 'a', to: ... }`) with {@link core.ObservableMixin#_boundObservables}.
  285. // This data structure is a reverse of {@link core.ObservableMixin#_boundObservables} and it is helpful for
  286. // {@link core.ObservableMixin#unbind}.
  287. //
  288. // See {@link core.ObservableMixin#bind}.
  289. //
  290. // A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
  291. // console.log( A._boundAttributes );
  292. //
  293. // Map( {
  294. // a: { observable: A, attr: 'a', to: [ [ B, 'x' ] ] },
  295. // b: { observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
  296. // c: { observable: A, attr: 'c', to: [ [ B, 'x' ] ] }
  297. // } )
  298. //
  299. // A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback );
  300. // console.log( A._boundAttributes );
  301. //
  302. // Map( {
  303. // a: { observable: A, attr: 'a', to: [ [ B, 'x' ] ] },
  304. // b: { observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
  305. // c: { observable: A, attr: 'c', to: [ [ B, 'x' ] ] },
  306. // d: { observable: A, attr: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
  307. // } )
  308. //
  309. // @private
  310. // @type {Map}
  311. Object.defineProperty( observable, boundAttributesSymbol, {
  312. value: new Map()
  313. } );
  314. }
  315. // A chaining for {@link core.ObservableMixin#bind} providing `.to()` interface.
  316. //
  317. // @private
  318. // @param {...[Observable|String|Function]} args Arguments of the `.to( args )` binding.
  319. function bindTo( ...args ) {
  320. /* jshint validthis: true */
  321. const parsedArgs = parseBindToArgs( ...args );
  322. const bindingsKeys = Array.from( this._bindings.keys() );
  323. const numberOfBindings = bindingsKeys.length;
  324. // Eliminate A.bind( 'x' ).to( B, C )
  325. if ( !parsedArgs.callback && parsedArgs.to.length > 1 ) {
  326. /**
  327. * Binding multiple observables only possible with callback.
  328. *
  329. * @error observable-bind-no-callback
  330. */
  331. throw new CKEditorError( 'observable-bind-to-no-callback: Binding multiple observables only possible with callback.' ) ;
  332. }
  333. // Eliminate A.bind( 'x', 'y' ).to( B, callback )
  334. if ( numberOfBindings > 1 && parsedArgs.callback ) {
  335. /**
  336. * Cannot bind multiple attributes and use a callback in one binding.
  337. *
  338. * @error observable-bind-to-extra-callback
  339. */
  340. throw new CKEditorError( 'observable-bind-to-extra-callback: Cannot bind multiple attributes and use a callback in one binding.' ) ;
  341. }
  342. parsedArgs.to.forEach( to => {
  343. // Eliminate A.bind( 'x', 'y' ).to( B, 'a' )
  344. if ( to.attrs.length && to.attrs.length !== numberOfBindings ) {
  345. /**
  346. * The number of attributes must match.
  347. *
  348. * @error observable-bind-to-attrs-length
  349. */
  350. throw new CKEditorError( 'observable-bind-to-attrs-length: The number of attributes must match.' );
  351. }
  352. // When no to.attrs specified, observing source attributes instead i.e.
  353. // A.bind( 'x', 'y' ).to( B ) -> Observe B.x and B.y
  354. if ( !to.attrs.length ) {
  355. to.attrs = this._bindAttrs;
  356. }
  357. // Eliminate A.bind( 'x', 'y' ).to( B, 'a', 'b' ) when B has no 'a'.
  358. if ( !hasAttributes( to.observable, to.attrs ) ) {
  359. /*
  360. * Observable has no such attribute(s).
  361. *
  362. * @error observable-bind-to-missing-attr
  363. */
  364. throw new CKEditorError( 'observable-bind-to-missing-attr: Observable has no such attribute(s).' );
  365. }
  366. } );
  367. this._to = parsedArgs.to;
  368. // Fill {@link BindChain#_bindings} with callback. When the callback is set there's only one binding.
  369. if ( parsedArgs.callback ) {
  370. this._bindings.get( bindingsKeys[ 0 ] ).callback = parsedArgs.callback;
  371. }
  372. attachBindToListeners( this._observable, this._to );
  373. // Update observable._boundAttributes and observable._boundObservables.
  374. updateBindToBound( this );
  375. // Set initial values of bound attributes.
  376. this._bindAttrs.forEach( attrName => {
  377. updateBoundObservableAttr( this._observable, attrName );
  378. } );
  379. }
  380. // Check if the {@link core.Observable} has given `attrs`.
  381. //
  382. // @private
  383. // @param {Observable} observable Observable to be checked.
  384. // @param {Array} arr An array of `String`.
  385. // @returns {Boolean}
  386. function hasAttributes( observable, attrs ) {
  387. return attrs.every( a => observable[ attributesSymbol ].has( a ) );
  388. }
  389. // Check if all entries of the array are of `String` type.
  390. //
  391. // @private
  392. // @param {Array} arr An array to be checked.
  393. // @returns {Boolean}
  394. function isStringArray( arr ) {
  395. return arr.every( a => typeof a == 'string' );
  396. }
  397. // Parses and validates {@link Observable#bind}`.to( args )` arguments and returns
  398. // an object with a parsed structure. For example
  399. //
  400. // A.bind( 'x' ).to( B, 'a', C, 'b', call );
  401. //
  402. // becomes
  403. //
  404. // {
  405. // to: [
  406. // { observable: B, attrs: [ 'a' ] },
  407. // { observable: C, attrs: [ 'b' ] },
  408. // ],
  409. // callback: call
  410. // }
  411. //
  412. // @private
  413. // @param {...*} args Arguments of {@link Observable#bind}`.to( args )`.
  414. // @returns {Object}
  415. function parseBindToArgs( ...args ) {
  416. // Eliminate A.bind( 'x' ).to()
  417. if ( !args.length ) {
  418. /**
  419. * Invalid argument syntax in `to()`.
  420. *
  421. * @error observable-bind-to-parse-error
  422. */
  423. throw new CKEditorError( 'observable-bind-to-parse-error: Invalid argument syntax in `to()`.' );
  424. }
  425. const parsed = { to: [] };
  426. let lastObservable;
  427. if ( typeof args[ args.length - 1 ] == 'function' ) {
  428. parsed.callback = args.pop();
  429. }
  430. args.forEach( a => {
  431. if ( typeof a == 'string' ) {
  432. lastObservable.attrs.push( a );
  433. } else if ( typeof a == 'object' ) {
  434. lastObservable = { observable: a, attrs: [] };
  435. parsed.to.push( lastObservable );
  436. } else {
  437. throw new CKEditorError( 'observable-bind-to-parse-error: Invalid argument syntax in `to()`.' );
  438. }
  439. } );
  440. return parsed;
  441. }
  442. // Synchronizes {@link Observable#_boundObservables} with {@link Binding}.
  443. //
  444. // @private
  445. // @param {Binding} binding A binding to store in {@link Observable#_boundObservables}.
  446. // @param {Observable} toObservable A observable, which is a new component of `binding`.
  447. // @param {String} toAttrName A name of `toObservable`'s attribute, a new component of the `binding`.
  448. function updateBoundObservables( observable, binding, toObservable, toAttrName ) {
  449. const boundObservables = observable[ boundObservablesSymbol ];
  450. const bindingsToObservable = boundObservables.get( toObservable );
  451. const bindings = bindingsToObservable || {};
  452. if ( !bindings[ toAttrName ] ) {
  453. bindings[ toAttrName ] = new Set();
  454. }
  455. // Pass the binding to a corresponding Set in `observable._boundObservables`.
  456. bindings[ toAttrName ].add( binding );
  457. if ( !bindingsToObservable ) {
  458. boundObservables.set( toObservable, bindings );
  459. }
  460. }
  461. // Synchronizes {@link Observable#_boundAttributes} and {@link Observable#_boundObservables}
  462. // with {@link BindChain}.
  463. //
  464. // Assuming the following binding being created
  465. //
  466. // A.bind( 'a', 'b' ).to( B, 'x', 'y' );
  467. //
  468. // the following bindings were initialized by {@link Observable#bind} in {@link BindChain#_bindings}:
  469. //
  470. // {
  471. // a: { observable: A, attr: 'a', to: [] },
  472. // b: { observable: A, attr: 'b', to: [] },
  473. // }
  474. //
  475. // Iterate over all bindings in this chain and fill their `to` properties with
  476. // corresponding to( ... ) arguments (components of the binding), so
  477. //
  478. // {
  479. // a: { observable: A, attr: 'a', to: [ B, 'x' ] },
  480. // b: { observable: A, attr: 'b', to: [ B, 'y' ] },
  481. // }
  482. //
  483. // Then update the structure of {@link Observable#_boundObservables} with updated
  484. // binding, so it becomes:
  485. //
  486. // Map( {
  487. // B: {
  488. // x: Set( [
  489. // { observable: A, attr: 'a', to: [ [ B, 'x' ] ] }
  490. // ] ),
  491. // y: Set( [
  492. // { observable: A, attr: 'b', to: [ [ B, 'y' ] ] },
  493. // ] )
  494. // }
  495. // } )
  496. //
  497. // @private
  498. // @param {BindChain} chain The binding initialized by {@link Observable#bind}.
  499. function updateBindToBound( chain ) {
  500. let toAttr;
  501. chain._bindings.forEach( ( binding, attrName ) => {
  502. // Note: For a binding without a callback, this will run only once
  503. // like in A.bind( 'x', 'y' ).to( B, 'a', 'b' )
  504. // TODO: ES6 destructuring.
  505. chain._to.forEach( to => {
  506. toAttr = to.attrs[ binding.callback ? 0 : chain._bindAttrs.indexOf( attrName ) ];
  507. binding.to.push( [ to.observable, toAttr ] );
  508. updateBoundObservables( chain._observable, binding, to.observable, toAttr );
  509. } );
  510. } );
  511. }
  512. // Updates an attribute of a {@link Observable} with a value
  513. // determined by an entry in {@link Observable#_boundAttributes}.
  514. //
  515. // @private
  516. // @param {Observable} observable A observable which attribute is to be updated.
  517. // @param {String} attrName An attribute to be updated.
  518. function updateBoundObservableAttr( observable, attrName ) {
  519. const boundAttributes = observable[ boundAttributesSymbol ];
  520. const binding = boundAttributes.get( attrName );
  521. let attrValue;
  522. // When a binding with callback is created like
  523. //
  524. // A.bind( 'a' ).to( B, 'b', C, 'c', callback );
  525. //
  526. // collect B.b and C.c, then pass them to callback to set A.a.
  527. if ( binding.callback ) {
  528. attrValue = binding.callback.apply( observable, binding.to.map( to => to[ 0 ][ to[ 1 ] ] ) );
  529. } else {
  530. attrValue = binding.to[ 0 ];
  531. attrValue = attrValue[ 0 ][ attrValue[ 1 ] ];
  532. }
  533. // TODO: Needs update after https://github.com/ckeditor/ckeditor5-core.issues/132.
  534. if ( observable.hasOwnProperty( attrName ) ) {
  535. observable[ attrName ] = attrValue;
  536. } else {
  537. observable.set( attrName, attrValue );
  538. }
  539. }
  540. // Starts listening to changes in {@link BindChain._to} observables to update
  541. // {@link BindChain._observable} {@link BindChain._bindAttrs}. Also sets the
  542. // initial state of {@link BindChain._observable}.
  543. //
  544. // @private
  545. // @param {BindChain} chain The chain initialized by {@link Observable#bind}.
  546. function attachBindToListeners( observable, toBindings ) {
  547. toBindings.forEach( to => {
  548. const boundObservables = observable[ boundObservablesSymbol ];
  549. let bindings;
  550. // If there's already a chain between the observables (`observable` listens to
  551. // `to.observable`), there's no need to create another `change` event listener.
  552. if ( !boundObservables.get( to.observable ) ) {
  553. observable.listenTo( to.observable, 'change', ( evt, attrName ) => {
  554. bindings = boundObservables.get( to.observable )[ attrName ];
  555. // Note: to.observable will fire for any attribute change, react
  556. // to changes of attributes which are bound only.
  557. if ( bindings ) {
  558. bindings.forEach( binding => {
  559. updateBoundObservableAttr( observable, binding.attr );
  560. } );
  561. }
  562. } );
  563. }
  564. } );
  565. }
  566. extend( ObservableMixin, EmitterMixin );
  567. /**
  568. * Fired when an attribute changed value.
  569. *
  570. * @event core.ObservableMixin#change
  571. * @param {String} name The attribute name.
  572. * @param {*} value The new attribute value.
  573. * @param {*} oldValue The previous attribute value.
  574. */
  575. /**
  576. * Fired when an specific attribute changed value.
  577. *
  578. * @event core.ObservableMixin#change:{attribute}
  579. * @param {*} value The new attribute value.
  580. * @param {*} oldValue The previous attribute value.
  581. */
  582. /**
  583. * Interface representing classes which mix in {@link core.ObservableMixin}.
  584. *
  585. * @interface core.Observable
  586. */