ExtJS 6 ViewModel store bindings and setters - extjs

I am using ExtJS 6 to build a filterable dataview panel, this is my panel : Panel.js
Ext.define('myApp.view.main.widgets.Panel', {
extend: 'Ext.panel.Panel',
....
viewModel: {
type: 'widgets-view-model',
stores: {
widgets: {
type: 'widgets'
}
}
},
items: [{
xtype: 'searchfield',
name: 'widgets-filter',
height: 24,
width: '100%',
bind: {
store: '{store}'
}
}, {
margin: '5 0 0 0',
autoScroll: true,
bodyPadding: 1,
flex: 1,
frame: true,
referenceHolder: true,
bind: {
widgets: '{widgets.data.items}'
},
setWidgets: function (widgets) {
this.lookupReference('the-widget-items-panel').setWidgets(widgets);
},
items: [{
layout: {
type: 'vbox',
pack: 'start',
align: 'stretch'
},
items: [{
reference: 'the-widget-items-panel',
xtype: 'the-widgets'
}]
}]
}]
});
The viewModel does nothing;
Ext.define('myApp.view.main.widgets.WidgetsViewModel', {
extend: 'Ext.app.ViewModel',
alias:'viewmodel.widgets-view-model'
});
In the view controller, I do the following in the view's afterrender event handler;
onAfterRender: function () {
var store = this.view.getViewModel().getStore('widgets');
store.load();
}
At which point, the widgets setter "setWidgets" is called as expected. However when I filter the store using the store's "filterBy" method, I expect the widgets setter "setWidgets" to be called, but it isn't.
I also tried resetting the store by doing the following;
store.removeAll();
store.load(function () {
console.log('reloaded...!')
});
to see if a reload of store data will trigger the widgets setter "setWidgets", but it doesn't.
It appears changes to the viewModel store only triggers calls to setters once only.
QUESTION:
Is this a feature of the framework or am I configuring things incorrectly.
How do I re-configure view/store/viewModel so that the widgets setter "setWidgets" is called for every update/change to the store, i.e. when;
data is loaded
store is filtered
store data changes
store updates

Filtering a store doesn't change the items instance, it only changes the contents - that's why setWidgets isn't being called again. If you want to respond to data changing in the store, you need to listen to events from the store.
Also - binding on widget.data.items is a bad idea; that's plugging into the internal structure of the store. Listen to the events instead:
load
filterchange
datachanged
update

Related

Duplicate references when reusing the same component in Sencha app

Suppose we have defined a component (e.g. FieldSet) that we'd like to reuse in the single app (e.g. display/use it in 2 different modal windows.) This FieldSet has a reference, which we use to access it. The goal is to have these 2 windows contain independent fieldsets, so we can control and collect the inputs from each one separately.
Here's the sample fiddle that demonstrates the problem. As soon as any function triggers any lookupReference(...) call, Sencha issues the warning for "Duplicate reference" for the fieldset. It correctly creates two distinct fieldset components (by assigning different ids) on each window, but fails to properly assign/locate the references. As a result, any actions on one of these windows' fieldsets would be performed on the "unknown" one (probably on the first created one), messing up the UI behavior.
I see how it is a problem for Sencha to understand which component to use when operating on the reference, but there should be a way to reuse the same component multiple times without confusing the instances. Any help is greatly appreciated.
According to the docs on ViewController:
A view controller is a controller that can be attached to a specific view instance so it can manage the view and its child components. Each instance of the view will have a new view controller, so the instances are isolated.
This means that your use of singleton on your ViewController isn't correct, as it must be tied to a single view instance.
To fix this, I'd recommend making some modifications to your Fiddle, mainly removing the singleton: true from your VC class, accessing the views through lookup, and getting their VC's through getController to access your func method.
Ext.application({
name: 'Fiddle',
launch: function () {
/**
* #thread https://stackoverflow.com/questions/67462770
*/
Ext.define('fsContainerHandler', {
extend: 'Ext.app.ViewController',
alias: 'controller.fsContainerHandler',
// TOOK OUT singleton: true
func: function () {
var x = this.lookupReference('fsRef');
alert(x);
}
});
Ext.define('fsContainer', {
extend: 'Ext.container.Container',
xtype: 'xFSContainer',
controller: 'fsContainerHandler',
items: [{
xtype: 'fieldset',
title: 'myFieldset',
reference: 'fsRef'
}]
});
Ext.define('mainContainerHandler', {
extend: 'Ext.app.ViewController',
alias: 'controller.mainContainerHandler',
singleton: true,
onButton1Click: function () {
var win = this.getView().window1;
win.show();
// CHANGED LOGIC
win.lookup('theContainer').getController().func();
},
onButton2Click: function () {
var win = this.getView().window2;
win.show();
// CHANGED LOGIC
win.lookup('theContainer').getController().func();
}
});
Ext.define('mainContainer', {
extend: 'Ext.container.Container',
width: 400,
controller: 'mainContainerHandler',
window1: null,
window2: null,
initComponent: function () {
this.window1 = Ext.create('window1');
this.window2 = Ext.create('window2');
this.callParent(arguments);
},
items: [{
xtype: 'button',
text: 'Window 1',
reference: 'btn1',
handler: mainContainerHandler.onButton1Click,
scope: mainContainerHandler
}, {
xtype: 'button',
text: 'Window 2',
reference: 'btn2',
handler: mainContainerHandler.onButton2Click,
scope: mainContainerHandler
}]
});
Ext.define('window1', {
extend: 'Ext.window.Window',
title: 'Window1',
modal: true,
width: 100,
height: 100,
closeAction: 'hide',
// ADDED referenceHolder
referenceHolder: true,
items: [{
xtype: 'xFSContainer',
// ADDED reference
reference: 'theContainer'
}]
});
Ext.define('window2', {
extend: 'Ext.window.Window',
title: 'Window2',
modal: true,
width: 100,
height: 100,
closeAction: 'hide',
// ADDED referenceHolder
referenceHolder: true,
items: [{
xtype: 'xFSContainer',
// ADDED reference
reference: 'theContainer'
}]
});
Ext.create('mainContainer', {
renderTo: document.body
});
}
});

