observablemixin.js 20 KB

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