]> git.proxmox.com Git - extjs.git/blob - extjs/packages/core/src/mixin/Bindable.js
add extjs 6.0.1 sources
[extjs.git] / extjs / packages / core / src / mixin / Bindable.js
1 /**
2 * This class is intended as a mixin for classes that want to provide a "bind" config that
3 * connects to a `ViewModel`.
4 * @private
5 * @since 5.0.0
6 */
7 Ext.define('Ext.mixin.Bindable', {
8 mixinId: 'bindable',
9
10 config: {
11 /**
12 * @cfg {Object} [bind]
13 * Setting this config option adds or removes data bindings for other configs.
14 * For example, to bind the `title` config:
15 *
16 * var panel = Ext.create({
17 * xtype: 'panel',
18 * bind: {
19 * title: 'Hello {user.name}'
20 * }
21 * });
22 *
23 * To dynamically add bindings:
24 *
25 * panel.setBind({
26 * title: 'Greetings {user.name}!'
27 * });
28 *
29 * To remove bindings:
30 *
31 * panel.setBind({
32 * title: null
33 * });
34 *
35 * The bind expressions are presented to `{@link Ext.app.ViewModel#bind}`. The
36 * `ViewModel` instance is determined by `lookupViewModel`.
37 */
38 bind: {
39 $value: null,
40 lazy: true
41 },
42
43 // @cmd-auto-dependency { aliasPrefix: 'controller.' }
44 /**
45 * @cfg {String/Object/Ext.app.ViewController} controller
46 * A string alias, a configuration object or an instance of a `ViewController` for
47 * this container. Sample usage:
48 *
49 * Ext.define('MyApp.UserController', {
50 * alias: 'controller.user'
51 * });
52 *
53 * Ext.define('UserContainer', {
54 * extend: 'Ext.container.container',
55 * controller: 'user'
56 * });
57 * // Or
58 * Ext.define('UserContainer', {
59 * extend: 'Ext.container.container',
60 * controller: {
61 * type: 'user',
62 * someConfig: true
63 * }
64 * });
65 *
66 * // Can also instance at runtime
67 * var ctrl = new MyApp.UserController();
68 * var view = new UserContainer({
69 * controller: ctrl
70 * });
71 *
72 */
73 controller: null,
74
75 /**
76 * @method getController
77 * Returns the {@link Ext.app.ViewController} instance associated with this
78 * component via the {@link #controller} config or {@link #setController} method.
79 * @return {Ext.app.ViewController} Returns this component's ViewController or
80 * null if one was not configured
81 */
82
83 /**
84 * @cfg {Boolean} defaultListenerScope
85 * If `true`, this component will be the default scope (this pointer) for events
86 * specified with string names so that the scope can be dynamically resolved. The
87 * component will automatically become the defaultListenerScope if a
88 * {@link #controller} is specified.
89 *
90 * See the introductory docs for {@link Ext.container.Container} for some sample
91 * usages.
92 *
93 * **NOTE**: This value can only be reliably set at construction time. Setting it
94 * after that time may not correctly rewire all of the potentially effected
95 * listeners.
96 */
97 defaultListenerScope: false,
98
99 /**
100 * @cfg {String/String[]/Object} publishes
101 * One or more names of config properties that this component should publish
102 * to its ViewModel. Generally speaking, only properties defined in a class config
103 * block (including ancestor config blocks and mixins) are eligible for publishing
104 * to the viewModel. Some components override this and publish their most useful
105 * configs by default.
106 *
107 * **Note:** We'll discuss publishing properties **not** found in the config block below.
108 *
109 * Values determined to be invalid by component (often form fields and model validations)
110 * will not be published to the ViewModel.
111 *
112 * This config uses the `{@link #cfg-reference}` to determine the name of the data
113 * object to place in the `ViewModel`. If `reference` is not set then this config
114 * is ignored.
115 *
116 * By using this config and `{@link #cfg-reference}` you can bind configs between
117 * components. For example:
118 *
119 * ...
120 * items: [{
121 * xtype: 'textfield',
122 * reference: 'somefield', // component's name in the ViewModel
123 * publishes: 'value' // value is not published by default
124 * },{
125 * ...
126 * },{
127 * xtype: 'displayfield',
128 * bind: 'You have entered "{somefield.value}"'
129 * }]
130 * ...
131 *
132 * Classes must provide this config as an Object:
133 *
134 * Ext.define('App.foo.Bar', {
135 * publishes: {
136 * foo: true,
137 * bar: true
138 * }
139 * });
140 *
141 * This is required for the config system to properly merge values from derived
142 * classes.
143 *
144 * For instances this value can be specified as a value as show above or an array
145 * or object as follows:
146 *
147 * {
148 * xtype: 'textfield',
149 * reference: 'somefield',
150 * publishes: [
151 * 'value',
152 * 'rawValue',
153 * 'dirty'
154 * ]
155 * }
156 *
157 * // This achieves the same result as the above array form.
158 * {
159 * xtype: 'textfield',
160 * reference: 'somefield',
161 * publishes: {
162 * value: true,
163 * rawValue: true,
164 * dirty: true
165 * }
166 * }
167 *
168 * In some cases, users may want to publish a property to the viewModel that is not found in a class
169 * config block. In these situations, you may utilize {@link #publishState} if the property has a
170 * setter method. Let's use {@link Ext.form.Labelable#setFieldLabel setFieldLabel} as an example:
171 *
172 * setFieldLabel: function(fieldLabel) {
173 * this.callParent(arguments);
174 * this.publishState('fieldLabel', fieldLabel);
175 * }
176 *
177 * With the above chunk of code, fieldLabel may now be published to the viewModel.
178 *
179 * @since 5.0.0
180 */
181 publishes: {
182 $value: null,
183 lazy: true,
184 merge: function (newValue, oldValue) {
185 return this.mergeSets(newValue, oldValue);
186 }
187 },
188
189 /**
190 * @cfg {String} reference
191 * Specifies a name for this component inside its component hierarchy. This name
192 * must be unique within its {@link Ext.container.Container#referenceHolder view}
193 * or its {@link Ext.app.ViewController ViewController}. See the documentation in
194 * {@link Ext.container.Container} for more information about references.
195 *
196 * **Note**: Valid identifiers start with a letter or underscore and are followed
197 * by zero or more additional letters, underscores or digits. References are case
198 * sensitive.
199 */
200 reference: null,
201
202 // @cmd-auto-dependency { directRef: 'Ext.data.Session' }
203 /**
204 * @cfg {Boolean/Object/Ext.data.Session} [session=null]
205 * If provided this creates a new `Session` instance for this component. If this
206 * is a `Container`, this will then be inherited by all child components.
207 *
208 * To create a new session you can specify `true`:
209 *
210 * Ext.create({
211 * xtype: 'viewport',
212 * session: true,
213 *
214 * items: [{
215 * ...
216 * }]
217 * });
218 *
219 * Alternatively, a config object can be provided:
220 *
221 * Ext.create({
222 * xtype: 'viewport',
223 * session: {
224 * ...
225 * },
226 *
227 * items: [{
228 * ...
229 * }]
230 * });
231 *
232 */
233 session: {
234 $value: null,
235 lazy: true
236 },
237
238 /**
239 * @cfg {String/String[]/Object} twoWayBindable
240 * This object holds a map of `config` properties that will update their binding
241 * as they are modified. For example, `value` is a key added by form fields. The
242 * form of this config is the same as `{@link #publishes}`.
243 *
244 * This config is defined so that updaters are not created and added for all
245 * bound properties since most cannot be modified by the end-user and hence are
246 * not appropriate for two-way binding.
247 */
248 twoWayBindable: {
249 $value: null,
250 lazy: true,
251 merge: function (newValue, oldValue) {
252 return this.mergeSets(newValue, oldValue);
253 }
254 },
255
256 // @cmd-auto-dependency { aliasPrefix: 'viewmodel.' }
257 /**
258 * @cfg {String/Object/Ext.app.ViewModel} viewModel
259 * The `ViewModel` is a data provider for this component and its children. The
260 * data contained in the `ViewModel` is typically used by adding `bind` configs
261 * to the components that want present or edit this data.
262 *
263 * When set, the `ViewModel` is created and links to any inherited `viewModel`
264 * instance from an ancestor container as the "parent". The `ViewModel` hierarchy,
265 * once established, only supports creation or destruction of children. The
266 * parent of a `ViewModel` cannot be changed on the fly.
267 *
268 * If this is a root-level `ViewModel`, the data model connection is made to this
269 * component's associated `{@link Ext.data.Session Data Session}`. This is
270 * determined by calling `getInheritedSession`.
271 *
272 */
273 viewModel: {
274 $value: null,
275 lazy: true
276 }
277 },
278
279 /**
280 * @property {String} [defaultBindProperty]
281 * This property is used to determine the property of a `bind` config that is just
282 * the value. For example, if `defaultBindProperty="value"`, then this shorthand
283 * `bind` config:
284 *
285 * bind: '{name}'
286 *
287 * Is equivalent to this object form:
288 *
289 * bind: {
290 * value: '{name}'
291 * }
292 *
293 * The `defaultBindProperty` is set to "value" for form fields and to "store" for
294 * grids and trees.
295 * @protected
296 */
297 defaultBindProperty: null,
298
299 /**
300 * @property {RegExp}
301 * Regular expression used for validating `reference` values.
302 * @private
303 */
304 validRefRe: /^[a-z_][a-z0-9_]*$/i,
305
306 /**
307 * Called by `getInherited` to initialize the inheritedState the first time it is
308 * requested.
309 * @protected
310 */
311 initInheritedState: function (inheritedState) {
312 var me = this,
313 reference = me.getReference(),
314 controller = me.getController(),
315 // Don't instantiate the view model here, we only need to know that
316 // it exists
317 viewModel = me.getConfig('viewModel', true),
318 session = me.getConfig('session', true),
319 defaultListenerScope = me.getDefaultListenerScope();
320
321 if (controller) {
322 inheritedState.controller = controller;
323 }
324
325 if (defaultListenerScope) {
326 inheritedState.defaultListenerScope = me;
327 } else if (controller) {
328 inheritedState.defaultListenerScope = controller;
329 }
330
331 if (viewModel) {
332 // If we're not configured with an instance, just stamp the current component as
333 // the thing that holds the view model. When we ask to get the inherited view model,
334 // we will know that it's not an instance yet so we need to spin it up on this component.
335 // We need to initialize them from top-down, but we don't want to do it up front.
336 if (!viewModel.isViewModel) {
337 viewModel = me;
338 }
339 inheritedState.viewModel = viewModel;
340 }
341
342 // Same checks as the view model
343 if (session) {
344 if (!session.isSession) {
345 session = me;
346 }
347 inheritedState.session = session;
348 }
349
350 if (reference) {
351 me.referenceKey = (inheritedState.referencePath || '') + reference;
352 me.viewModelKey = (inheritedState.viewModelPath || '') + reference;
353 }
354 },
355
356 /**
357 * Gets the controller that controls this view. May be a controller that belongs
358 * to a view higher in the hierarchy.
359 *
360 * @param {Boolean} [skipThis=false] `true` to not consider the controller directly attached
361 * to this view (if it exists).
362 * @return {Ext.app.ViewController} The controller. `null` if no controller is found.
363 *
364 * @since 5.0.1
365 */
366 lookupController: function(skipThis) {
367 return this.getInheritedConfig('controller', skipThis) || null;
368 },
369
370 /**
371 * Returns the `Ext.data.Session` for this instance. This property may come
372 * from this instance's `{@link #session}` or be inherited from this object's parent.
373 * @param {Boolean} [skipThis=false] Pass `true` to ignore a `session` configured on
374 * this instance and only consider an inherited session.
375 * @return {Ext.data.Session}
376 * @since 5.0.0
377 */
378 lookupSession: function (skipThis) {
379 // See lookupViewModel
380 var ret = skipThis ? null : this.getSession(); // may be the initGetter!
381 if (!ret) {
382 ret = this.getInheritedConfig('session', skipThis);
383 if (ret && !ret.isSession) {
384 ret = ret.getInherited().session = ret.getSession();
385 }
386 }
387
388 return ret || null;
389 },
390
391 /**
392 * Returns the `Ext.app.ViewModel` for this instance. This property may come from this
393 * this instance's `{@link #viewModel}` or be inherited from this object's parent.
394 * @param {Boolean} [skipThis=false] Pass `true` to ignore a `viewModel` configured on
395 * this instance and only consider an inherited view model.
396 * @return {Ext.app.ViewModel}
397 * @since 5.0.0
398 */
399 lookupViewModel: function (skipThis) {
400 var ret = skipThis ? null : this.getViewModel(); // may be the initGetter!
401
402 if (!ret) {
403 ret = this.getInheritedConfig('viewModel', skipThis);
404 // If what we get back is a component, it means the component was configured
405 // with a view model, however the construction of it has been delayed until
406 // we need it. As such, go and construct it and store it on the inherited state.
407 if (ret && !ret.isViewModel) {
408 ret = ret.getInherited().viewModel = ret.getViewModel();
409 }
410 }
411
412 return ret || null;
413 },
414
415 /**
416 * Publish this components state to the `ViewModel`. If no arguments are given (or if
417 * this is the first call), the entire state is published. This state is determined by
418 * the `publishes` property.
419 *
420 * This method is called only by component authors.
421 *
422 * @param {String} [property] The name of the property to update.
423 * @param {Object} [value] The value of `property`. Only needed if `property` is given.
424 * @protected
425 * @since 5.0.0
426 */
427 publishState: function (property, value) {
428 var me = this,
429 state = me.publishedState,
430 binds = me.getBind(),
431 binding = binds && property && binds[property],
432 count = 0,
433 name, publishes, vm, path;
434
435 if (binding && !binding.syncing && !binding.isReadOnly()) {
436 // If the binding has never fired & our value is either:
437 // a) undefined
438 // b) null
439 // c) The value we were initially configured with
440 // Then we don't want to publish it back to the view model. If we do, we'll be
441 // overwriting whatever is in the viewmodel and it will never have a chance to fire.
442 if (!(binding.calls === 0 && (value == null || value === me.getInitialConfig()[property]))) {
443 binding.setValue(value);
444 }
445 }
446
447 if (!(publishes = me.getPublishes())) {
448 return;
449 }
450
451 if (!(vm = me.lookupViewModel())) {
452 return;
453 }
454
455 // Important to access path after lookupViewModel, which will kick off
456 // our inheritedState if we don't have one
457 if (!(path = me.viewModelKey)) {
458 return;
459 }
460
461 if (property && state) {
462 if (!publishes[property]) {
463 return;
464 }
465
466 // If we are setting an individual property and that is not a {} or a [] then
467 // check to see if it is unchanged.
468 if (!(value && value.constructor === Object) && !(value instanceof Array)) {
469 if (state[property] === value) {
470 return;
471 }
472 }
473 path += '.';
474 path += property;
475 } else {
476 state = state || (me.publishedState = {});
477
478 for (name in publishes) {
479 ++count;
480 // If there are no properties to publish this loop will not run and the
481 // value = null above will remain.
482 if (name === property) {
483 state[name] = value;
484 } else {
485 state[name] = me[name];
486 }
487 }
488
489 if (!count) { // if (no properties were put in "state")
490 return;
491 }
492 value = state;
493 }
494
495 vm.set(path, value);
496 },
497
498 //=========================================================================
499
500 privates: {
501 /**
502 * Ensures that the given property (if it is a Config System config) has a proper
503 * "updater" method on this instance to sync changes to the config.
504 * @param {String} property The name of the config property.
505 * @private
506 * @since 5.0.0
507 */
508 addBindableUpdater: function (property) {
509 var me = this,
510 configs = me.self.$config.configs,
511 cfg = configs[property],
512 updateName;
513
514 // While we store the updater on this instance, the function is cached and
515 // re-used across all instances.
516 if (cfg && !me.hasOwnProperty(updateName = cfg.names.update)) {
517 me[updateName] = cfg.bindableUpdater ||
518 (cfg.root.bindableUpdater = me.makeBindableUpdater(cfg));
519 }
520 },
521
522 /**
523 * @param {String/Object} binds
524 * @param {Object} currentBindings
525 * @return {Object}
526 * @private
527 * @since 5.0.0
528 */
529 applyBind: function (binds, currentBindings) {
530 if (!binds) {
531 return binds;
532 }
533
534 var me = this,
535 viewModel = me.lookupViewModel(),
536 twoWayable = me.getTwoWayBindable(),
537 getBindTemplateScope = me._getBindTemplateScope,
538 b, property, descriptor;
539
540 if (!currentBindings || typeof currentBindings === 'string') {
541 currentBindings = {};
542 }
543
544 //<debug>
545 if (!viewModel) {
546 Ext.raise('Cannot use bind config without a viewModel');
547 }
548 //</debug>
549
550 if (Ext.isString(binds)) {
551 //<debug>
552 if (!me.defaultBindProperty) {
553 Ext.raise(me.$className + ' has no defaultBindProperty - '+
554 'Please specify a bind object');
555 }
556 //</debug>
557
558 b = binds;
559 binds = {};
560 binds[me.defaultBindProperty] = b;
561 }
562
563 for (property in binds) {
564 descriptor = binds[property];
565 b = currentBindings[property];
566
567 if (b && typeof b !== 'string') {
568 b.destroy();
569 b = null;
570 }
571
572 if (descriptor) {
573 b = viewModel.bind(descriptor, me.onBindNotify, me);
574 b._config = Ext.Config.get(property);
575 b.getTemplateScope = getBindTemplateScope;
576
577 //<debug>
578 if (!me[b._config.names.set]) {
579 Ext.raise('Cannot bind ' + property + ' on ' + me.$className +
580 ' - missing a ' + b._config.names.set + ' method.');
581 }
582 //</debug>
583 }
584
585 currentBindings[property] = b;
586 if (twoWayable && twoWayable[property] && !b.isReadOnly()) {
587 me.addBindableUpdater(property);
588 }
589 }
590
591 return currentBindings;
592 },
593
594 applyController: function (controller) {
595 if (controller) {
596 controller = Ext.Factory.controller(controller);
597 controller.setView(this);
598 }
599 return controller;
600 },
601
602 applyPublishes: function (all) {
603 if (this.lookupViewModel()) {
604 for (var property in all) {
605 this.addBindableUpdater(property);
606 }
607 }
608
609 return all;
610 },
611
612 //<debug>
613 applyReference: function (reference) {
614 var validIdRe = this.validRefRe || Ext.validIdRe;
615 if (reference && !validIdRe.test(reference)) {
616 Ext.raise('Invalid reference "' + reference + '" for ' + this.getId() +
617 ' - not a valid identifier');
618 }
619 return reference;
620 },
621 //</debug>
622
623 /**
624 * Transforms a Session config to a proper instance.
625 * @param {Object} session
626 * @return {Ext.data.Session}
627 * @private
628 * @since 5.0.0
629 */
630 applySession: function (session) {
631 if (!session) {
632 return null;
633 }
634
635 if (!session.isSession) {
636 var parentSession = this.lookupSession(true), // skip this component
637 config = (session === true) ? {} : session;
638
639 if (parentSession) {
640 session = parentSession.spawn(config);
641 } else {
642 // Mask this use of Session from Cmd - the dependency is not ours but
643 // the caller
644 session = new Ext.data['Session'](config);
645 }
646 }
647
648 return session;
649 },
650
651 /**
652 * Transforms a ViewModel config to a proper instance.
653 * @param {String/Object/Ext.app.ViewModel} viewModel
654 * @return {Ext.app.ViewModel}
655 * @private
656 * @since 5.0.0
657 */
658 applyViewModel: function (viewModel) {
659 var me = this,
660 config, session;
661
662 if (!viewModel) {
663 return null;
664 }
665
666 if (!viewModel.isViewModel) {
667 config = {
668 parent: me.lookupViewModel(true) // skip this component
669 };
670
671 config.session = me.getSession();
672 if (!session && !config.parent) {
673 config.session = me.lookupSession();
674 }
675
676 if (viewModel) {
677 if (viewModel.constructor === Object) {
678 Ext.apply(config, viewModel);
679 } else if (typeof viewModel === 'string') {
680 config.type = viewModel;
681 }
682 }
683
684 viewModel = Ext.Factory.viewModel(config);
685 }
686 return viewModel;
687 },
688
689 _getBindTemplateScope: function () {
690 // This method is called as a method on a Binding instance, so the "this" pointer
691 // is that of the Binding. The "scope" of the Binding is the component owning it.
692 return this.scope.resolveListenerScope();
693 },
694
695 destroyBindable: function() {
696 var me = this,
697 viewModel = me.getConfig('viewModel', true),
698 session = me.getConfig('session', true),
699 controller = me.getController();
700
701 if (viewModel && viewModel.isViewModel) {
702 viewModel.destroy();
703 me.setViewModel(null);
704 }
705
706 if (session && session.isSession) {
707 if (session.getAutoDestroy()) {
708 session.destroy();
709 }
710 me.setSession(null);
711 }
712 if (controller) {
713 me.setController(null);
714 controller.destroy();
715 }
716 },
717
718 /**
719 * This method triggers the lazy configs and must be called when it is time to
720 * fully boot up. The configs that must be initialized are: `bind`, `publishes`,
721 * `session`, `twoWayBindable` and `viewModel`.
722 * @private
723 * @since 5.0.0
724 */
725 initBindable: function () {
726 this.initBindable = Ext.emptyFn;
727 this.getBind();
728 this.getPublishes();
729
730 // If we have binds, the applyBind method will call getTwoWayBindable to ensure
731 // we have the necessary updaters. If we have no binds then applyBind will not
732 // be called and we will ignore our twoWayBindable config (which is fine).
733 //
734 // If we have publishes or binds then the viewModel will be requested. If not
735 // this viewModel will be lazily requested by a descendant via inheritedState
736 // or not at all. If there is no descendant using bind or publishes, then the
737 // viewModel will sit and wait.
738 //
739 // As goes the fate of the viewModel so goes the fate of the session. If we
740 // have requested the viewModel then the session will also be spun up. If not,
741 // we wait for a descendant or the user to request them.
742 },
743
744 /**
745 * Returns an `update` method for the given Config that will call `{@link #publishState}`
746 * to ensure two-way bindings (via `bind`) as well as any `publishes` are updated.
747 * This method is cached on the `cfg` instance for re-use.
748 * @param {Ext.Config} cfg
749 * @return {Function} The updater function.
750 * @private
751 * @since 5.0.0
752 */
753 makeBindableUpdater: function (cfg) {
754 var updateName = cfg.names.update;
755
756 return function (newValue, oldValue) {
757 var me = this,
758 updater = me.self.prototype[updateName];
759
760 if (updater) {
761 updater.call(me, newValue, oldValue);
762 }
763 me.publishState(cfg.name, newValue);
764 };
765 },
766
767 /**
768 * Checks if a particular binding is synchronizing the value.
769 * @param {String} name The name of the property being bound to.
770 * @return {Boolean} `true` if the binding is syncing.
771 *
772 * @protected
773 */
774 isSyncing: function(name) {
775 var bindings = this.getBind(),
776 ret = false,
777 binding;
778
779 if (bindings) {
780 binding = bindings[name];
781 if (binding) {
782 ret = binding.syncing > 0;
783 }
784 }
785 return ret;
786 },
787
788 onBindNotify: function (value, oldValue, binding) {
789 binding.syncing = (binding.syncing + 1) || 1;
790 this[binding._config.names.set](value);
791 --binding.syncing;
792 },
793
794 removeBindings: function() {
795 var me = this,
796 bindings, key, binding;
797
798 if (!me.destroying) {
799 bindings = me.getBind();
800 if (bindings && typeof bindings !== 'string') {
801 for (key in bindings) {
802 binding = bindings[key];
803 binding.destroy();
804 binding._config = binding.getTemplateScope = null;
805 }
806 }
807 }
808 me.setBind(null);
809 },
810
811 /**
812 * Updates the session config.
813 * @param {Ext.data.Session} session
814 * @private
815 */
816 updateSession: function (session) {
817 var state = this.getInherited();
818
819 if (session) {
820 state.session = session;
821 } else {
822 delete state.session;
823 }
824 },
825
826 /**
827 * Updates the viewModel config.
828 * @param {Ext.app.ViewModel} viewModel
829 * @param {Ext.app.ViewModel} oldViewModel
830 * @private
831 */
832 updateViewModel: function (viewModel) {
833 var state = this.getInherited(),
834 controller = this.getController();
835
836 if (viewModel) {
837 state.viewModel = viewModel;
838 viewModel.setView(this);
839 if (controller) {
840 controller.initViewModel(viewModel);
841 }
842 } else {
843 delete state.viewModel;
844 }
845 }
846 } // private
847 });