ExtJS 5 MVVM: Updating Parent's ViewModel from Child's - extjs

My ExtJS 5 component's ViewModel is inheriting data from an ancestor's ViewModel.
How do I update the parent's ViewModel's data from the child's perspective? If I try childPanel.getViewModel().set('myProp', 'foo'); that creates a new instance of myProp on the childPanel ViewModel, instead of updating parentPanel's VM. Other components using myProp's value will be different than childPanel.
By creating that local myProp on the child, when the parent's myProp changes, the child will not change with it, because of the severed relationship.
I only want 1 instance of the myProp property, and for that value to be on the parent's ViewModel.
To fix this, it seems that my child VM would have to know if the property was inherited or if it was stored locally, requiring the child to know the correct architecture of the application. It has to know much more of the application's architecture than I am comfortable with, significantly coupling the child to the parent.
Example showing the child creating a new instance of panelTitle instead of updating the parent's:
https://fiddle.sencha.com/#fiddle/1e09
EDIT: Moved Button's Click Event to ViewController
Ext.application({
name: 'Fiddle',
launch: function() {
Ext.define('ChildPanel', {
extend: 'Ext.panel.Panel',
alias: 'widget.childpanel',
controller: 'childpanelcontroller',
bind: {
title: '{panelTitle}'
},
// first panel has its own viewmodel, which is a child of the viewport's VM
viewModel: {
data: {
'btnText': 'Click to Change Title'
}
},
items: [{
xtype: 'button',
scale: 'large',
bind: {
text: '{btnText}'
},
listeners: {
'click': 'onButtonClick'
}
}]
});
Ext.define('ChildPanelController', {
extend: 'Ext.app.ViewController',
alias: 'controller.childpanelcontroller',
onButtonClick: function(btn) {
// updates first panel's VM
this.getViewModel().set('panelTitle', '<span style="color:red;">Now They Have Different Titles</span>');
debugger;
window.setTimeout(function() {
// by setting the first panel's VM instead of the viewport's,
// the first panel will not use viewport's VM for `panelTitle` property again
btn.up('viewport').getViewModel().set('panelTitle', '<span style="color:green;">And Are No Longer Using the Same VM Data</span>');
}, 500);
}
});
Ext.create('Ext.container.Viewport', {
viewModel: {
data: {
'panelTitle': 'Both Have the Same Title'
}
},
layout: 'hbox',
items: [{
xtype: 'childpanel',
flex: 1
}, {
// second panel uses the viewport's viewmodel
xtype: 'panel',
bind: {
title: '{panelTitle}'
},
flex: 1,
margin: '0 0 0 25px'
}]
});
}
});

After talking to Sencha support, I created an override for the Ext.app.ViewModel set function.
Ext.define('Overrides.app.ViewModel', {
override: 'Ext.app.ViewModel',
/**
* Override adds option to only update the ViewModel that "owns" a {#link #property-data} property.
* It will traverse the ViewModel tree to find the ancestor that the initial ViewModel inherited `path` from.
* If no owner is found, it updates the initial ViewModel.
*
* Only uses the override if `onlyUpdateOwner` parameter is `true`.
*
* #param {Boolean} [onlyUpdateOwner=false] If `true`, uses override to update VM where `path` is stored
* #inheritdoc Ext.app.ViewModel#method-set
* #override_ext_version ExtJS 5.1.2.748
*/
set: function (path, value, onlyUpdateOwner) {
var vm = this,
foundOwner = false,
ownerVM = vm;
onlyUpdateOwner = Ext.valueFrom(onlyUpdateOwner, false);
if (onlyUpdateOwner) {
// find ViewModel that initial ViewModel inherited `path` from (if it did)
do {
// `true` if `path` ***ever*** had a value (anything but `undefined`)
if (ownerVM.hadValue && ownerVM.hadValue[path]) {
break;
}
} while (ownerVM = ownerVM.getParent());
}
// reverts to initial ViewModel if did not inherit it
ownerVM = ownerVM || vm;
ownerVM.callParent([path, value]);
}
});
It now gives the developer the option to only update the ViewModel that was the source of the property.
If it cannot find a ViewModel that "owns" the property, it falls back to the initially-called-from ViewModel.
It finishes by calling the original ExtJS set function with the proper ViewModel context.

In order to get the ViewModel from any child component, you can override the Component class with this:
Ext.define(null, {
override: 'Ext.Component',
getVM() {
let vm = this.up();
do {
if(vm.getViewModel())
break;
} while (vm = vm.up());
return vm.getViewModel()
}
});
Then you can just call this.getVM() from any child component, no matter how down deep they are. Something similiar can be done to get the ViewController.
I wonder if this functionality is already part of ExtJS.

Related

ExtJS6: Binding store to panel items

