.
Data binding and the ViewModel that powers it are powerful additions to Ext JS.
Together they enable you to do more with less code and write in a much more declarative style while helping you maintain a clean separation of concerns.
A ViewModel is a class that manages a data object. It then allows those interested in this data to bind to it and be notified when.
Components have a
bind config that allows them to associate many of their configs to
data from the ViewModel. Using bind, you can be sure that the appropriate component
config will have its setter method called whenever the bound value changes - no custom
event handlers needed.
In this guide, we will walk through some examples that show the power of ViewModels and Data Binding.
Probably the best way to understand binding and ViewModels is to look at the various ways you can use bindings on components. This is because components are the primary consumers of data binding and components are something familiar to Ext JS developers. In order for binding to work, however, we do need a ViewModel so we will reference one for now and define it later.
Binding for components is the process of connecting data from a
Ext.app.ViewModel to a component's config properties. Any configuration a
component possesses may be bound to, so long as it has a setter method. For instance,
since there's a
setTitle() method on Ext.panel.Panel, you can bind to the
title configuration.
In this example, we will set the
width of a panel based on the results of our
ViewModel's
data. We can bind our data to
width since
setWidth() is a method that
Ext.panel.Panel may use.
Ext.create('Ext.panel.Panel', { title: 'Simple Form', viewModel: { type: 'test' // we will define the "test" ViewModel soon }, bind: { html: '<p>Hello {name}</p>', width: '{someWidth}' } });
The syntax used for bind values is very similar to Ext.Template. You can put
text around tokens inside braces. You can use formatters as well, as with
Ext.Template. Unlike
Ext.Template, however, when a template is a single token (like
'{someWidth}') then its value is passed unmodified. That is, it is not converted to a
string.
We will see later how the data for
name and
someWidth are defined. The above example
simply shows how the data is consumed by a component.
Many configs you will want to bind are boolean values, such as visible (or
hidden),
disabled,
checked, and
pressed. Bind templates support boolean negation "inline"
in the template. Other forms of algebra are relegated to formulas (see below), but
boolean inversion is common enough there is special provision for it. For example:
Ext.create('Ext.panel.Panel', { title: 'Simple Form', viewModel: { type: 'test' }, items: [{ xtype: 'button', bind: { hidden: '{!name}' // negated } }] });
This also highlights how values in single token templates are not converted to strings.
In the above, while "name" is a string value, it is negated using "!" and becomes a
boolean value and that is passed to the
setHidden method on the button.
Bound config properties will always overwrite configurations set statically on the component as soon as the bound result is available. In other words, bound data will always take priority over static configuration values but may be delayed in order to fetch that data.
Ext.create('Ext.panel.Panel', { title: 'Simple Form', viewModel: { type: 'test' }, bind: { title: 'Hello {name}' } });
Once the binding for "name" is delivered, the "Simple Form" title will be replaced.
One of the most helpful parts of binding is that all of the children of the component with a viewModel also have access to their container's data.
In this example, you can see the children items of a form may be bound to their container's viewModel.
Ext.create('Ext.panel.Panel', { title: 'Simple Form', viewModel: { type: 'test' }, layout: 'form', defaultType: 'textfield', items: [{ fieldLabel: 'First Name', bind: '{firstName}' // uses "test" ViewModel from parent },{ fieldLabel: 'Last Name', bind: '{lastName}' }] });
The bind config also allows for two-way data binding, which translates to live
synchronization of data between the view and the model. Any data change made in the
view is automatically written back to the model. This automatically updates any other
components that may be bound to that same data.
Note: Not all configs will publish their value to the ViewModel when changed.
Configs defined within the
publish and
twoWayBindable array will publish changes
back up to the ViewModel. Values may also be published in Component / application logic
using the
publishState method.
In the above example, because the "firstName" and "lastName" properties were bound to text fields, changes in the input would be written back to the ViewModel. To see how all this connects, it is time to complete the example and define the ViewModel.
Ext.define('MyApp.view.TestViewModel', { extend: 'Ext.app.ViewModel', alias: 'viewmodel.test', // connects to viewModel/type below data: { firstName: 'John', lastName: 'Doe' }, formulas: { // We'll explain formulas in more detail soon. name: function (get) { var fn = get('firstName'), ln = get('lastName'); return (fn && ln) ? (fn + ' ' + ln) : (fn || ln || ''); } } }); Ext.define('MyApp.view.TestView', { extend: 'Ext.panel.Panel', layout: 'form', // Always use this form when defining a view class. This // allows the creator of the component to pass data without // erasing the ViewModel type that we want. viewModel: { type: 'test' // references alias "viewmodel.test" }, bind: { title: 'Hello {name}' }, defaultType: 'textfield', items: [{ fieldLabel: 'First Name', bind: '{firstName}' },{ fieldLabel: 'Last Name', bind: '{lastName}' },{ xtype: 'button', text: 'Submit', bind: { hidden: '{!name}' } }] }); Ext.onReady(function () { Ext.create('MyApp.view.TestView', { renderTo: Ext.getBody(), width: 400 }); });
When the above panel is displayed we can see that changes in the text fields are
reflected in the panel
title as well as the
hidden state of the "Submit" button.
Sometimes a component's state, e.g. the
checked state of a checkbox or the selected
record of a grid, is interesting to other components. When a component is assigned a
reference to identify it, that component will publish some of its key properties in
the ViewModel.
In this example, we have the "Admin Key" textfield's disabled config bound to the the checked state of the checkbox. This results in the textfield being disabled until the checkbox is checked. This sort of behavior is well suited for dynamic forms like this:
Ext.create('Ext.panel.Panel', { title: 'Sign Up Form', viewModel: { type: 'test' }, items: [{ xtype: 'checkbox', boxLabel: 'Is Admin', reference: 'isAdmin' },{ xtype: 'textfield', fieldLabel: 'Admin Key', bind: { disabled: '{!isAdmin.checked}' } }] });
So far we've seen three basic forms of bind descriptors:
{firstName} - A "direct bind" to some value in the ViewModel. This value is passed
through unmodified and so may arrive as any type of data.
Hello {name} - A "bind template" always produces a string by inserting the textual
value of the various bind expressions. Bind templates can also use formatters as with a
normal
Ext.Template, for example: 'Hello {name:capitalize}'.
{!isAdmin.checked} - The negated form of a direct bind, useful for binding to
boolean config properties.
Beyond these basic forms, there are a few specialized forms of bind descriptors that you can use.
If an object or array is given as a bind descriptor, the ViewModel will produce an object or array of the same shape but with the various properties replaced by their bind result. For example:
Ext.create('Ext.Component', { bind: { data: { fname: '{firstName}', lname: '{lastName}' } } });
This sets the "data" config for the component to be an object with two properties whose values are set from the ViewModel.
When a particular record is desired, say "User" with
id of 42, the bind descriptor is
an object but with a "reference" property. For example:
Ext.create('Ext.Component', { bind: { data: { reference: 'User', id: 42 } } });
In this case, the component's
tpl will receive the User record once it is loaded. This
currently requires the use of an
Ext.data.Session.
Similar to a record bind, one can also bind to an association, say the User's Address record:
Ext.create('Ext.Component', { bind: { data: { reference: 'User', id: 42, association: 'address' } } });
In this case, the component's
tpl will receive the User's "address" record once it is
loaded. This also currently requires the use of an
Ext.data.Session.
The final form of bind descriptor is used when you need to describe binding options. The following example shows how to receive only one value for a binding and then disconnect automatically.
Ext.create('Ext.Component', { bind: { data: { bindTo: '{name}', single: true } } });
The
bindTo property is the second reserved name in a bind descriptor object (the first
being
reference). When present, it signifies that the value of
bindTo is the actual
bind descriptor and the other properties are configuration options for the binding.
The other bind option currently supported is
deep. This option is used when binding to
an object so that the binding will be notified when any property of that object changes,
not just the reference itself. This would be most useful when binding to the
data
config of a component since these often receive objects.
Ext.create('Ext.Component', { bind: { data: { bindTo: '{someObject}', deep: true } } });
Now that we've gotten a taste for how components use ViewModels and gotten a glimpse of what ViewModels look like, it is time to learn more about ViewModels and what they offer.
As stated previously, a ViewModel is a manager for an underlying
data object. It is
the content of that object that is being consumed by bind statements. The inheritance of
data from a parent ViewModel to its child ViewModels leverages the JavaScript prototype
chain. This is covered in more detail in the
View Model Internals guide, but in a nutshell a child
ViewModel's
data object has the
data object of its parent ViewModel as its
prototype.
In addition to holding data and providing binding, ViewModels also offer a convenient
way to calculate data from other data called
formulas. Formulas allow you to
encapsulate data dependencies in the ViewModel and keep your views free to focus on
declaring their structure.
In other words, the data is not changed in the ViewModel's data, but can be displayed
differently by transforming it using formulas. This is similar to how the
convert
configuration works for fields of a traditional data model.
We saw a simple "name" formula in the previous example. In that case, the "name" formula was simply a function that combined two other values from the ViewModel: "firstName" and "lastName".
Formulas can also use the results of other formulas as if the result were just another data property. For example:
Ext.define('MyApp.view.TestViewModel2', { extend: 'Ext.app.ViewModel', alias: 'viewmodel.test2', formulas: { x2y: function (get) { return get('x2') * get('y'); }, x2: function (get) { return get('x') * 2; } } });
The "x2" formulas uses an "x" property to define "x2" as "x * 2". The "x2y" formulas uses both "x2" and "y". This definition means that if "x" changes, "x2" is recalculated and then "x2y". But if "y" changes, only "x2y" needs to be recalculated.
In the above examples, the dependencies for the formula were found via inspecting the function, however this isn't always the best solution. An explicit bind statement can be used, which will return a simple object when all the values in the bind have presented.
Ext.define('MyApp.view.TestViewModel2', { extend: 'Ext.app.ViewModel', alias: 'viewmodel.test2', formulas: { something: { bind: { x: '{foo.bar.x}', y: '{bar.foo.thing.zip.y}' }, get: function (data) { return data.x + data.y; } } } });
When a formula is invertible we can also define a
set method to be called when the
value is set (say via two-way binding). Since the "this" pointer is the ViewModel, the
set method can call
this.set() to set the appropriate properties in the ViewModel.
A revised version of the TestViewModel below shows how "name" can be defined as a two-way formula.
Ext.define('MyApp.view.TestViewModel', { extend: 'Ext.app.ViewModel', alias: 'viewmodel.test', formulas: { name: { get: function (get) { var fn = get('firstName'), ln = get('lastName'); return (fn && ln) ? (fn + ' ' + ln) : (fn || ln || ''); }, set: function (value) { var space = value.indexOf(' '), split = (space < 0) ? value.length : space; this.set({ firstName: value.substring(0, split), lastName: value.substring(split + 1) }); } } } });
With all the power of ViewModels, formulas, and data binding, it can be easy to overuse or abuse these mechanisms and create an application that is hard to understand or debug, or is slow to update or leaks memory. To help avoid these issues and still get the most out of ViewModels here are some recommended techniques:
Always use the following form when configuring your viewModel. This is important because of the way the Config System merges config values. With this form, the "type" property is preserved during the merge.
Ext.define('MyApp.view.TestView', { //... viewModel: { type: 'test' }, });
Make names obvious, especially in high level ViewModels. In JavaScript we rely on textual searching so pick names that make finding usages possible. The more code that might use a property the more important it is to pick meaningful or even unique names.
Don't nest data in objects more deeply than necessary. Multiple top-level objects stored in the ViewModel will require less bookkeeping compared to one object with lots of nested sub-objects. Further, this will help make dependencies on this information more obvious than if many components depend on some large containing object. There are reasons to share objects, but remember the ViewModel is just a managed object so you can use its properties too.
Use child ViewModels to allow data to be cleaned up with the components that needed it. If you put all data in high-level ViewModels, that data will likely never be removed even when the child views that needed it have been destroyed. Instead, create ViewModels for the child view and pass data into its ViewModel.
Don't create child ViewModels unless they are actually needed. Each ViewModel instance takes time to create and memory to manage. If a child view does not need data unique to itself, it can simply use the ViewModel it inherits from its container. See the previous recommendation though, because it is better to create child ViewModels when they are needed than pollute a parent ViewModel and effectively leak memory.
Use formulas instead of repeating binds. If you followed how formulas are ways to
combine the results of several bound values you can probably see how using a formula
would help reduce the number of dependencies compared to directly using those same
values in many places. For example, one formula with 3 dependencies and 4 users make
3 + 4 = 7 dependencies to track in the ViewModel. Compared to 4 users with those 3
dependencies on themselves we would have
3 * 4 = 12 dependencies. Fewer dependencies
to track means less memory used and time to process them.
Don't chain formulas too deeply. This is not so much a run-time cost issue as a code clarity issue. Chained formulas can mask the connections between data and components making it hard to understand what is happening.
Two-way formulas must be stable. Say formula "foo" is a calculation of value "bar". When "foo" is set it will set "bar" by inverting the formula from its get method. The result is stable if the get method will produce the exact same value for "foo" that is now being set. If it does not, the process will cycle until it reaches a stable point or it will continue indefinitely. Neither result is likely desirable.
For more information about the viewModels, please take a few minutes to check out our ViewModel Internals guide.