How to properly define properties in ExtJS custom class

I am attempting to create a reusable title bar for our grids. This will require a couple of properties which can be set when the grid title bar is used. The problem I am running into is that the property is undefined when I attempt to use it.
I looked at how ExtJS appears to do this and saw that they set up their properties in the config block. So I tried that with no luck. I have also tried removing the config block and adding the property with the same result.
Ext.define('ERM.view.mastersite.GridTitleBar', {
extend: 'Ext.TitleBar',
xtype: 'gridtitlebar',
margin: '0 0 20 0',
shadow: true,
cls: 'x-big',
style: {
border: 'solid lightgrey 2px'
},
config: {
addNewToolTip: 'test',
},
items: [{
xtype: 'button',
iconCls: 'md-icon-add-circle',
text: 'Add',
align: 'right',
tooltip: this.parent.addNewToolTip,
}],
});
I'm expecting the tool tip to show "test" by default, or if the default is overridden, I'm expecting it to show the overridden string.
Edit
Second attempt based on the answers below.
Ext.define('ERM.view.mastersite.GridTitleBar', {
extend: 'Ext.TitleBar',
xtype: 'gridtitlebar',
margin: '0 0 20 0',
shadow: true,
cls: 'x-big',
style: {
border: 'solid lightgrey 2px'
},
config: {
addNewToolTip: 'test',
},
initialize: function () {
const me = this;
me.items = [{
xtype: 'button',
text: 'Add',
iconCls: 'md-icon-add-circle',
tooltip: me.getAddNewToolTip(),
}];
this.callParent();
},
});
if you are using modern toolkit use initialize method instead of initComponent
You're correctly defining the property. Your problem is the way you are accessing it.
At the time of building the items construct, the this context is whatever has loaded the file - probably the boot loader. Strangely enough, that won't have your new property.
In order to access properties of your new class/object, you need to define the items construct after the object is created. One good location for that is in the initComponent method.
Ext.define('ERM.view.mastersite.GridTitleBar', {
// in here, the 'this' context is whatever loaded the file.
...
config: {
addNewToolTip: 'test',
},
...
initComponent: function() {
// In here, like most methods in ExtJS, the 'this' context is the owning instance.
this.items = [{
xtype: 'button',
...
// Oh, and it's a good idea to use the accessors for config variables
tooltip: this.getAddNewToolTip()
}]
// don't forget to call the parent.
this.callParent(arguments);
});

ExtJs 6: How to bind data between two grids?

I have 3 items in my main panel (Main.js):
Form (display data from selected row of Grid1)
Grid1 -Ext.grid.Panel (which get data from JSON file)
Grid2 -Ext.grid.Panel (should display some of the columns from selected row in Grid1)
All 3 views share same MainModel.js associated with Main.js.
I am able to bind Grid1 data to form using formula:
formulas: {
someVal: {
bind: '{employeeDetails.selection}', //reference to grid1
get: function(item){
return item;
}
},
Form-
items:[
{
xtype: 'form',
title: 'Form',
defaultType: 'textfield',
items: [
{
xtype: 'displayfield',
fieldLabel: 'ID',
bind: '{someVal.id}'
}, //...
But I cannot find any way to do the same between Grid1 and Grid2. I googled for hours. Only source for data for ExtJs grid seems to be store. Essentially is there any way to populate grid other than using store. Can we use bind inside columns or something? Thanks.
EDIT:
updated formula for selection:
myStoreDataFormula: {
bind:{
bindTo:'{employeeDetails.selection}',
deep:true
},
get: function(employee){
if(employee!=null){
var empArray = [];
empArray.push(employee.data);
return empArray;
}
}
}
A somewhat obscure feature when using a store defined inside a viewmodel, is that instead of defining concrete values, you can use the '{ ... }' mustache to bind to other viewmodel fields / formulas, or component configs that are published via their reference (personally I found this most useful for putting path variable's into url of the store's proxy).
Here is an example of grid bound to store, which in turn has it's data bound to a formula:
Ext.define('MyView', {
viewModel: {
stores: {
myStore: {
fields: ['name'],
data: '{myStoreDataFormula}'
}
},
formulas: {
myStoreDataFormula: function(get) {
return [{
name: 'Foo'
}, {
name: 'Bar'
}];
}
}
},
extend: 'Ext.grid.Panel',
xtype: 'MyView',
bind: {
store: '{myStore}'
},
columns:[{
dataIndex: 'name',
flex: 1
}]
});
Ext.application({
name : 'Fiddle',
launch : function() {
Ext.create({
xtype: 'MyView',
width: 300,
height: 300,
renderTo: Ext.getBody()
});
}
});
Yes, this would still have you to have 2 stores, but you can make second grid's store to be fully dependent on the first grid's published selection config.

Using grid as field in ExtJS

Binding model property to a form field is pretty easy in ExtJS:
// ...skipped everything until fields config for brevity
}, {
xtype: 'textfield',
bind: '{modelInstance.someField}'
}, { // ...
In this case, modelInstance string field someField will be synchronized to textbox value, thanks to two way binding. And that is great.
What I want to achieve is to get same kind of behavior in the case when model field is not a string but an array. This is the model:
Ext.define('namespace.model.CustomModel', {
fields: ['easyField', {
name: 'hardField',
type: 'auto' // This will be an array during runtime
}],
idProperty: 'easyField'
});
I would like to do something like this:
// ...skipped everything until fields config for brevity,
// assume that viewmodel and everything else are set up as expected
}, {
xtype: 'textfield',
bind: '{modelInstance.easyField}'
}, {
xtype: 'gridfield',
bind: {
gridArray: '{modelInstance.hardField}'
}
}, { // ...
Understandably, I want gridfield component to extend Ext.grid.Panel and synchronize its store data to modelInstance field hardField.
Currently I have this:
Ext.define('namespace.form.field.GridField', {
extends: 'Ext.grid.Panel',
xtype: 'gridfield',
// skip requires for brevity
viewModel: {
stores: {
gridFieldItems: {
type: 'gridfielditems' // Simple in memory store
}
},
data: {
}
},
store: '{gridFieldItems}',
// This config enables binding to take place as it creates getters and setters,
// gridArray is set initially to '{modelInstance.hardField}' as expected
config: {
gridArray: null
},
// This is necessary for this grid-field component to update
// '{modelInstance.hardField}' back in the owners viewModel.
publishes: {
gridArray: true
},
// ???
// bind: {
// gridArray: bind gridArray to store data somehow?
// }
});
Here's the problem:
how do I inject existing modelInstance.hardField array as gridFieldItems store initial data,
how do I bind gridArray config to store data so that it is updates as we go along cruding the grid,
do all of these in an elegant MVVM way without writing a bunch of listeners trying to force syncing between JS objects.
Please provide tested solution which is known to work, I already tried a lot of different ways myself, but without success so far.
Here is a working fiddle to achieve this binding. The easy way is to bind the array field with "data" attribute of a store.
A good suggestion on the work you've done is to avoid defining a viewmodel inside a generic component (gridfield) but use viewmodels only on you application specific views.
On you generic component you should define only configuration attributes with setter/getter/update logics in order to be able to use them with bind. In this case there is no need of custom properties as the store is enough.
edit
To avoid the "easy binding" you can implement the set/get logic of the array in your girdfield component. Fore example using the "updateGridArray" method called by the setter and the "datachanged" event of the store.
The fiddle is updated and the example girdfield uses the cell editing plugin to show the 2-way binding.
fiddle: https://fiddle.sencha.com/#view/editor&fiddle/2a3b
Ext.define('Fiddle.model.CustomModel', {
extend: 'Ext.data.Model',
fields: ['easyField', {
name: 'hardField',
type: 'auto' // This will be an array during runtime
}],
idProperty: 'easyField'
});
Ext.define('Fiddle.fiddleview.CustomViewModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.fiddleview',
data: {
currentModel: null
}
});
Ext.define('Fiddle.form.field.GridField', {
extend: 'Ext.grid.Panel',
xtype: 'gridfield',
config: {
gridArray: null
},
publishes: [
'selection',
'gridArray'
],
selModel: 'cellmodel',
plugins: {
cellediting: {
clicksToEdit: 1
}
},
columns: [{
text: 'Column 1',
flex: 1,
editor: true,
dataIndex: 'field1'
}],
initComponent: function () {
this.store = {
fields: ['field1'],
data: [],
listeners: {
scope: this,
datachanged: function (store) {
this.setGridArray(store.getRange().map(function (record) {
return record.getData();
}));
}
}
};
this.callParent();
},
updateGridArray: function (gridArray) {
this.getStore().suspendEvent('datachanged');
if (Ext.isEmpty(gridArray)) {
this.getStore().removeAll();
} else {
this.getStore().loadData(gridArray);
}
this.getStore().resumeEvent('datachanged');
}
});
var myView = Ext.create('Ext.container.Container', {
renderTo: Ext.getBody(),
viewModel: 'fiddleview',
items: [{
xtype: 'gridfield',
bind: {
gridArray: '{currentModel.hardField}'
}
}]
});
// Bind on model's array to check two-way update
// It will execute also on cell edit
myView.getViewModel().bind('{currentModel.hardField}', function (value) {
window.alert('model\'s array changed');
});
// The binding is now active, when the "currentModel" in the viewmodel changes, grid rows are refresched
myView.getViewModel().set('currentModel', new Fiddle.model.CustomModel({
hardField: [{
field1: 'value1'
}]
}));
// Also when data changes in the "currentModel"
myView.getViewModel().get('currentModel').get('hardField').push({
field1: 'value2'
});

ExtJS grid combo renderer not working

I have a grid in my ExtJS 4.2.1 application that has an editable column with combo editor.
I need to render the column with the value from the DisplayField of the combo but the comboStore.getCount() = 0
Here is my grid:
Ext.define('App.employee.Grid', {
extend: 'Ext.grid.Panel',
requires: ['Ext.grid.plugin.CellEditing'],
alias: 'widget.employee.grid',
config: {
LocationId: 0
},
initComponent: function() {
var me = this,
store = me.buildStore(),
comboStore = Ext.create('App.store.catalog.Location', { autoLoad: true });
me.rowEditing = Ext.create('Ext.grid.plugin.RowEditing', {
clicksToMoveEditor: 1,
autoCancel: false,
listeners: {
edit: function(editor, e) {
}
}
});
me.cellEditing = new Ext.grid.plugin.CellEditing({
clicksToEdit: 1
});
Ext.applyIf(me, {
plugins: [me.rowEditing],
columns: [{
xtype: 'rownumberer',
text: '#',
width: 50,
sortable: false,
align: 'center'
//locked: true
},{
text: 'Number',
dataIndex: 'EmployeeNumber',
align: 'center',
width: 90
}, {
text: 'Name',
dataIndex: 'EmployeeName',
flex: 1
}, {
text: 'Location',
dataIndex: 'LocationId',
width: 140,
renderer: function(value) {
// HERE!!!
// me.comboStore.getCount() = 0 so I never get a record
var record = me.comboStore.findRecord('LocationId', value);
return record.get('Description');
},
editor: {
xtype: 'combobox',
typeAhead: true,
triggerAction: 'all',
store: comboStore,
displayField: 'Description',
valueField: 'LocationId',
queryMode: 'local',
listConfig: {
width: 250,
// Custom rendering template for each item
getInnerTpl: function() {
return '<b>{Code}</b><br/>(<span style="font-size:0.8em;">{Description}</span>)';
}
}
}
}],
store: store,
});
me.callParent(arguments);
}
});
The problem is in the renderer function because the comboStore is always empty. The strange thing is that in my view If I click to edit the row and open the combo the combo has values.
[UPDATE]
What I think is that my comboStore has a delay when loading so the renderer is fired before the comboStore gets loaded. I figure this out because if I debug in chrome and I wait a few seconds, then it works... but don't know how to force to wait until comboStore is loaded.
Any clue on how to solve this? Appreciate any help.
A couple of solutions:
Ensure that the combo store is loaded before the grid store. This can be done by loading the combo first and from load event of its store trigger the grid store load. The disadvantage is that it adds unnecessary delay so this method impairs the user experience.
Load all needed stores in one request. It requires a bit of coding but it saves server roundtrip so it's a very valuable approach. Store loadData method is used to actually load store with the received data. You would, of course, first call it on combo, then on the grid.
The best method that I use almost exclusively is to turn the whole thing upside down and link display field to the store, not value field. Server must return both display and value fields in the grid store and a little piece of code that updates both fields in the grid store after editing is complete is required. This method is demonstrated here: Remote Combo in ExtJS Grid

Resources