Does ExtJS6 allows a store binding to simple panel items, such that it plucks specified columns and display them as item
{
xtype: 'panel',
id: 'master_list',
title: 'MasterList',
defaultType: 'button',
bind: {
store: '{zones}'
}
}
Atm in extJS 6, the item config on a panel is not a bindable item, mostly due to the fact that there is no getItem() and setItems() method found on the panel. You could always override panel and add that functionality and it would look something like this:
Ext.define("Ext.panel.StoreButtonPanel", {
/* extend a panel so you get same base functionality of a panel */
extend: 'Ext.panel.Panel',
/* other configs and overrides you might want */
setStore:function(){
// function to bind the store to panel
},
getStore:function(){
// function to get store from panel
}
setItems: fuunction(){
var me = this,
myStore = me.getStore();
// loop through store and add items.
myStore.each(function(storeItem){
// create the items that you want from teh store via loop and using
// storeItem
Ext.create('Ext.button.Button', {
text: storeItem.get('text'),
/* other things here if needed */
})
});
},
init: function(){
var me = this;
// call method to create items if a store is found.
if(me.getStore()){
me.setItems();
}
me.callParent();
}
});
You can add store parameter to your panel. Extjs will load the store directly.
store: Ext.Create('Yourapp.store.storename'),

Extjs - init class atttribute and get class name

i have simple 'gridpanel' with 'tbar' like this
Ext.define('Ext.abc.grid', {
extend: 'Ext.grid.Panel',
type:1,
tbar:[
{
text:'title1',
class :'a1',
handler:function(type){
if (this.type == 1) { // button not 1
Ext.query(".a2").setDisabled(false);
}
},{
text:'title2',
class :'a2',
handler:function(type){
if (this.type == 1) { // button not 1
Ext.query(".a1").setDisabled(false);
}
}
]
});
i try to add class (a1) to button title1 and the some for title2, but when i get class like
Ext.query(".a1").setDisabled(false);
it's not working
and i can't get type = 1 when i click title1, i using this.type but results is 'button' not 1
How can i do that, thanks
You've got several problems here.
First, see sha's answer, you're getting an array as the result of your call to Ext.query(...).
Second, Ext.query returns Ext.dom.Element, which are Ext objects for representing actual DOM elements like div, img, etc. What you want to access, your buttons, are Ext.Component. You can query components with Ext.ComponentQuery.
Then, you're using this.type in your button handler functions, but when these method get called, this will be the button itself (this can be customized using the scope option), not the container on which you set type: 1.
Edit:
Here's how to make your example work:
Ext.define('Ext.abc.Grid', {
extend: 'Ext.grid.Panel'
,type: 1
,tbar: [{
text: 'title1'
,itemId: 'button1'
// just FYI, here the scope (this) is the window, because we are not
// in a method
,scope: this // so this doesn't work
,handler: function() {
// using ComponentQuery to get a reference to the other components
var grid = this.up('grid'), // by xtype
tbar = this.up(), // by relative position
button2 = tbar.down('#button2'); // by itemId
if (grid.type === 1) {
button2.disable();
}
}
}, {
text: 'title2'
,itemId: 'button2'
,handler: function() { ... }
}]
});
Now, reading your mind, here's what I think you actually want to do:
Ext.define('Ext.abc.Grid', {
extend: 'Ext.grid.Panel'
,type: 1
,tbar: [{
text: 'title1'
,itemId: 'button1'
}, {
text: 'title2'
,itemId: 'button2'
}]
// reading in your mind, I guess, this is what you really want to do:
,initComponent: function() {
this.callParent();
if (this.type === 1) {
this.down('#button2').disable();
} else {
this.down('#button1').disable();
}
}
});
Ext.query returns you an array http://docs.sencha.com/extjs/4.1.3/#!/api/Ext-method-query
You can't simply call setDisabled() on an array. You need to loop through all elements.

Sencha Touch - hasParent() returns false; getParent() returns undefined; on component that has a parent

I have a simple view that has a list on the left and a container to show details of the tapped list item on the right defined as such:
Ext.define('app.view.activity.ListContainer', {
extend: 'Ext.Container',
alias: 'widget.activitylistcontainer',
requires: [
'app.view.activity.List',
'app.view.activity.ListDetail'
],
config: {
layout: {
type: 'hbox'
},
items: [
{
xtype: 'activitylist',
flex: 4
},
{
xtype: 'activitylistdetail',
flex: 5
}
]
}
});
When the activity.List Store loads, I want activity.ListDetail to show the details of the first Record in the Store. My problem is that I cannot get to activity.ListContainer because it is not being recognised as the parent of activity.List.
onActivityListInitialize: function(component, options){
var activityStore = Ext.create('app.store.Activity');
activityStore.on('load', function(store, records, successful, operation, eOpts){
var activityListHasContainer = component.hasParent(),
activityListContainer = component.getParent(),
activityListContainerUp = component.up('activitylistcontainer');
console.log(component, component.parent, activityListHasContainer, activityListContainer, activityListContainerUp);
});
component.setStore(activityStore);
}
The output of the console.log is Object, undefined, false, undefined, undefined. I included component.parent in the log because when I inspect the object there is a parent property, but for whatever reason it shows undefined when logged out.
Could someone help shed some light on this? Is activity.ListContainer not considered a parent of activity.List in Sencha? Or is it something else in my app that is screwing with this? If it helps, activity.ListContainer is inside a navigationview
Turns out activity.ListContainer does not exist at the time the activity.ActivityList initialize event is triggered. My solution is to move this method to the activity.ListContainer initialize event.

ExtJS 4: How to know when any field in a form (Ext.form.Panel) changes?

I'd like a single event listener that fires whenever any field in a form (i.e., Ext.form.Panel) changes. The Ext.form.Panel class doesn't fire an event for this itself, however.
What's the best way to listen for 'change' events for all fields in a form?
Update: Added a 3rd option based on tip in comments (thanks #innerJL!)
Ok, looks like there are at least two fairly simple ways to do it.
Option 1) Add a 'change' listener to each field that is added to the form:
Ext.define('myapp.MyFormPanel', {
extend: 'Ext.form.Panel',
alias: 'myapp.MyFormPanel',
...
handleFieldChanged: function() {
// Do something
},
listeners: {
add: function(me, component, index) {
if( component.isFormField ) {
component.on('change', me.handleFieldChanged, me);
}
}
}
});
This has at least one big drawback; if you "wrap" some fields in other containers and then add those containers to the form, it won't recognize the nested fields. In other words, it doesn't do a "deep" search through the component to see if it contains form field that need 'change' listeners.
Option 2) Use a component query to listen to all 'change' events fired from fields in a container.
Ext.define('myapp.MyController', {
extend: 'Ext.app.Controller',
...
init: function(application) {
me.control({
'[xtype="myapp.MyFormPanel"] field': {
change: function() {
// Do something
}
}
});
}
});
Option 3) Listen for the 'dirtychange' fired from the form panel's underlying 'basic' form (Ext.form.Basic). Important: You need to make sure you must enable 'trackResetOnLoad' by ensuring that {trackResetOnLoad:true} is passed to your form panel constructor.
Ext.define('myapp.MyFormPanel', {
extend: 'Ext.form.Panel',
alias: 'myapp.MyFormPanel',
constructor: function(config) {
config = config || {};
config.trackResetOnLoad = true;
me.callParent([config]);
me.getForm().on('dirtychange', function(form, isDirty) {
if( isDirty ) {
// Unsaved changes exist
}
else {
// NO unsaved changes exist
}
});
}
});
This approach is the "smartest"; it allows you to know when the form has been modified, but also if the user modifies it back to it's original state. For example, if they change a text field from "Foo" to "Bar", the 'dirtychange' event will fire with 'true' for the isDirty param. But if the user then changes the field back to "Foo", the 'dirtychange' event will fire again and isDirty will be false.
I want to complement Clint's answer. There is one more approach (and I think it's the best for your problem). Just add change listener to form's defaults config:
Ext.create('Ext.form.Panel', {
// ...
defaults: {
listeners: {
change: function(field, newVal, oldVal) {
//alert(newVal);
}
}
},
// ...
});

