0

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...

boggy
  • 3,674
  • 3
  • 33
  • 56
  • 1
    Are you sure that the `url` config is bindable? If I check the docs, neither `proxy` of `Ext.data.Store` nor `url` of `Ext.data.proxy.Ajax` is marked as `bindable`. – Peter Koltai Jan 27 '23 at 08:36
  • Very good question. I thought it is. I use Sencha Architect. In SA the attribute is marked as bindable (it has the magnet icon). Anyway, just see the fiddle - uncomment the `vm.notify()` line and you'll see that it starts working properly. It's not even necessary to place the property in a bind object. ExtJs does something with it. Hmmm... – boggy Jan 27 '23 at 17:21
  • @PeterKoltai I updated my answer with more info. – boggy Jan 27 '23 at 17:39
  • It's interesting. Usually Ext JS docs mark `bindable` all the configs which can be binded. But I see the test you linked and it seems it works. – Peter Koltai Jan 27 '23 at 22:02
  • 1
    Here is an old SO question that sounds familiar: https://stackoverflow.com/questions/26368253/extjs-store-with-data-bound-url-not-re-evaluating-data-properties – Peter Koltai Jan 27 '23 at 22:03
  • From this it seems you may be right, "maybe it is by design", to optimize ViewModel updates. – Peter Koltai Jan 27 '23 at 22:04

2 Answers2

0

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

  1. Listen for initViewModel method in controller and then load the store
  2. Or add afterrender event on the grid, read its store and then load it
  3. Add delay before triggering the event
// Add delay to ensure view model is initialized
Ext.defer(function() {
  this.fireEvent('loadData', record);
}, 1000, this);
Gaurav
  • 171
  • 4
0

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();
    }