Related
To reproduce this issue, I created a fiddle: https://fiddle.sencha.com/#view/editor&fiddle/3mk0 . The steps to reproduce are as follows:
Open your favorite browser and open the debugging tools to be able to view the JavaScript console
Navigate to the fiddle and run it
Double-click on a row - the grid is empty. Check the JS console: Uncaught TypeError: store is null (I used FF). The crux of the issue is that, in this line, let store = vm.getStore('testStore');, vm.getStore('testStore') returns null the first time. That's what I am trying to understand - why doesn't ExtJs initialize completely the ViewModel and the store, and instead it returns null. The problem is the binding in the url of the proxy. If the store doesn't have any binding in the url, it's going to work the first time as well.
Close the window, and double-click again on a row. This time the grid will show the data.
I know how to fix this - if I issue a vm.notify() before I set the store, it's going to work properly (I added a commented out line in the code).
Here is the source code from the fiddle:
app.js
/// Models
Ext.define('App.model.GroupRecord', {
extend: 'Ext.data.Model',
alias: 'model.grouprecord',
requires: [
'Ext.data.field.Integer',
'Ext.data.field.String'
],
fields: [{
type: 'int',
name: 'id'
}, {
type: 'string',
name: 'description'
}]
});
Ext.define('App.model.SomeRecord', {
extend: 'Ext.data.Model',
requires: [
'Ext.data.field.Integer',
'Ext.data.field.String'
],
fields: [{
type: 'int',
name: 'id'
}, {
type: 'string',
name: 'description'
}, {
type: 'int',
name: 'groupId'
}]
});
//// SomeGridPanel
Ext.define('App.view.SomeGridPanel', {
extend: 'Ext.grid.Panel',
alias: 'widget.somegridpanel',
requires: [
'App.view.SomeGridPanelViewModel',
'App.view.SomeGridPanelViewController',
'Ext.view.Table',
'Ext.grid.column.Number'
],
controller: 'somegridpanel',
viewModel: {
type: 'somegridpanel'
},
bind: {
store: '{testStore}'
},
columns: [{
xtype: 'numbercolumn',
dataIndex: 'id',
text: 'ID',
format: '0'
}, {
xtype: 'gridcolumn',
dataIndex: 'description',
text: 'Description'
}]
});
Ext.define('App.view.SomeGridPanelViewModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.somegridpanel',
requires: [
'Ext.data.Store',
'Ext.data.proxy.Ajax',
'Ext.data.reader.Json'
],
data: {
groupId: null
},
stores: {
testStore: {
model: 'App.model.SomeRecord',
proxy: {
type: 'ajax',
url: 'data1.json?groupId={groupId}',
reader: {
type: 'json'
}
}
}
}
});
Ext.define('App.view.SomeGridPanelViewController', {
extend: 'Ext.app.ViewController',
alias: 'controller.somegridpanel',
onRefresh: function (groupId) {
console.log('calling the grid panel onrefresh for groupId: ' + groupId);
let vm = this.getViewModel();
console.log(vm);
vm.set('groupId', groupId);
//vm.notify(); // <- uncomment this line to make it work properly
let store = vm.getStore('testStore');
//tore.proxy.extraParams.groupId = groupId;
store.load();
}
});
// TestWindow
Ext.define('App.view.TestWindow', {
extend: 'Ext.window.Window',
alias: 'widget.testwindow',
requires: [
'App.view.TestWindowViewModel',
'App.view.TestWindowViewController',
'App.view.SomeGridPanel',
'Ext.grid.Panel'
],
controller: 'testwindow',
viewModel: {
type: 'testwindow'
},
height: 323,
width: 572,
layout: 'fit',
closeAction: 'hide',
bind: {
title: '{title}'
},
items: [{
xtype: 'somegridpanel',
reference: 'someGrid'
}]
})
Ext.define('App.view.TestWindowViewModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.testwindow',
data: {
title: 'Test Window'
}
});
Ext.define('App.view.TestWindowViewController', {
extend: 'Ext.app.ViewController',
alias: 'controller.testwindow',
listen: {
controller: {
'*': {
loadData: 'onLoadData'
}
}
},
onLoadData: function (groupRecord) {
console.log('Loading data...');
console.log(groupRecord);
let vm = this.getViewModel();
vm.set('title', 'Group: ' + groupRecord.get('description'));
this.lookup('someGrid').getController().onRefresh(groupRecord.get('id'));
}
});
// ViewPort
Ext.define('App.view.MainViewport', {
extend: 'Ext.container.Viewport',
alias: 'widget.mainviewport',
requires: [
'App.view.MainViewportViewModel',
'App.view.MainViewportViewController',
'Ext.grid.Panel',
'Ext.view.Table',
'Ext.grid.column.Number'
],
controller: 'mainviewport',
viewModel: {
type: 'mainviewport'
},
height: 250,
width: 400,
items: [{
xtype: 'gridpanel',
title: 'Main Grid - double-click on a row',
bind: {
store: '{groupRecords}'
},
columns: [{
xtype: 'numbercolumn',
dataIndex: 'id',
text: 'Group id',
format: '00'
}, {
xtype: 'gridcolumn',
flex: 1,
dataIndex: 'description',
text: 'Group'
}],
listeners: {
rowdblclick: 'onGridpanelRowDblClick'
}
}]
});
Ext.define('App.view.MainViewportViewModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.mainviewport',
requires: [
'Ext.data.Store',
'Ext.data.proxy.Memory'
],
stores: {
groupRecords: {
model: 'App.model.GroupRecord',
data: [{
id: 1,
description: 'Group 1'
}, {
id: 2,
description: 'Group 2'
}, {
id: 3,
description: 'Group 3'
}, {
id: 4,
description: 'Group 4'
}, {
id: 5,
description: 'Group 5'
}],
proxy: {
type: 'memory'
}
}
}
});
Ext.define('App.view.MainViewportViewController', {
extend: 'Ext.app.ViewController',
alias: 'controller.mainviewport',
onGridpanelRowDblClick: function (tableview, record, element, rowIndex, e, eOpts) {
if (!this._testWindow) {
this._testWindow = Ext.create('widget.testwindow');
}
this._testWindow.show();
this.fireEvent('loadData', record);
}
});
Ext.application({
//name : 'Fiddle',
models: [
'SomeRecord',
'GroupRecord'
],
views: [
'MainViewport',
'TestWindow',
'SomeGridPanel'
],
name: 'App',
launch: function () {
Ext.create('App.view.MainViewport');
}
});
}
data1.json:
function(params, req, Fiddle) {
var range = [];
for (var i = 1; i <= 50; i++) {
var obj = {
id: i,
description: `Item ${i}`,
groupId: Math.floor( (i - 1) / 10) + 1
}
range.push(obj);
}
let allData = range.filter(it => it.groupId === params.groupId )
//console.log(params);
//console.log(req);
return allData;
}
The code is a bit contrived but it follows an issue (though not identical) I had in the real app. In the real app I have a weird intermittent issue, where I have a complex form with subcomponents, and a vm.notify() call made in a subcomponent, fires a method bound to a ViewModel data member (in other words it fires when that data member changes), which in turn tries to refresh a local store in another subcomponent but this subcomponent's ViewModel getStore() call returns null. The proxy of that store has the url property bound to a ViewModel data member. That's the common pattern. It seems to me that stores with proxies that have the url property bound to a ViewModel data property (ex: url: 'data1.json?groupId={groupId}') are not initialized properly by the time the form is rendered, and eventually, after some ViewModel computation cycles, they get initialized finally.
TIA
Update: There was a question below whether the url attribute of the proxy is bindable. I think it is bindable. Sencha Architect shows it as bindable, though, when enabled, it doesn't place the property in a bind object.
I did search the ExtJs 7.6.0 code base for samples where the {...} expressions are used in the url attribute, and I stumbled upon this test case:
packages\core\test\specs\app\ViewModel.js from line 7237:
describe("initial", function() {
it("should not create the store until a required binding is present", function() {
viewModel.setStores({
users: {
model: 'spec.User',
proxy: {
type: 'ajax',
url: '{theUrl}'
}
}
});
notify();
expect(viewModel.getStore('users')).toBeNull();
setNotify('theUrl', '/foo');
var store = viewModel.getStore('users');
expect(store.isStore).toBe(true);
expect(store.getProxy().getUrl()).toBe('/foo');
});
it("should wait for all required bindings", function() {
viewModel.setStores({
users: {
model: 'spec.User',
proxy: {
type: 'ajax',
url: '{theUrl}',
extraParams: {
id: '{theId}'
}
}
}
});
notify();
expect(viewModel.getStore('users')).toBeNull();
setNotify('theUrl', '/foo');
expect(viewModel.getStore('users')).toBeNull();
setNotify('theId', 12);
var store = viewModel.getStore('users');
expect(store.isStore).toBe(true);
expect(store.getProxy().getUrl()).toBe('/foo');
expect(store.getProxy().getExtraParams().id).toBe(12);
});
});
What is interesting is that it is expected in the test that viewmodel getStore would return null before setting the value for theUrl: expect(viewModel.getStore('users')).toBeNull(); !
Maybe it is by design...
Reason for the issue is - triggered event listener is getting executed before the View Model is initialised and grid is rendered
We can use following approach to fix these issues
Add delay before triggering the event
Listen for initViewModel method in controller and then load the store
Or add afterrender event on the grid, read its store and then load it
// Add delay to ensure view model is initialized
Ext.defer(function() {
this.fireEvent('loadData', record);
}, 1000, this);
There is a override function called "init" in each controller so use your initial code in this function. This function invokes every time when UI refresh/added new.
Example:
init: function() {
var vm = this.getViewModel();
var store = vm.data.myCurrentStore;
store.load();
}
I have an ExtJS 6.5.1 app and I am just now starting to migrate our app from MVC to MVVM, so I am pretty clueless about VM and VC.
I have a viewModel with an inline store like so:
Ext.define("MYAPP.view.ViewportViewModel",{
extend:"Ext.app.ViewModel",
alias: 'viewmodel.viewport',
constructor: function(config) {
var me = this;
this.callParent(arguments);
me.setStores({
info: {
autoLoad:true,
fields:["TEST"],
proxy:{
type:"ajax",
url:"blah.html",
reader:{
type:"json"
}
}
}
});
}
});
From inside my controller, how can I "get" the store so I can change the URL, reload, pass extraParams etc?
Thanks
You can get your store using this.getViewModel().getStore('info') inside of ViewController.
After getting store you can set another url using store.getProxy().setUrl(), load using store.load() and for sending extra params store.getProxy().extraParams.
Here is example
//this one way
store.load({
url: '{your url here}',
params: {
userid: 22216
}
});
//this another way
store.getProxy().setUrl('{your url here}');
store.getProxy().extraParams = {
userid: 22216
};
store.load();
In this FIDDLE, I have created a demo using view model and view controller. I hope this will help/guide you to achieve your requirement.
CODE SNIPPET
Ext.application({
name: 'Fiddle',
launch: function () {
Ext.define('MyViewController', {
extend: 'Ext.app.ViewController',
alias: 'controller.myview',
onRefreshButtonTap: function () {
var info = this.getViewModel().getStore('info');
info.getProxy().setUrl('data2.json');
info.load();
}
});
Ext.define("ViewportViewModel", {
extend: "Ext.app.ViewModel",
alias: 'viewmodel.myvm',
constructor: function (config) {
var me = this;
this.callParent(arguments);
me.setStores({
info: {
autoLoad: true,
fields: ['name', 'email', 'phone'],
proxy: {
type: 'ajax',
url: 'data1.json',
reader: {
type: 'json',
rootProperty: ''
}
}
}
});
}
});
//creating panel with GRID and FORM
Ext.create({
xtype: 'panel',
controller: 'myview',
title: 'Binding Example',
renderTo: Ext.getBody(),
viewModel: {
type: 'myvm'
},
layout: 'vbox',
items: [{
xtype: 'grid',
flex: 1,
width: '100%',
bind: '{info}',
columns: [{
text: 'Name',
dataIndex: 'name'
}, {
text: 'Email',
dataIndex: 'email',
flex: 1
}, {
text: 'Phone',
dataIndex: 'phone'
}],
listeners: {
itemclick: 'onGridItemClick'
}
}],
tbar:[{
text:'Refresh',
handler:'onRefreshButtonTap'
}]
});
}
});
I am trying to find the best way to bind items to a textfield in my extjs project. I downloaded the data into a store with one record in the controller. How would I bind to this textfield from the one record? I would preferably bind in the view, not in the controller
You should read this guide to understand better what binding is
The best solution for you is bind the record on the viewmodel of the view, so:
textfield.setBind({
value:'{myRec.valueToRefer}'
})
viewmodel.set('myRec',record.getData());
If you want, you can also use a form to handle this, using form.loadRecord and giving to the textfield a name..
A tip:
set inside the viewmodel a value to null:
data:{
myRec:null
}
Set your record in the viewmodel after setting the bind to the textfield.
Other tip:
If you can, avoid using setBind and prefer to set the binding directly on textfield creation:
//WILL WORK BUT YOU CAN AVOID IT
textfield.setBind({
value:'{myRec.valueToBind}'
})
//YES
var textfield=Ext.create({
xtype:'textfield',
bind:{
value:'{myRec.valueToBind}'
}
});
Refer to Sencha documentation also
You can use bind config to bind the data or any other config for ExtJS component.
Bind setting this config option adds or removes data bindings for other configs.
For example, to bind the title config:
var panel = Ext.create({
xtype: 'panel',
bind: {
title: 'Hello {user.name}'
}
});
To dynamically add bindings:
panel.setBind({
title: 'Greetings {user.name}!'
});
To remove bindings:
panel.setBind({
title: null
});
In this FIDDLE, I have created a demo for biding. I hope this will help/guide you to achieve you requirement.
CODE SNIPPET
Ext.application({
name: 'Fiddle',
launch: function () {
//defining Store
Ext.define('Store', {
extend: 'Ext.data.Store',
alias: 'store.gridstore',
autoLoad: true,
fields: ['name', 'email', 'phone'],
proxy: {
type: 'ajax',
url: 'data1.json',
reader: {
type: 'json',
rootProperty: ''
}
}
});
//defining view model
Ext.define('MyViewModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.myvm',
data: {
formdata: null
},
stores: {
gridstore: {
type: 'gridstore'
}
}
});
//Controller
Ext.define('MyViewController', {
extend: 'Ext.app.ViewController',
alias: 'controller.myview',
onGridItemClick: function (grid, record) {
//Bind the form data for CLICKED record
this.getViewModel().set('formdata', record)
}
});
//creating panel with GRID and FORM
Ext.create({
xtype: 'panel',
controller: 'myview',
title: 'Binding Example',
renderTo: Ext.getBody(),
viewModel: {
type: 'myvm'
},
layout: 'vbox',
items: [{
xtype: 'grid',
flex: 1,
width: '100%',
bind: '{gridstore}',
columns: [{
text: 'Name',
dataIndex: 'name'
}, {
text: 'Email',
dataIndex: 'email',
flex: 1
}, {
text: 'Phone',
dataIndex: 'phone'
}],
listeners: {
itemclick: 'onGridItemClick'
}
}, {
xtype: 'form',
flex: 1,
width: '100%',
defaults: {
anchor: '100%'
},
title: 'Bind this form on Grid item Click',
bodyPadding:15,
margin: '20 0 0 0',
// The fields
defaultType: 'textfield',
items: [{
fieldLabel: 'Name',
name: 'first',
allowBlank: false,
bind: '{formdata.name}'
}, {
fieldLabel: 'Email',
name: 'email',
allowBlank: false,
bind: '{formdata.email}'
}, {
fieldLabel: 'Phone',
name: 'phone',
allowBlank: false,
bind: '{formdata.phone}'
}]
}]
});
}
});
I am using ExtJs6.2 Modern toolkit.I have a situation where all my display labels are stored in DB and I need to use those to display on my form.
example
{
xtype: 'textfield',
readOnly: true,
scrollable: false,
label: ** showFromStore ** ,
labelAlign: 'top',
name: 'someName',
bind: '{SomeValue}'
},
What would be the best way to manage this display on the forms?
For binding all config of textfield or any other EXTJS component you can use bind config.
Example
Ext.create({
xtype: 'textfield',
bind: {
label: '{user.label}',
name: '{user.name}',
value: '{user.value}',
}
})
In this FIDDLE, I have created a demo using viewmodel, formpanel and textfield. I hope this will help/guide you to achieve your requirement.
CODE SNIPPET
Ext.application({
name: 'Fiddle',
launch: function () {
//defining view model
Ext.define('MyViewModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.myvm',
data: {
users: null
},
stores: {
formstore: {
autoLoad: true,
fields: ['name', 'email', 'phone', 'namelabel', 'phonelabel', 'emaillabel'],
proxy: {
type: 'ajax',
url: 'data.json',
reader: {
type: 'json',
rootProperty: ''
}
},
listeners: {
load: function (store, data, success) {
Ext.ComponentQuery.query('#myform')[0].getViewModel().set('users', data[0]);
}
}
}
}
});
//creating form
Ext.create({
xtype: 'formpanel',
itemId: 'myform',
title: 'Form example',
renderTo: Ext.getBody(),
fullscreen: true,
viewModel: {
type: 'myvm'
},
defaults: {
xtype: 'textfield',
readOnly: true
},
items: [{
bind: {
label: '{users.namelabel}',
value: '{users.name}'
}
}, {
bind: {
label: '{users.emaillabel}',
value: '{users.email}'
}
}, {
bind: {
label: '{users.phonelabel}',
value: '{users.phone}'
}
}]
});
}
});
JSON CODE
[{
"name": "Lisa Doshi",
"email": "lisa#simpsons.com",
"phone": "555-111-1224",
"namelabel": "Full Name",
"emaillabel": "Email Address",
"phonelabel": "Phone Number"
}]
I am not sure how to create a Fiddle to demonstrate this,but I will try to provide all details here
data returned from service is
[{"ID":"P1","Label":"Procedure"},{"ID":"P2","Label":"Process"},,
{"ID":"P3","Label":"Problem"}]
I am loading this data in a store
Ext.define('My.store.myLabels',
{
alias: 'store.Lang',
extend: 'Ext.data.Store',
config:
{
fields: ['ID', 'Label'],
pageSize: 10000,
proxy:
{
type: 'jsonp',
url: myserviceURL
}
}
});
Now how do I bind a single value from the store here
xtype: 'textfield',
readOnly: true,
scrollable: false,
labelAlign: 'top',
name: 'Process',
itemId: 'P1',
bind:{
value: '{somevalue}',
label: * how to bind the value of P1 here *,
}
I tried a formula: but with that I would have to write a formula for each UI element.
So my question is:
How to access a single item from the store to bind it here like:
store.data.items["0"].ID.P1
Or how to access the component
ID/itemId that triggered the view model formula so that I can do
displayLabel: {
get: function (get) {
var store = Ext.StoreMgr.get('myLabels');
//search the store based on item id of the component that
//triggered the formula
if (index > -1) {
return store.getAt(index).data.Label;
}
}
}
Let's say I've got a ViewController, ViewModel, and my View. In the View, I've got a form panel that gets a loaded record. When this record loads into the form, I want to hide or show a button based on the record's status field, so I figured do something with binding. However, it looks like binding is limited to only inverting, not actually using an expression. To get a better understanding, take a look at this code:
Ext.application({
name : 'Fiddle',
launch : function() {
Ext.define('User', {
extend: 'Ext.data.Model',
fields: ['name', 'status']
});
Ext.define('UserListController', {
extend : 'Ext.app.ViewController',
alias: 'controller.userlist'
});
Ext.define('UserListViewModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.userlist'
});
Ext.define('UserList', {
extend: 'Ext.form.Panel',
controller: 'userlist',
viewModel: 'userlist',
tbar: [{
text: 'Add',
reference: 'addButton',
bind: {
//hidden: '{status == 2}'
}
}, {
text: 'Delete',
reference: 'deleteButton',
bind: {
//hidden: '{status == 1}'
}
}],
items: [{
xtype: 'displayfield',
name: 'name',
fieldLabel: 'Name'
}, {
xtype: 'displayfield',
name: 'status',
fieldLabel: 'Status'
}]
});
var myForm = Ext.create('UserList', {
width: 400,
height: 200,
renderTo: Ext.getBody()
});
var record = Ext.create('User', {
name: 'blah',
status: 2
});
myForm.loadRecord(record);
if (record.get('status') === 2) {
myForm.lookupReference('addButton').hide();
}
}
});
As you can see, I'm currently just probing the values of the record to hide the addButton. Is there anyway I can accomplish this with binding or some other approach? It's good to note that I also looked at formulas, but from what I'm understanding, that's only for changing how data is rendered, so it didn't seem like the proper route.
If your record is part of the view model data - use formulas, like:
formulas: {
hideDeleteButton: function (getter) {
return getter('record.status') === 2;
},
hideAddButton: function (getter) {
return getter('record.status') === 1;
}
}
And then in your view you can bind:
{
text: 'Add',
reference: 'addButton',
bind: {
hidden: '{hideAddButton}'
}
}, {
text: 'Delete',
reference: 'deleteButton',
bind: {
hidden: '{hideDeleteButton}'
}
}
A working example: https://fiddle.sencha.com/#fiddle/mcg