├── LICENSE ├── src ├── extjs-5-602 │ └── Ext │ │ └── vmx │ │ ├── mixin │ │ └── Bindable.js │ │ └── app │ │ └── SplitViewModel.js └── extjs-620 │ └── Ext │ └── vmx │ ├── mixin │ └── Bindable.js │ └── app │ └── SplitViewModel.js └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 alexeysolonets 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/extjs-5-602/Ext/vmx/mixin/Bindable.js: -------------------------------------------------------------------------------- 1 | /* global Ext */ 2 | 3 | /** 4 | * This override allows binding to the component's own properties. 5 | * These properties should be declared in the 'publishes' config. 6 | */ 7 | Ext.define('Ext.vmx.mixin.Bindable', { 8 | initBindable: function () { 9 | var me = this; 10 | Ext.mixin.Bindable.prototype.initBindable.apply(me, arguments); 11 | me.publishInitialState(); 12 | }, 13 | 14 | /** 15 | Notifying both own and parent ViewModels about the state changes 16 | */ 17 | publishState: function (property, value) { 18 | var me = this, 19 | vm = me.lookupViewModel(), 20 | parentVm = me.lookupViewModel(true), 21 | path = me.viewModelKey; 22 | 23 | if (path && property && parentVm) { 24 | path += '.' + property; 25 | parentVm.set(path, value); 26 | } 27 | 28 | Ext.mixin.Bindable.prototype.publishState.apply(me, arguments); 29 | 30 | if (property && vm && vm.getView() == me) { 31 | vm.set(property, value); 32 | } 33 | }, 34 | 35 | /** 36 | Publish the initial state 37 | */ 38 | publishInitialState: function () { 39 | var me = this, 40 | state = me.publishedState || (me.publishedState = {}), 41 | publishes = me.getPublishes(), 42 | name; 43 | 44 | for (name in publishes) { 45 | if (state[name] === undefined) { 46 | me.publishState(name, me[name]); 47 | } 48 | } 49 | } 50 | }, function () { 51 | Ext.Array.each([Ext.Component, Ext.Widget], function (Class) { 52 | Class.prototype.initBindable = Ext.vmx.mixin.Bindable.prototype.initBindable; 53 | Class.prototype.publishState = Ext.vmx.mixin.Bindable.prototype.publishState; 54 | Class.mixin([Ext.vmx.mixin.Bindable]); 55 | }); 56 | }); -------------------------------------------------------------------------------- /src/extjs-620/Ext/vmx/mixin/Bindable.js: -------------------------------------------------------------------------------- 1 | /* global Ext */ 2 | 3 | /** 4 | * This override allows binding to the component's own properties. 5 | * These properties should be declared in the 'publishes' config. 6 | */ 7 | Ext.define('Ext.vmx.mixin.Bindable', { 8 | initBindable: function () { 9 | var me = this; 10 | Ext.mixin.Bindable.prototype.initBindable.apply(me, arguments); 11 | me.publishInitialState(); 12 | }, 13 | 14 | /** 15 | Notifying both own and parent ViewModels about the state changes 16 | */ 17 | publishState: function (property, value) { 18 | var me = this, 19 | vm = me.lookupViewModel(), 20 | parentVm = me.lookupViewModel(true), 21 | path = me.viewModelKey; 22 | 23 | if (path && property && parentVm) { 24 | path += '.' + property; 25 | parentVm.set(path, value); 26 | } 27 | 28 | Ext.mixin.Bindable.prototype.publishState.apply(me, arguments); 29 | 30 | if (property && vm && vm.getView() == me) { 31 | vm.set(property, value); 32 | } 33 | }, 34 | 35 | /** 36 | Publish the initial state 37 | */ 38 | publishInitialState: function () { 39 | var me = this, 40 | state = me.publishedState || (me.publishedState = {}), 41 | publishes = me.getPublishes(), 42 | name; 43 | 44 | for (name in publishes) { 45 | if (state[name] === undefined) { 46 | me.publishState(name, me[name]); 47 | } 48 | } 49 | } 50 | }, function () { 51 | Ext.Array.each([Ext.Component, Ext.Widget], function (Class) { 52 | Class.prototype.initBindable = Ext.vmx.mixin.Bindable.prototype.initBindable; 53 | Class.prototype.publishState = Ext.vmx.mixin.Bindable.prototype.publishState; 54 | Class.mixin([Ext.vmx.mixin.Bindable]); 55 | }); 56 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Extensions for custom components with a ViewModel. 2 | 3 | - Ext.vmx.mixin.Bindable 4 | - Ext.vmx.app.SplitViewModel 5 | 6 | # Ext.vmx.mixin.Bindable 7 | 8 | ## Outside binding 9 | Allows an outer component to bind to a component with own ViewModel. For instance, this is common when you extend from a Grid with a ViewModel inside. 10 | 11 | ```javascript 12 | Ext.define('MyGrid', { 13 | extend: 'Ext.grid.Panel', 14 | xtype: 'mygrid', 15 | 16 | viewModel: { 17 | 18 | } 19 | }); 20 | ``` 21 | 22 | Without the extension you aren't able to bind to it's selection from the outside ([EXTJS-15503](http://google.com/search?q=EXTJS-15503)): 23 | ```javascript 24 | Ext.define('MyPanel', { 25 | extend: 'Ext.panel.Panel', 26 | 27 | viewModel: { 28 | 29 | }, 30 | 31 | items: [{ 32 | xtype: 'mygrid', 33 | reference: 'mygrid' 34 | }, { 35 | xtype: 'textfield', 36 | fieldLabel: 'Selected record', 37 | /* 38 | Won't work because of the viewModel inside 'MyGrid'. 39 | Use 'Ext.vmx.mixin.Bindable' to fix. 40 | */ 41 | bind: '{mygrid.selection.name}' 42 | }] 43 | }); 44 | ``` 45 | 46 | 47 | ## Self binding 48 | Allows to make a bind between component's configuration properties and it's inner components: 49 | 50 | ```javascript 51 | Ext.define('MyGrid', { 52 | extend: 'Ext.grid.Panel', 53 | xtype: 'mygrid', 54 | 55 | viewModel: { 56 | 57 | }, 58 | 59 | config: { 60 | readOnly: false 61 | }, 62 | 63 | publishes: ['readOnly'], 64 | 65 | tbar: [{ 66 | text: 'Add', 67 | itemId: 'addButton', 68 | bind: { 69 | disabled: '{readOnly}' 70 | } 71 | }, { 72 | text: 'Remove', 73 | itemId: 'removeButton', 74 | bind: { 75 | disabled: '{readOnly}', 76 | text: 'Remove {selection.name}' 77 | } 78 | }] 79 | }); 80 | ``` 81 | 82 | ## Demo 83 | Full demo abailable at https://fiddle.sencha.com/#fiddle/1si5 84 | 85 | ## Limitations 86 | No known issues. Backward compatible. 87 | Keep in mind Sencha's recommendations: [Don't nest data objects more deeply than necessary](http://docs.sencha.com/extjs/6.2.1/guides/application_architecture/view_models_data_binding.html#application_architecture-_-view_models_data_binding_-_recommendations). 88 | 89 | ## Usage 90 | ```javascript 91 | Ext.application({ 92 | requires: [ 93 | 'Ext.vmx.mixin.Bindable' 94 | ] 95 | }); 96 | ``` 97 | 98 | # Ext.vmx.app.SplitViewModel 99 | 100 | Allows you to: 101 | - Define non-unique data field names among ViewModels 102 | - Reference to a parent ViewModel from a child 103 | 104 | This extension needs in a more detailed explanation. Imagine you have two components, one is nested into another. They both have ViewModels. And both of these ViewModels have a `color` data field. You want to use configuration properties to control component's state. How would you bind the inner component's `color` config to the outer component's `color`? If you try, you would see that the binding doesn't work as far as names are not unique. 105 | https://fiddle.sencha.com/#fiddle/1so5 106 | 107 | Seems we have to use different names for ViewModels' data fields. E.g. `innerColor` and `outerColor`. Obviously this is not convinient and doesn't guaranee uniqueness. 108 | 109 | So here is the `SplitViewModel` extension that internally gives unique names to the data fields of a ViewModel. As the result ViewModels never interfere. To complete our example we are going to to make an explicit back-reference binding as shown: 110 | ```javascript 111 | Ext.define('Fiddle.view.OuterContainer', { 112 | // ... 113 | 114 | viewModel: { 115 | name: 'outercontainer', 116 | data: { 117 | color: null 118 | } 119 | }, 120 | 121 | // ... 122 | 123 | items: [{ 124 | xtype: 'innercontainer', 125 | bind: { 126 | color: '{outercontainer.color}' 127 | } 128 | }] 129 | }); 130 | ``` 131 | 132 | 1. We gave a `name` to the outer ViewModel since it's anonymous (in the same file). Either way the `name` would be taken from the `alias` automatically, e.g. 133 | 134 | `alias: 'viewmodel.outercontainer'`. 135 | 136 | 2. We gave an explicit back-reference to the outer ViewModel: 137 | 138 | `color: '{outercontainer.color}'` 139 | 140 | ## Demo 141 | Try it https://fiddle.sencha.com/#fiddle/1so9 142 | 143 | ## Pros 144 | ViewModels are now isolated from each other. Developers are able to create independent views using both ViewControllers and ViewModels with no worrying about the ViewModels interference. With this extension binding becomes similar to a circuit boar wiring. Very predictable. 145 | 146 | ## Limitations 147 | If you had nested views both with ViewModels like in example above, then you must add an explicit back-reference to your bindings where necessary. Even if property names are unique. All you have to do is to prefix the binding expression with ViewModel's `type` or `name`. 148 | 149 | ## Usage 150 | ```javascript 151 | Ext.application({ 152 | requires: [ 153 | 'Ext.vmx.app.SplitViewModel' 154 | ] 155 | }); 156 | ``` 157 | 158 | # Combining extensions 159 | You are going to have the best results with both extensions included. Take a look at this example 160 | 161 | https://fiddle.sencha.com/#fiddle/1sod 162 | 163 | There are no handlers. Everything is done with a declaration syntax. Component's state is controlled by configuration properties but the internals are bound via ViewModel. This is great. I wish Sencha would have done this by default. 164 | -------------------------------------------------------------------------------- /src/extjs-620/Ext/vmx/app/SplitViewModel.js: -------------------------------------------------------------------------------- 1 | /* global Ext */ 2 | 3 | /** 4 | * This override encapsulates the ViewModel of a component. 5 | * This is achieved by giving unique names to the properties of the ViewModel. 6 | */ 7 | Ext.define('Ext.vmx.app.SplitViewModel', { 8 | override: 'Ext.app.ViewModel', 9 | 10 | config: { 11 | /** 12 | @cfg {String} 13 | ViewModel name 14 | */ 15 | name: undefined, 16 | 17 | /** 18 | @cfg {String} 19 | @private 20 | Identifier (name + sequential identifier) 21 | */ 22 | uniqueName: undefined, 23 | 24 | /** 25 | @cfg {String} 26 | @private 27 | uniqueName + '_' 28 | */ 29 | prefix: undefined 30 | }, 31 | 32 | uniqueNameRe: /_id_\d+/, 33 | 34 | privates: { 35 | applyData: function (newData, data) { 36 | newData = this.getPrefixedData(newData); 37 | data = this.getPrefixedData(data); 38 | 39 | return this.callParent([newData, data]); 40 | }, 41 | 42 | applyLinks: function (links) { 43 | links = this.getPrefixedData(links); 44 | return this.callParent([links]); 45 | }, 46 | 47 | applyFormulas: function (formulas) { 48 | formulas = this.getPrefixedData(formulas); 49 | return this.callParent([formulas]); 50 | }, 51 | 52 | bindExpression: function (path, callback, scope, options) { 53 | path = this.getPrefixedPath(path); 54 | return this.callParent([path, callback, scope, options]); 55 | } 56 | }, 57 | 58 | linkTo: function (key, reference) { 59 | key = this.getPrefixedPath(key); 60 | return this.callParent([key, reference]); 61 | }, 62 | 63 | get: function (path) { 64 | path = this.getPrefixedPath(path); 65 | return this.callParent([path]); 66 | }, 67 | 68 | set: function (path, value) { 69 | if (Ext.isString(path)) { 70 | path = this.getPrefixedPath(path); 71 | } 72 | else if (Ext.isObject(path)) { 73 | path = this.getPrefixedData(path); 74 | } 75 | this.callParent([path, value]); 76 | }, 77 | 78 | /** 79 | * The name is either a specified name 80 | * or a type of the ViewModel 81 | * or just 'viewmodel' (for anonymous ViewModels). 82 | */ 83 | applyName: function (name) { 84 | name = name || this.type || 'viewmodel'; 85 | return name; 86 | }, 87 | 88 | /** 89 | * Unique name is based on the Ext.id generator 90 | */ 91 | applyUniqueName: function (uniqueName) { 92 | uniqueName = uniqueName || Ext.id(null, this.getName() + '_id_'); 93 | return uniqueName; 94 | }, 95 | 96 | /** 97 | * Prefix is the unique name with the delimiter 98 | */ 99 | applyPrefix: function (prefix) { 100 | prefix = prefix || this.getUniqueName() + '_'; 101 | return prefix; 102 | }, 103 | 104 | /** 105 | Get the data object with the keys prefixed 106 | */ 107 | getPrefixedData: function (data) { 108 | var name, newName, value, 109 | result; 110 | 111 | if (!data) { 112 | return null; 113 | } 114 | 115 | result = {}; 116 | 117 | for (name in data) { 118 | value = data[name]; 119 | newName = this.getPrefixedPath(name); 120 | result[newName] = value; 121 | } 122 | 123 | return result; 124 | }, 125 | 126 | /** 127 | Get the path with a correct prefix 128 | 129 | Examples: 130 | 131 | foo.bar -> myviewmodel_id_123_foo.bar 132 | myviewmodel.foo.bar -> myviewmodel_id_123_foo.bar 133 | myviewmodel_id_123_foo.bar -> myviewmodel_id_123_foo.bar (no change) 134 | 135 | */ 136 | getPrefixedPath: function (path) { 137 | var parts, 138 | maybeHasViewModelName, 139 | name, 140 | vm; 141 | 142 | // If there is already a unique name in the path 143 | if (this.uniqueNameRe.test(path)) { 144 | return path; 145 | } 146 | 147 | // The descriptor may contain a name of a ViewModel: myviewmodel.foo.bar 148 | maybeHasViewModelName = path.indexOf('.') > -1; 149 | if (maybeHasViewModelName) { 150 | parts = path.split('.'); 151 | name = parts[0]; 152 | 153 | // Searching for it 154 | vm = this.findViewModelByName(name); 155 | if (vm) { 156 | // Found. Binding to the specified ViewModel 157 | path = vm.getPrefix() + parts.slice(1).join('.'); 158 | } 159 | else { 160 | // Not found. Binding to this ViewModel 161 | path = this.getPrefix() + path; 162 | } 163 | } 164 | else { 165 | // The descriptor doesn't contain the name of a ViewModel. 166 | // Binding to this ViewModel 167 | path = this.getPrefix() + path; 168 | } 169 | 170 | return path; 171 | }, 172 | 173 | /** 174 | Find a ViewModel by the name up the hierarchy 175 | @param {String} name ViewModel's name 176 | @param {Boolean} skipThis true to ignore this instance 177 | */ 178 | findViewModelByName: function (name, skipThis) { 179 | var result, 180 | vm = skipThis ? this.getParent() : this; 181 | 182 | while (vm) { 183 | if (vm.getName() == name) { 184 | return vm; 185 | } 186 | vm = vm.getParent(); 187 | } 188 | 189 | return null; 190 | } 191 | }); 192 | -------------------------------------------------------------------------------- /src/extjs-5-602/Ext/vmx/app/SplitViewModel.js: -------------------------------------------------------------------------------- 1 | /* global Ext */ 2 | 3 | /** 4 | * This override encapsulates the ViewModel of a component. 5 | * This is achieved by giving unique names to the properties of the ViewModel. 6 | */ 7 | Ext.define('Ext.vmx.app.SplitViewModel', { 8 | override: 'Ext.app.ViewModel', 9 | 10 | config: { 11 | /** 12 | @cfg {String} 13 | ViewModel name 14 | */ 15 | name: undefined, 16 | 17 | /** 18 | @cfg {String} 19 | @private 20 | Identifier (name + sequential identifier) 21 | */ 22 | uniqueName: undefined, 23 | 24 | /** 25 | @cfg {String} 26 | @private 27 | uniqueName + '_' 28 | */ 29 | prefix: undefined 30 | }, 31 | 32 | uniqueNameRe: /_id_\d+/, 33 | 34 | privates: { 35 | applyData: function (newData, data) { 36 | newData = this.getPrefixedData(newData); 37 | data = this.getPrefixedData(data); 38 | 39 | return this.callParent([newData, data]); 40 | }, 41 | 42 | applyLinks: function (links) { 43 | links = this.getPrefixedData(links); 44 | return this.callParent([links]); 45 | }, 46 | 47 | applyFormulas: function (formulas) { 48 | formulas = this.getPrefixedData(formulas); 49 | return this.callParent([formulas]); 50 | }, 51 | 52 | bindExpression: function (path, callback, scope, options) { 53 | path = this.getPrefixedPath(path); 54 | return this.callParent([path, callback, scope, options]); 55 | } 56 | }, 57 | 58 | linkTo: function (key, reference) { 59 | key = this.getPrefixedPath(key); 60 | return this.callParent([key, reference]); 61 | }, 62 | 63 | get: function (path) { 64 | path = this.getPrefixedPath(path); 65 | return this.callParent([path]); 66 | }, 67 | 68 | set: function (path, value) { 69 | if (Ext.isString(path)) { 70 | path = this.getPrefixedPath(path); 71 | } 72 | else if (Ext.isObject(path)) { 73 | path = this.getPrefixedData(path); 74 | } 75 | this.callParent([path, value]); 76 | }, 77 | 78 | /** 79 | * The name is either a specified name 80 | * or a type of the ViewModel 81 | * or just 'viewmodel' (for anonymous ViewModels). 82 | */ 83 | applyName: function (name) { 84 | name = name || this.type || 'viewmodel'; 85 | return name; 86 | }, 87 | 88 | /** 89 | * Unique name is based on the Ext.id generator 90 | */ 91 | applyUniqueName: function (uniqueName) { 92 | uniqueName = uniqueName || Ext.id(null, this.getName() + '_id_'); 93 | return uniqueName; 94 | }, 95 | 96 | /** 97 | * Prefix is the unique name with the delimiter 98 | */ 99 | applyPrefix: function (prefix) { 100 | prefix = prefix || this.getUniqueName() + '_'; 101 | return prefix; 102 | }, 103 | 104 | /** 105 | Get the data object with the keys prefixed 106 | */ 107 | getPrefixedData: function (data) { 108 | var name, newName, value, 109 | result; 110 | 111 | if (!data) { 112 | return null; 113 | } 114 | 115 | result = {}; 116 | 117 | for (name in data) { 118 | value = data[name]; 119 | newName = this.getPrefixedPath(name); 120 | result[newName] = value; 121 | } 122 | 123 | return result; 124 | }, 125 | 126 | /** 127 | Get the path with a correct prefix 128 | 129 | Examples: 130 | 131 | foo.bar -> myviewmodel_id_123_foo.bar 132 | myviewmodel.foo.bar -> myviewmodel_id_123_foo.bar 133 | myviewmodel_id_123_foo.bar -> myviewmodel_id_123_foo.bar (no change) 134 | 135 | */ 136 | getPrefixedPath: function (path) { 137 | var parts, 138 | maybeHasViewModelName, 139 | name, 140 | vm, 141 | isNegative; 142 | 143 | // If there is already a unique name in the path 144 | if (this.uniqueNameRe.test(path)) { 145 | return path; 146 | } 147 | 148 | isNegative = path.indexOf('!') == 0; 149 | if (isNegative) { 150 | // '!foo.bar' -> 'foo.bar' 151 | path = path.substring(1); 152 | } 153 | 154 | // The descriptor may contain a name of a ViewModel: myviewmodel.foo.bar 155 | maybeHasViewModelName = path.indexOf('.') > -1; 156 | if (maybeHasViewModelName) { 157 | parts = path.split('.'); 158 | name = parts[0]; 159 | 160 | // Searching for it 161 | vm = this.findViewModelByName(name); 162 | if (vm) { 163 | // Found. Binding to the specified ViewModel 164 | path = vm.getPrefix() + parts.slice(1).join('.'); 165 | } 166 | else { 167 | // Not found. Binding to this ViewModel 168 | path = this.getPrefix() + path; 169 | } 170 | } 171 | else { 172 | // The descriptor doesn't contain the name of a ViewModel. 173 | // Binding to this ViewModel 174 | path = this.getPrefix() + path; 175 | } 176 | 177 | if (isNegative) { 178 | path = '!' + path; 179 | } 180 | 181 | return path; 182 | }, 183 | 184 | /** 185 | Find a ViewModel by the name up the hierarchy 186 | @param {String} name ViewModel's name 187 | @param {Boolean} skipThis true to ignore this instance 188 | */ 189 | findViewModelByName: function (name, skipThis) { 190 | var result, 191 | vm = skipThis ? this.getParent() : this; 192 | 193 | while (vm) { 194 | if (vm.getName() == name) { 195 | return vm; 196 | } 197 | vm = vm.getParent(); 198 | } 199 | 200 | return null; 201 | } 202 | }); 203 | --------------------------------------------------------------------------------