How can I access class variables in an ExtJS event handler?

this.getUrl = 'test';
this.items.add(
new Ext.form.Checkbox(
{
listeners: {
check: function(checkbox, checked) {
alert(this.getUrl);
},
}
)
)
How do I access this.getUrl in the check handler?
I wonder why nobody has suggested the obvious, just do it the Ext way and use the 'scope' config property:
this.getUrl = 'test';
this.items.add(
new Ext.form.Checkbox(
{
listeners: {
check: function(checkbox, checked) {
alert(this.getUrl);
},
scope: this
}
)
)
Event handlers are usually called from a different scope (this value). If all you want is a single value in the handler, lexical scoping is the easiest way to go:
var getUrl = 'test'; // now it's just a regular variable
this.items.add(
new Ext.form.Checkbox(
{
listeners: {
check: function(checkbox, checked) {
alert(getUrl); // still available - lexical scope!
},
}
)
)
Or if you really do want the parent object available as this in your event handler, you can use Ext.Function.bind to modify the scope:
this.getUrl='test';
this.items.add(
new Ext.form.Checkbox(
{
listeners: {
check: Ext.Function.bind( function(checkbox, checked) {
alert(this.getUrl);
}, this ), // second arg tells bind what to use for 'this'
}
)
)
Update: Ext.Function.bind is an ExtJS 4 feature. If you're on ExtJS 3.x or lower, you can use Function.createDelegate to the same end:
this.getUrl='test';
this.items.add(
new Ext.form.Checkbox(
{
listeners: {
check: function(checkbox, checked) {
alert(this.getUrl);
}.createDelegate(this)
}
)
)
There are multiple ways to access the property getUrl. Here are the few possible options:
1. Use Ext.getCmp: If you set an id for your FormPanel (or other extjs component whatever you are using), you can access it using Ext.getCmp() method. So,
var yourComponent = Ext.getCmp('yourComponentId');
alert(yourComponent.getUrl);
2. Use OwnerCt property: If you need to access your parent container (If the parent is holding your checkbox) you can access the parent container through the public property OwnerCt.
3. Use refOwner property: If you use ref system in your code, you can make use of this property to get hold of the container and access the required variable.
I think it will be easy for you to go with the first option.

Resources