]>
git.proxmox.com Git - extjs.git/blob - extjs/packages/core/src/mixin/Bindable.js
2 * This class is intended as a mixin for classes that want to provide a "bind" config that
3 * connects to a `ViewModel`.
7 Ext
.define('Ext.mixin.Bindable', {
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:
16 * var panel = Ext.create({
19 * title: 'Hello {user.name}'
23 * To dynamically add bindings:
26 * title: 'Greetings {user.name}!'
35 * The bind expressions are presented to `{@link Ext.app.ViewModel#bind}`. The
36 * `ViewModel` instance is determined by `lookupViewModel`.
43 // @cmd-auto-dependency { aliasPrefix: 'controller.' }
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:
49 * Ext.define('MyApp.UserController', {
50 * alias: 'controller.user'
53 * Ext.define('UserContainer', {
54 * extend: 'Ext.container.container',
58 * Ext.define('UserContainer', {
59 * extend: 'Ext.container.container',
66 * // Can also instance at runtime
67 * var ctrl = new MyApp.UserController();
68 * var view = new UserContainer({
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
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.
90 * See the introductory docs for {@link Ext.container.Container} for some sample
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
97 defaultListenerScope
: false,
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.
107 * **Note:** We'll discuss publishing properties **not** found in the config block below.
109 * Values determined to be invalid by component (often form fields and model validations)
110 * will not be published to the ViewModel.
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
116 * By using this config and `{@link #cfg-reference}` you can bind configs between
117 * components. For example:
121 * xtype: 'textfield',
122 * reference: 'somefield', // component's name in the ViewModel
123 * publishes: 'value' // value is not published by default
127 * xtype: 'displayfield',
128 * bind: 'You have entered "{somefield.value}"'
132 * Classes must provide this config as an Object:
134 * Ext.define('App.foo.Bar', {
141 * This is required for the config system to properly merge values from derived
144 * For instances this value can be specified as a value as show above or an array
145 * or object as follows:
148 * xtype: 'textfield',
149 * reference: 'somefield',
157 * // This achieves the same result as the above array form.
159 * xtype: 'textfield',
160 * reference: 'somefield',
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:
172 * setFieldLabel: function(fieldLabel) {
173 * this.callParent(arguments);
174 * this.publishState('fieldLabel', fieldLabel);
177 * With the above chunk of code, fieldLabel may now be published to the viewModel.
184 merge: function (newValue
, oldValue
) {
185 return this.mergeSets(newValue
, oldValue
);
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.
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
202 // @cmd-auto-dependency { directRef: 'Ext.data.Session' }
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.
208 * To create a new session you can specify `true`:
219 * Alternatively, a config object can be provided:
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}`.
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.
251 merge: function (newValue
, oldValue
) {
252 return this.mergeSets(newValue
, oldValue
);
256 // @cmd-auto-dependency { aliasPrefix: 'viewmodel.' }
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.
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.
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`.
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
287 * Is equivalent to this object form:
293 * The `defaultBindProperty` is set to "value" for form fields and to "store" for
297 defaultBindProperty
: null,
301 * Regular expression used for validating `reference` values.
304 validRefRe
: /^[a-z_][a-z0-9_]*$/i,
307 * Called by `getInherited` to initialize the inheritedState the first time it is
311 initInheritedState: function (inheritedState
) {
313 reference
= me
.getReference(),
314 controller
= me
.getController(),
315 // Don't instantiate the view model here, we only need to know that
317 viewModel
= me
.getConfig('viewModel', true),
318 session
= me
.getConfig('session', true),
319 defaultListenerScope
= me
.getDefaultListenerScope();
322 inheritedState
.controller
= controller
;
325 if (defaultListenerScope
) {
326 inheritedState
.defaultListenerScope
= me
;
327 } else if (controller
) {
328 inheritedState
.defaultListenerScope
= controller
;
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
) {
339 inheritedState
.viewModel
= viewModel
;
342 // Same checks as the view model
344 if (!session
.isSession
) {
347 inheritedState
.session
= session
;
351 me
.referenceKey
= (inheritedState
.referencePath
|| '') + reference
;
352 me
.viewModelKey
= (inheritedState
.viewModelPath
|| '') + reference
;
357 * Gets the controller that controls this view. May be a controller that belongs
358 * to a view higher in the hierarchy.
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.
366 lookupController: function(skipThis
) {
367 return this.getInheritedConfig('controller', skipThis
) || null;
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}
378 lookupSession: function (skipThis
) {
379 // See lookupViewModel
380 var ret
= skipThis
? null : this.getSession(); // may be the initGetter!
382 ret
= this.getInheritedConfig('session', skipThis
);
383 if (ret
&& !ret
.isSession
) {
384 ret
= ret
.getInherited().session
= ret
.getSession();
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}
399 lookupViewModel: function (skipThis
) {
400 var ret
= skipThis
? null : this.getViewModel(); // may be the initGetter!
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();
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.
420 * This method is called only by component authors.
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.
427 publishState: function (property
, value
) {
429 state
= me
.publishedState
,
430 binds
= me
.getBind(),
431 binding
= binds
&& property
&& binds
[property
],
433 name
, publishes
, vm
, path
;
435 if (binding
&& !binding
.syncing
&& !binding
.isReadOnly()) {
436 // If the binding has never fired & our value is either:
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
);
447 if (!(publishes
= me
.getPublishes())) {
451 if (!(vm
= me
.lookupViewModel())) {
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
)) {
461 if (property
&& state
) {
462 if (!publishes
[property
]) {
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
) {
476 state
= state
|| (me
.publishedState
= {});
478 for (name
in publishes
) {
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
) {
485 state
[name
] = me
[name
];
489 if (!count
) { // if (no properties were put in "state")
498 //=========================================================================
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.
508 addBindableUpdater: function (property
) {
510 configs
= me
.self
.$config
.configs
,
511 cfg
= configs
[property
],
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
));
523 * @param {String/Object} binds
524 * @param {Object} currentBindings
529 applyBind: function (binds
, currentBindings
) {
535 viewModel
= me
.lookupViewModel(),
536 twoWayable
= me
.getTwoWayBindable(),
537 getBindTemplateScope
= me
._getBindTemplateScope
,
538 b
, property
, descriptor
;
540 if (!currentBindings
|| typeof currentBindings
=== 'string') {
541 currentBindings
= {};
546 Ext
.raise('Cannot use bind config without a viewModel');
550 if (Ext
.isString(binds
)) {
552 if (!me
.defaultBindProperty
) {
553 Ext
.raise(me
.$className
+ ' has no defaultBindProperty - '+
554 'Please specify a bind object');
560 binds
[me
.defaultBindProperty
] = b
;
563 for (property
in binds
) {
564 descriptor
= binds
[property
];
565 b
= currentBindings
[property
];
567 if (b
&& typeof b
!== 'string') {
573 b
= viewModel
.bind(descriptor
, me
.onBindNotify
, me
);
574 b
._config
= Ext
.Config
.get(property
);
575 b
.getTemplateScope
= getBindTemplateScope
;
578 if (!me
[b
._config
.names
.set]) {
579 Ext
.raise('Cannot bind ' + property
+ ' on ' + me
.$className
+
580 ' - missing a ' + b
._config
.names
.set + ' method.');
585 currentBindings
[property
] = b
;
586 if (twoWayable
&& twoWayable
[property
] && !b
.isReadOnly()) {
587 me
.addBindableUpdater(property
);
591 return currentBindings
;
594 applyController: function (controller
) {
596 controller
= Ext
.Factory
.controller(controller
);
597 controller
.setView(this);
602 applyPublishes: function (all
) {
603 if (this.lookupViewModel()) {
604 for (var property
in all
) {
605 this.addBindableUpdater(property
);
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');
624 * Transforms a Session config to a proper instance.
625 * @param {Object} session
626 * @return {Ext.data.Session}
630 applySession: function (session
) {
635 if (!session
.isSession
) {
636 var parentSession
= this.lookupSession(true), // skip this component
637 config
= (session
=== true) ? {} : session
;
640 session
= parentSession
.spawn(config
);
642 // Mask this use of Session from Cmd - the dependency is not ours but
644 session
= new Ext
.data
['Session'](config
);
652 * Transforms a ViewModel config to a proper instance.
653 * @param {String/Object/Ext.app.ViewModel} viewModel
654 * @return {Ext.app.ViewModel}
658 applyViewModel: function (viewModel
) {
666 if (!viewModel
.isViewModel
) {
668 parent
: me
.lookupViewModel(true) // skip this component
671 config
.session
= me
.getSession();
672 if (!session
&& !config
.parent
) {
673 config
.session
= me
.lookupSession();
677 if (viewModel
.constructor === Object
) {
678 Ext
.apply(config
, viewModel
);
679 } else if (typeof viewModel
=== 'string') {
680 config
.type
= viewModel
;
684 viewModel
= Ext
.Factory
.viewModel(config
);
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();
695 destroyBindable: function() {
697 viewModel
= me
.getConfig('viewModel', true),
698 session
= me
.getConfig('session', true),
699 controller
= me
.getController();
701 if (viewModel
&& viewModel
.isViewModel
) {
703 me
.setViewModel(null);
706 if (session
&& session
.isSession
) {
707 if (session
.getAutoDestroy()) {
713 me
.setController(null);
714 controller
.destroy();
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`.
725 initBindable: function () {
726 this.initBindable
= Ext
.emptyFn
;
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).
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.
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.
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.
753 makeBindableUpdater: function (cfg
) {
754 var updateName
= cfg
.names
.update
;
756 return function (newValue
, oldValue
) {
758 updater
= me
.self
.prototype[updateName
];
761 updater
.call(me
, newValue
, oldValue
);
763 me
.publishState(cfg
.name
, newValue
);
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.
774 isSyncing: function(name
) {
775 var bindings
= this.getBind(),
780 binding
= bindings
[name
];
782 ret
= binding
.syncing
> 0;
788 onBindNotify: function (value
, oldValue
, binding
) {
789 binding
.syncing
= (binding
.syncing
+ 1) || 1;
790 this[binding
._config
.names
.set](value
);
794 removeBindings: function() {
796 bindings
, key
, binding
;
798 if (!me
.destroying
) {
799 bindings
= me
.getBind();
800 if (bindings
&& typeof bindings
!== 'string') {
801 for (key
in bindings
) {
802 binding
= bindings
[key
];
804 binding
._config
= binding
.getTemplateScope
= null;
812 * Updates the session config.
813 * @param {Ext.data.Session} session
816 updateSession: function (session
) {
817 var state
= this.getInherited();
820 state
.session
= session
;
822 delete state
.session
;
827 * Updates the viewModel config.
828 * @param {Ext.app.ViewModel} viewModel
829 * @param {Ext.app.ViewModel} oldViewModel
832 updateViewModel: function (viewModel
) {
833 var state
= this.getInherited(),
834 controller
= this.getController();
837 state
.viewModel
= viewModel
;
838 viewModel
.setView(this);
840 controller
.initViewModel(viewModel
);
843 delete state
.viewModel
;