View Models & Data Binding

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 it changes. The ViewModel, like ViewController, is owned by the view that references it. Because ViewModels are associated with a view, they are also able to link to a parent ViewModel owned by ancestor components in the component hierarchy. This allows child views to simply “inherit” the data of their parent ViewModel.

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.

Component 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 and Configs

Binding for components is the process of connecting data from an 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.

  1. Ext.create('Ext.panel.Panel', {
  2. title: 'Simple Form',
  3. viewModel: {
  4. type: 'test' // we will define the "test" ViewModel soon
  5. },
  6. bind: {
  7. html: '<p>Hello {name}</p>',
  8. width: '{someWidth}'
  9. }
  10. });

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.

Binding Boolean Configs

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:

  1. Ext.create('Ext.panel.Panel', {
  2. title: 'Simple Form',
  3. viewModel: {
  4. type: 'test'
  5. },
  6. items: [{
  7. xtype: 'button',
  8. bind: {
  9. hidden: '{!name}' // negated
  10. }
  11. }]
  12. });

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.

Binding and Priority

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.

  1. Ext.create('Ext.panel.Panel', {
  2. title: 'Simple Form',
  3. viewModel: {
  4. type: 'test'
  5. },
  6. bind: {
  7. title: 'Hello {name}'
  8. }
  9. });

Once the binding for “name” is delivered, the “Simple Form” title will be replaced.

Binding and Child Components

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.

  1. Ext.create('Ext.panel.Panel', {
  2. title: 'Simple Form',
  3. viewModel: {
  4. type: 'test'
  5. },
  6. layout: 'form',
  7. defaultType: 'textfield',
  8. items: [{
  9. fieldLabel: 'First Name',
  10. bind: '{firstName}' // uses "test" ViewModel from parent
  11. },{
  12. fieldLabel: 'Last Name',
  13. bind: '{lastName}'
  14. }]
  15. });

Two Way Binding

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.

  1. Ext.define('MyApp.view.TestViewModel', {
  2. extend: 'Ext.app.ViewModel',
  3. alias: 'viewmodel.test', // connects to viewModel/type below
  4. data: {
  5. firstName: 'John',
  6. lastName: 'Doe'
  7. },
  8. formulas: {
  9. // We'll explain formulas in more detail soon.
  10. name: function (get) {
  11. var fn = get('firstName'), ln = get('lastName');
  12. return (fn && ln) ? (fn + ' ' + ln) : (fn || ln || '');
  13. }
  14. }
  15. });
  16. Ext.define('MyApp.view.TestView', {
  17. extend: 'Ext.panel.Panel',
  18. layout: 'form',
  19. // Always use this form when defining a view class. This
  20. // allows the creator of the component to pass data without
  21. // erasing the ViewModel type that we want.
  22. viewModel: {
  23. type: 'test' // references alias "viewmodel.test"
  24. },
  25. bind: {
  26. title: 'Hello {name}'
  27. },
  28. defaultType: 'textfield',
  29. items: [{
  30. fieldLabel: 'First Name',
  31. bind: '{firstName}'
  32. },{
  33. fieldLabel: 'Last Name',
  34. bind: '{lastName}'
  35. },{
  36. xtype: 'button',
  37. text: 'Submit',
  38. bind: {
  39. hidden: '{!name}'
  40. }
  41. }]
  42. });
  43. Ext.onReady(function () {
  44. Ext.create('MyApp.view.TestView', {
  45. renderTo: Ext.getBody(),
  46. width: 400
  47. });
  48. });

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.

Binding and Component State

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:

  1. Ext.create('Ext.panel.Panel', {
  2. title: 'Sign Up Form',
  3. viewModel: {
  4. type: 'test'
  5. },
  6. items: [{
  7. xtype: 'checkbox',
  8. boxLabel: 'Is Admin',
  9. reference: 'isAdmin'
  10. },{
  11. xtype: 'textfield',
  12. fieldLabel: 'Admin Key',
  13. bind: {
  14. disabled: '{!isAdmin.checked}'
  15. }
  16. }]
  17. });

Bind Descriptors

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.

Multi-Bind

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:

  1. Ext.create('Ext.Component', {
  2. bind: {
  3. data: {
  4. fname: '{firstName}',
  5. lname: '{lastName}'
  6. }
  7. }
  8. });

This sets the “data” config for the component to be an object with two properties whose values are set from the ViewModel.

Record Bind

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:

  1. Ext.create('Ext.Component', {
  2. bind: {
  3. data: {
  4. reference: 'User',
  5. id: 42
  6. }
  7. }
  8. });

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.

Association Bind

Similar to a record bind, one can also bind to an association, say the User’s Address record:

  1. Ext.create('Ext.Component', {
  2. bind: {
  3. data: {
  4. reference: 'User',
  5. id: 42,
  6. association: 'address'
  7. }
  8. }
  9. });

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.

Bind Options

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.

  1. Ext.create('Ext.Component', {
  2. bind: {
  3. data: {
  4. bindTo: '{name}',
  5. single: true
  6. }
  7. }
  8. });

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.

  1. Ext.create('Ext.Component', {
  2. bind: {
  3. data: {
  4. bindTo: '{someObject}',
  5. deep: true
  6. }
  7. }
  8. });

Creating ViewModels

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.

Formulas

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:

  1. Ext.define('MyApp.view.TestViewModel2', {
  2. extend: 'Ext.app.ViewModel',
  3. alias: 'viewmodel.test2',
  4. formulas: {
  5. x2y: function (get) {
  6. return get('x2') * get('y');
  7. },
  8. x2: function (get) {
  9. return get('x') * 2;
  10. }
  11. }
  12. });

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.

Formulas With Explicit Binding

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.

  1. Ext.define('MyApp.view.TestViewModel2', {
  2. extend: 'Ext.app.ViewModel',
  3. alias: 'viewmodel.test2',
  4. formulas: {
  5. something: {
  6. bind: {
  7. x: '{foo.bar.x}',
  8. y: '{bar.foo.thing.zip.y}'
  9. },
  10. get: function (data) {
  11. return data.x + data.y;
  12. }
  13. }
  14. }
  15. });

Two-Way Formulas

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.

  1. Ext.define('MyApp.view.TestViewModel', {
  2. extend: 'Ext.app.ViewModel',
  3. alias: 'viewmodel.test',
  4. formulas: {
  5. name: {
  6. get: function (get) {
  7. var fn = get('firstName'), ln = get('lastName');
  8. return (fn && ln) ? (fn + ' ' + ln) : (fn || ln || '');
  9. },
  10. set: function (value) {
  11. var space = value.indexOf(' '),
  12. split = (space < 0) ? value.length : space;
  13. this.set({
  14. firstName: value.substring(0, split),
  15. lastName: value.substring(split + 1)
  16. });
  17. }
  18. }
  19. }
  20. });

Recommendations

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.

    1. Ext.define('MyApp.view.TestView', {
    2. //...
    3. viewModel: {
    4. type: 'test'
    5. },
    6. });
  • 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.

Further Reading

For more information about the viewModels, please take a few minutes to check out our ViewModel Internals guide.