9

I have two models: Page and Department. I am showing pages in a list view in extjs and I would like to display the Department's Name instead of the department_id in the List view. I have not gotten to the part of actually adding the department to the page VIA the GUI, only through direct db insert statements, but I would like to at least be able to display the department name in the list view.

I have the following so far, this is showing department_id

models

Ext.define('ExtMVC.model.Department', {
    extend: 'Ext.data.Model',
    fields: ['name']
});

Ext.define('ExtMVC.model.Page', {
    extend: 'Ext.data.Model',
    fields: ['title','body','department_id'],
    associations: [
        {type: 'belongsTo', model: 'Department'}
    ]
});

stores

Ext.define('ExtMVC.store.Pages', {
    extend: 'Ext.data.Store',
    model: 'ExtMVC.model.Page',
    autoLoad: true,
    proxy: {
      type: 'rest',
      url: '/admin/pages',
      format: 'json'
    }
});
Ext.define('ExtMVC.store.Departments', {
    extend: 'Ext.data.Store',
    model: 'ExtMVC.model.Department',
    autoLoad: true,
    proxy: {
      type: 'rest',
      url: '/admin/departments',
      format: 'json'
    }
});

List View

Ext.define('ExtMVC.view.page.List' ,{
    extend: 'Ext.grid.Panel',
    alias : 'widget.pagelist',

    title : 'All Pages',
    store: 'Pages',

    initComponent: function() {
        this.tbar = [{
            text: 'Create Page', action: 'create'
        }];

        this.columns = [
            {header: 'Title',       dataIndex: 'title',       flex: 1},
            {header: 'Department',  dataIndex: 'department_id',  flex: 1}
        ];
        this.callParent(arguments);
    }
});

controller (fwiw)

Ext.define('ExtMVC.controller.Pages', {
    extend: 'Ext.app.Controller',

    init: function() {
      this.control({
            'pagelist': {
                itemdblclick: this.editPage
            },
            'pagelist > toolbar > button[action=create]': {
                click: this.onCreatePage
            },
            'pageadd button[action=save]': {
              click: this.doCreatePage
            },
            'pageedit button[action=save]': {
              click: this.updatePage
            }
        });
    },

    onCreatePage: function () {
      var view = Ext.widget('pageadd');
    },

    onPanelRendered: function() {
        console.log('The panel was rendered');
    },

    doCreatePage: function (button) {
      var win = button.up('window'),
      form = win.down('form'),
      values = form.getValues(),
      store = this.getPagesStore();
      if (form.getForm().isValid()) {
        store.add(values);
        win.close();
        this.getPagesStore().sync();
      }
    },

    updatePage: function (button) {
        var win = button.up('window'),
            form = win.down('form'),
            record = form.getRecord(),
            values = form.getValues(),
            store = this.getPagesStore();
        if (form.getForm().isValid()) {
            record.set(values);
            win.close();
            this.getPagesStore().sync();
        }
    },

    editPage: function(grid, record) {
      var view = Ext.widget('pageedit');
      view.down('form').loadRecord(record);
    },

    stores: [
        'Pages',
        'Departments'
    ],

    models: [
      'Page'

    ],

    views: [
        'page.List',
        'page.Add',
        'page.Edit'
    ]
});
jdkealy
  • 4,807
  • 6
  • 34
  • 56
  • 1
    I would just bring in the department name with the Page record and map it in the Model so that I could use it in the grid. – dbrin Feb 22 '13 at 23:53
  • Any joy with sorting this ? running into similar questions myself ... – Manse Apr 26 '13 at 10:28

3 Answers3

9

Ext's associations have visibly not been designed for use in stores, but rather for working with single records... So, I agree with what has already been said, that you'd better flatten your model on the server-side. Nevertheless, it is possible to achieve what you want.

The association won't load your associated model (i.e. Department) until you call the generated getter method (i.e. getDepartment()). Trying to go this way, that is calling this method for each Page record loaded in your store would require an incredible amount of hack because the grid reacts synchronously to the refresh event of the store, while the getDepartment() method returns asynchronously...

That's why you will have to load your departments data in the same request that loads your pages. That is, your server must return records of the form:

{title: 'First Page', body: 'Lorem', department_id: 1, department: {name: 'Foo'}}

