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

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.

Related

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

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.

Event listener on a Sencha Touch generated setter

Is it possible to set an event listener to trigger when a user-defined config property is changed? Ex.
Ext.define('myClass', {
singleton: true,
config: {
myProperty: null
}
}
And I want to set up a listener so that when I call myClass.setMyProperty('blah'); it gets triggered. Possible?
Generated setters does not fire any event.
However you can define your own setter and fire your custom event from it. Your class have to use Ext.mixin.Observable mixin (or extend from some class which use this mixin) if you want to fire events from it.
Ext.define('myClass', {
mixins: ['Ext.mixin.Observable'],
singleton: true,
config: {
myProperty: null
},
setMyProperty: function(value) {
this.myProperty = value;
this.fireEvent('myPropertySetted');
}
}
The comments in the accepted answer actually contain the full answer, so for the sake of googlers who get here, I'll type it up:
The answer is to create a function called applyX or updateX (where X is your capitalized property), depending on if you want to receive the new value before or after (respectively) it is physically changed in the config. Sencha will call your function:
Ext.define('myClass', {
singleton: true,
config: {
myProperty: null
},
updateMyProperty: function(value) {
... do something with value. this.getMyProperty() will also work
}
}
Read more here:
http://docs.sencha.com/touch/2.4/core_concepts/class_system.html

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 to change a tooltip's position in ExtJs 3.3

i want to change a tooltip's position to show it upon a button. I tried the method below as mentioned in ExtJs's forum. It doesn't work, i can't override the getTargetXY method, the tooltip is shown always in the same position. Do you have any solution ?
this.desktopButton = new Ext.Button({
icon: "arrow_in.png",
scope: this,
handler: this.desktopButtonHandler,
tooltip: new Ext.ToolTip({
text: 'Message',
getTargetXY: function () {
return [100, 100];
}
})
});
Ext elements can only be passed configuration options as specified in the documentation; getTargetXY is not one of those options.
If you want to override that method, you have two choices:
Override all Ext Tooltips to use your new function
Extend the existing Tooltip class to support overriding that method
I would not recommend overriding the method, as that could have other consequences. I will, however, explain how to do both.
To override the tooltip:
Ext.override(Ext.Tooltip, {
getTargetXY: function() {
return [100, 100]
}
});
To extend the tooltip:
MyToolTip = Ext.Extend(Ext.Tooltip, {
constructor: function(config) {
var config = config || {};
if (config.getTargetXY) {
this.getTargetXY = config.getTargetXY;
}
MyToolTip.superclass.constructor.call(this, config);
}
});
Note that setting 'targetXY' may prove unhelpful, as Ext JS may override this setting (depending on the view size).
Overriding the "showAt" method can prevent this:
showAt:function() {
var xy = [this.getTargetXY()[0],this.getTargetXY()[1]-this.height]
MyTooltip.superclass.showAt.call(this, xy);
}

Extended ExtJS Class can't find custom listener function - "oe is undefined"

I'm adding a custom context menu to a TreePanel.
This was all working when I had a separate function for the context menu, but I was having problems where the context menu items would end up doubled/tripling up if I clicked on one of the options and then viewed the context menu again.
I had a look around for other contextmenu examples and came up with this one by Aaron Conran I pretty much "stole" it wholesale with a few additions, tacking the function directly into the Ext.ext.treePanel config. This gave me an error about "oe is undefined" which seemed to refer to "contextmenu: this.onContextMenu" in the tree config.
I figured it was probably something to do with the way I was defining all of this, so I decided to look at extending Ext.ext.TreePanel with my function in it as a learning exercise as much as anything.
Unfortunately, having managed to sort out extending TreePanel I'm now back to getting "oe is undefined" when the page tries to build the TreePanel. I've had a look around and I'm not really sure whats causing the problem, so any help or suggestions would be greatly appreciated.
Here is the code that is used to define/build the tree panel. I hope its not too horrible.
siteTree = Ext.extend(Ext.tree.TreePanel,{
constructor : function(config){
siteTree.superclass.constructor.call(this, config);
},
onContextMenu: function(n,e){
if (!this.contextMenu){
console.log('treeContextMenu',n,e);
if (n.parentNode.id == 'treeroot'){
var menuitems = [{text:'Add Child',id:'child'}];
} else {
var menuitems =
[{text:'Add Child',id:'child'},
{text:'Add Above',id:'above'},
{text:'Add Below',id:'below'}];
}
this.contextMenu = new Ext.menu.Menu({
id:'treeContextMenu',
defaults :{
handler : treeContextClick,
fqResourceURL : n.id
},
items : menuitems
});
}
var xy = e.getXY();
this.contextMenu.showAt(xy);
}
});
var treePanel = new siteTree({
id: 'tree-panel',
title : 'Site Tree',
region : 'center',
height : 300,
minSize: 150,
autoScroll: true,
// tree-specific configs:
rootVisible: false,
lines: false,
singleExpand: true,
useArrows: true,
dataUrl:'admin.page.getSiteTreeChildren?'+queryString,
root: {
id: 'treeroot',
nodeType: 'async',
text: 'nowt here',
draggable: false
},
listeners:{
contextmenu: this.onContextMenu
}
});
As a total aside; Is there a better way to do this in my context menu function?
if (n.parentNode.id == 'treeroot') {
Basically, if the clicked node is the top level I only want to give the user an add Child option, not add above/below.
Thanks in advance for your help
In your instantiation of your siteTree class you have:
listeners: {
contextmenu: this.onContextMenu
}
However, at the time of the instantiation this.onContextMenu is not pointing to the onContextMenu method you defined in siteTree.
One way of fixing it is to call the method from within a wrapper function:
listeners: {
contextmenu: function() {
this.onContextMenu();
}
}
Assuming you don't override the scope in the listeners config 'this' will be pointing to the siteTree instance at the time the listener is executed.
However, since you are already defining the context menu in the siteTree class, you may as well define the listener there:
constructor: function( config ) {
siteTree.superclass.constructor.call(this, config);
this.on('contextmenu', this.onContextMenu);
}
Ensuring the context menu is removed with the tree is also a good idea. This makes your siteTree definition:
var siteTree = Ext.extend(Ext.tree.TreePanel, {
constructor: function( config ) {
siteTree.superclass.constructor.call(this, config);
this.on('contextmenu', this.onContextMenu);
this.on('beforedestroy', this.onBeforeDestroy);
},
onContextMenu: function( node, event ) {
/* create and show this.contextMenu as needed */
},
onBeforeDestroy: function() {
if ( this.contextMenu ) {
this.contextMenu.destroy();
delete this.contextMenu;
}
}
});
I had this problem yesterday. The issue with the duplicate and triplicate items in the context menu is due to extjs adding multiple elements to the page with the same ID. Each time you call this.contextMenu.showAt(xy) you are adding a div with the ID 'treeContextMenu' to the page. Most browsers, IE especially, deal with this poorly. The solution is to remove the old context menu before adding the new one.
Here is an abridged version of my code:
var old = Ext.get("nodeContextMenu");
if(!Ext.isEmpty(old)) {
old.remove();
}
var menu = new Ext.menu.Menu({
id:'nodeContextMenu',
shadow:'drop',
items: [ ... ]
});
menu.showAt(e.xy);
I suggest never using hardcoded IDs. #aplumb suggests cleaning the DOM to reuse an existing ID. OK, but I suggest you cleanup the DOM when you no longer need the widgets/elements in the DOM and you should never reuse an ID.
var someId = Ext.id( null, 'myWidgetId' );
var someElement = new SuperWidget({
id: someId,
...
});
Just to add to owlness's answer
This bit here:
listeners: {
contextmenu: this.onContextMenu
}
Gets executed when the javascript file is loaded. this at that stage is most likely pointing to the window object.
A simple way to fix it is adding the listener on hide event of context menu, so you destroy him.
new Ext.menu.Menu(
{
items:[...],
listeners: { hide: function(mn){ mn.destroy(); } }
}
).show(node.ui.getAnchor());
;)

Resources