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.
Related
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'
});
I am facing an issue with Date Field in ExtJs. My view model data does not bind to the date field correctly. From server, if my date gets rendered like this
"2010-07-13T01:02:10.000Z" it binds correctly. But if it gets rendered like "2010-07-13T01:02:10", it does not bind correctly to the date field.
Is there something we need to do to bind the data to date field? I cannot post the actual code but I tried creating a small sample but in that both formats are not binding correctly. But if i specify '2010-07-13', it binds correctly.
ExtJS Version 6.0.1.250
Ext.define('Plus.view.MainM', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.main',
data: {
StartDate: '2010-07-13T01:15:20.000Z',
Name: 'Kapil'
}
});
Ext.define('Plus.view.MainV', {
extend: 'Ext.form.Panel',
xtype: 'app-main',
viewModel: {
type: 'main'
},
items: [{
xtype: 'textfield',
itemId: 'Name',
bind: '{Name}',
},
{
xtype: 'datefield',
itemId: 'StartDate',
width: 105,
bind: '{StartDate}'
}]
});
Ext.application({
name: 'Plus',
extend: 'Ext.app.Application',
autoCreateViewport: 'MainV',
launch: function() {
}
});
Thanks for the help.
I'm sure there's something I'm missing here and I just can't see what it is.
I have demo project I'm building in extjs 6. In it I have a grid of inventory items.
Ext.define("InventoryDemo.view.inventory.list.Inventory",{
extend: "Ext.container.Container",
xtype: 'inventory',
requires: [
"InventoryDemo.view.inventory.list.InventoryController",
"InventoryDemo.view.inventory.list.InventoryModel"
],
controller: "inventory-inventory",
viewModel: {
type: "inventory-inventory"
},
closable: true,
listeners:{
refreshList: 'onRefreshList'
},
layout:{
type: 'hbox',
align: 'stretch'
},
items:[
{
xtype: 'grid',
flex: 1,
tbar:[
{xtype: 'button', text: 'New Item', handler: 'newInventoryItem'}
],
bind:{
store: '{inventory}'
},
listeners:{
itemclick: 'showDetails'
},
columns:[
{ text: 'Name', dataIndex: 'name', flex: 1 },
{ text: 'Price', dataIndex: 'price' },
{ text: 'Active', dataIndex: 'active' },
]
}
]
});
When you click on a row, a new detail panel is created and the selected record is linked to its viewmodel and it's added to the container view that holds the grid. I also want to use the same detail panel when creating a new inventory record so I extracted the shared logic for creating and editing so it can be reused in the controller.
Here's the list's controller:
Ext.define('InventoryDemo.view.inventory.list.InventoryController', {
extend: 'Ext.app.ViewController',
alias: 'controller.inventory-inventory',
config:{
// holds the newly created detail panel
detailsPanel: null
},
showDetails: function (grid, record, item, index, e, eOpts){
this.createDetailsPanel();
this.addTitleToDetailsPanel(record.get('name'));
// This creates the link in the new detail panel's viewmodel for the
// selected record. We specifically do NOT do this in the
// `newInventoryItem`.
details.getViewModel().linkTo('inventoryitem', record);
this.addDetailsPanelToView();
},
newInventoryItem: function (button, e){
this.createDetailsPanel();
this.addTitleToDetailsPanel('New Item');
// I thought that because the previous panel was destroyed during the
// `createDetailsPanel` method any previously linked record would not
// be linked to the new detail panel created and that not linking here
// would give me an empty detail panel.
this.addDetailsPanelToView();
},
createDetailsPanel: function (){
if(this.getDetailsPanel() !== null){
// I'm destroying any previous view here which, as I understand,
// would also destroy the the associated ViewController and ViewModel
// which would also kill any links to the viewmodel
this.getDetailsPanel().destroy();
}
details = Ext.create('InventoryDemo.view.inventory.details.Inventory',{
session: true,
listeners:{
refreshList: 'onRefreshList'
}
});
this.setDetailsPanel(details);
},
addDetailsPanelToView: function (){
this.getView().add(this.getDetailsPanel());
},
addTitleToDetailsPanel: function (title){
this.getDetailsPanel().setTitle("<h3>" + title + "</h3>");
},
onRefreshList: function (){
this.getViewModel().get('inventory').load();
}
});
The details panel being created looks like this:
Ext.define("InventoryDemo.view.inventory.details.Inventory",{
extend: "Ext.form.Panel",
requires: [
"InventoryDemo.view.inventory.details.InventoryController",
"InventoryDemo.view.inventory.details.InventoryModel"
],
controller: "inventory-details-inventory",
viewModel: {
type: "inventory-details-inventory"
},
flex: 1,
closable: true,
bodyPadding: 10,
reference: 'inventorydetails',
defaults:{
layout: 'anchor',
anchor: '50%'
},
dockedItems:[
{
xtype: 'toolbar',
dock: 'bottom',
ui: 'footer',
items:[
{xtype: 'button', text: 'Update', handler: 'updateRecord'},
{xtype: 'button', text: 'Delete', handler: 'deleteRecord'}
]
}
],
items:[
{
xtype: 'hiddenfield',
name: '_method',
value: 'PUT'
},
{
xtype: 'fieldset',
title: 'IDs',
collapsible: true,
defaults:{
xtype: 'textfield'
},
items:[
{
name: 'id',
fieldLabel: 'ID',
readOnly: true,
bind: '{inventoryitem.id}'
},
{
name: 'brand_id',
fieldLabel: 'Brand ID',
readOnly: true,
bind: '{inventoryitem.brand_id}'
}
]
},
{
xtype: 'fieldset',
title: 'Details',
defaults:{
xtype: 'textfield'
},
items:[
{
name: 'name',
fieldLabel: 'Name',
bind: '{inventoryitem.name}'
},
{
name: 'price',
fieldLabel: 'Price',
bind: '{inventoryitem.price}'
}
]
}
]
});
The problem that I'm running into is if I click on a row to view it's details (which works) and then click on the New Item button, the record that was loaded on the previous detail panel is still loaded on the new detail panel.
If I click the New Item button first I get the blank form I'm going for and if I select different item rows each of the records from the selected row loads into the detail panel correctly (it's not a situation where the record from the first row is "stuck" in the detail panel), but as soon as I select a row, the New Item button will only give me forms with the previously loaded record.
Is there something that would make a link to a viewmodel persist between the destruction and creation of two separate views/viewmodels/viewcontrollers (or is there a flaw in my controller logic that I'm just not seeing)?
if the only thing you need in the detail's viewModel is this singular property you are trying to link to, consider not using a stand-alone viewModel for the detail at all.
When your detail panel resides in the items of your inventory view, it actually has natural access to the view's viewModel (detailPanel.lookupViewModel() will return the single closest viewModel in the component tree hierarchy). Bindings should work too.
However, if you need a separate viewModel for the detail panel, you can create the detail view with an ad-hoc viewModel config that gets merged into the detail's viewModel upon instantiation.
The view:
Ext.define('APP.view.TheView',{
extend: "Ext.container.Container",
alias: 'widget.theView',
controller: 'theView',
viewModel: { type: 'theView' },
config: {
detailConfig: { xtype: 'theDetail', reference: 'detail' }
},
items: ...
};
The view's viewController:
Ext.define('APP.controller.TheView',{
extend: "Ext.app.ViewController",
alias: 'controller.theView',
onOpenDetail: function(){
var detail = this.lookupReference('detail');
if(!detail){
var view = this.getView(),
detailConfig = view.getDetailConfig(),
theProperty = this.getViewModel().get('theProperty');
detailConfig = Ext.apply({
viewModel: {
// This actually gets correctly merged with whatever there is
// in the viewModel configuration of the detail
data: { theProperty: theProperty }
}
}, detailConfig));
detail = view.add(detailConfig);
}
// Do something with the detail instance
}
};
And the detail:
Ext.define('APP.view.TheDetail',{
extend: "Ext.panel.Panel",
alias: 'widget.theDetail',
controller: 'theDetail',
viewModel: { type: 'theDetail' },
items: ...
};
Hope this helps a little! :-)
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
I'm using ExtJS 4.1.2, and I have an Ext.grid.Panel with a check-box selection model and a text-box in the header for filtering the items in the grid:
var nameStore = new Ext.data.Store({
...
proxy: { type: 'ajax' ... },
fields: [ { name: 'name', type: 'string' }, ... ],
});
var headerBar = new Ext.toolbar.Toolbar({
xtype: 'toolbar',
dock: 'top',
layout: 'fit',
items: [{
xtype: 'textfield',
...,
listeners: {
change: function(fld, newVal, oldVal, opts) {
var grid = ...;
if (newVal.length > 0)
grid.store.filterBy(function(record, id) { return record.get('name').indexOf(newVal) !== -1; });
else
grid.store.clearFilter()
}
}
}]
});
var nameGrid = new Ext.grid.Panel({
...
selType: 'checkboxmodel',
selModel: { mode: 'SIMPLE' },
store: nameStore,
columns: [ { dataIndex: 'name', flex: true }, ... ],
dockedItems: [ headerBar ],
bbar: [ { xtype: 'tbtext', text: '0 items selected' ... },
listeners: {
selectionchange: function(selModel, selected, opts) {
var txt = ...;
txt.setText(Ext.String.format("{0} items selected", selected.length));
}
}
});
As shown nameStore's filter is applied (or removed) based on the value in headerBar's textbox. Also, a text string in the bottom bar is updated as items are selected/deselected.
Each of these features is working smoothly on its own. But the interaction is giving me trouble: if an item is selected and then gets filtered out, it is deselected and then remains so when the filter is cleared.
How can I maintain the selections (or at least appear to do so to the user) of "hidden" items, either for use elsewhere in my application or to reselect when the filter is cleared? Thanks!
you need not to add the selected grid separately. This can be done in one grid only. Simple method is to have an array variable in page scope and capture the grid select event or itemclick event whatever you want.
e.g if you use select event it will give you your record.
select( this, record, index, eOpts )
you can get your record id and push it to the array variable that you declared.
Once you filtered out the grid. you can loop through the filtered records and call select method by getting selection model.
e.g
grid.getSelectionModel().select(record);
Hope this will help.