In order for your Page model's proxy to consume this, you need to configure your association this way:

Ext.define('ExtMVC.model.Page', {
    // ...
    associations: [{
        type: 'belongsTo'
        // You need the fully qualified name of your associated model here
        // ... which will prevent Ext from generating everything magically
        ,model: 'ExtMVC.model.Department'

        // So you must also configure the getter/setter names (if you need them)            
        ,getterName: 'getDepartment'

        // Child data will be loaded from this node (in the parent's data)
        ,associationKey: 'department'

        // Friendly name of the node in the associated data (would default to the FQ model name)
        ,name: 'department'
    }]
});

Then comes the really ugly part. Your grid's columns cannot access the associated data with the classic dataIndex property. However, provided the associated records have already been loaded, this can be accessed from a TemplateColumn like this:

{
    header: 'Department'
    ,xtype: 'templatecolumn'
    ,tpl: '{department.name}'
}

Unfortunately, that will prevent you from using some more appropriate column class (date column, etc.), that you may have configured globally. Also, this column loses track of the model field it represents, which means that some features based on introspection won't be able to do their magic (the grid filter ux, for example, uses the field type to decide automatically on the type of filter).

But in the particular case you've exposed, that won't matter...

Complete Example

Here's what it gives when you bring it all together (or see it in action)...

Ext.define('ExtMVC.model.Department', {
    extend: 'Ext.data.Model',
    fields: ['name'],
    proxy: {
        type: 'memory'
        ,reader: 'json'
        ,data: [
            {id: 1, name: 'Foo'}
            ,{id: 2, name: 'Bar'}
            ,{id: 30, name: 'Baz'}
        ]
    }
});

Ext.define('ExtMVC.model.Page', {
    extend: 'Ext.data.Model',
    fields: ['title','body','department_id'],
    associations: [{
        type: 'belongsTo'
        ,model: 'ExtMVC.model.Department'
        ,getterName: 'getDepartment'
        ,associationKey: 'department'
        ,name: 'department'
    }],
    proxy: {
        type: 'memory'
        ,reader: 'json'
        ,data: [
            {title: 'First Page', body: 'Lorem', department_id: 1, department: {name: 'Foo'}}
            ,{title: 'Second Page', department: {name: 'Bar'}}
            ,{title: 'Last Page', department: {name: 'Baz'}}
        ]
    }
});

Ext.define('ExtMVC.store.Pages', {
    extend: 'Ext.data.Store',
    model: 'ExtMVC.model.Page',
    autoLoad: true
});

Ext.define('ExtMVC.view.page.List', {
    extend: 'Ext.grid.Panel',
    alias : 'widget.pagelist',

    title : 'All Pages',
    store: Ext.create('ExtMVC.store.Pages'),

    initComponent: function() {
        this.tbar = [{
            text: 'Create Page', action: 'create'
        }];

        this.columns = [
            {header: 'Title', dataIndex: 'title', flex: 1}
            ,{header: 'Department', xtype: 'templatecolumn', flex: 1, tpl: '{department.name}'}
        ];

        this.callParent(arguments);
    }
});

Ext.widget('pagelist', {renderTo: 'ct', height: 200});
rixo
  • 23,815
  • 4
  • 63
  • 68
1

As one of the comments mentions, it would be a lot simpler to bind the department name to the Pages model when working with a grid/list. Denormalisation for display is not a bad thing.

An alternative is perhaps reverse the modelling, in that you could say the a department 'hasMany' Pages.

This allows you define the primary and foreign keys on the relationship and then your department store will (per row) have a automatic 'pages()' store which will contain the child info.

I've typically done this for master/detail forms where I want to bind 'pages()' to a list/grid, but keep the department model as the master record on a form for example.

dougajmcdonald
  • 19,231
  • 12
  • 56
  • 89
0

Why shouldn't use renderer config function on the grid's column ?

This implies to (auto)load the departments store upfront on application launch (you might need it in more places anyway, think of grid cell editor as full department list).

{
  name:'Department',
  dataIndex:'department_id',
  renderer: function(deptId){
    return Ext.data.StoreManager.lookup('Departments').getById(deptId).get('name');
  }
}

PS: I was using myself denormalisation for display, but doesn't feel good ;)

qdev
  • 1,371
  • 1
  • 15
  • 18