ExtJS-复杂的表单序列化

ExtJS - Complex form serialization

本文关键字:序列化 表单 复杂 ExtJS-      更新时间:2023-09-26

我有一个复杂的表单,不能使用表单序列化技术。表单中有许多字段以及动态网格(根据用户选择某些标准动态生成的网格)

我想做的是,收集用户输入/选择+添加网格中可用的选定记录,然后最终用这些数据制作一个JSON数组,以便能够在服务器端发布。

我想,我可以使用ExtJS的getCmp函数来获取整个数据,但正如您可能猜测的那样,这种方式有点难以维护。此外,我不知道用这种方式获取网格数据!

PS:代码有点长,所以我裁剪了一些部分。

模型和存储定义

Ext.Loader.setConfig({enabled: true});
Ext.Loader.setPath('Ext.ux', '<?php echo js_url(); ?>resources/ux');
Ext.require([
  'Ext.grid.*',
  'Ext.data.*',
  'Ext.form.*',
  'Ext.state.*',
  'Ext.util.*',
  'Ext.layout.container.Column',
  'Ext.selection.CheckboxModel',
  'Ext.ux.RowExpander',
  'Ext.ux.statusbar.StatusBar'  
]);
var navigate = function (panel, direction) {
    var layout = panel.getLayout();
    layout[direction]();
    Ext.getCmp('move-prev').setDisabled(!layout.getPrev());
    Ext.getCmp('move-next').setDisabled(!layout.getNext());
};
// Article Model
Ext.define('Article', {
    extend: 'Ext.data.Model',
    fields: [
        {name: 'ARTICLE_ID', type: 'int'},
        {name: 'DESCRIPTION', type: 'string'}
    ]
});
// Article Json Store
var articles = new Ext.data.JsonStore({
    model: 'Article',
    proxy: {
        type: 'ajax',
        url: '<?php echo base_url() ?>create/get_articles',
        reader: {
            type: 'json',
            root: 'artList',
            idProperty: 'ARTICLE_ID'
        }
    }
});
winArticle = new Ext.Window({
width: 600,
modal: true,
title: 'Artikel Seçimi',
closeAction: 'hide',
bodyPadding: 10,
items: new Ext.Panel({
    items: [
        {
            xtype: 'fieldset',
            title: 'Artikel Sorgulama',
            defaultType: 'textfield',
            layout: 'anchor',
            defaults: {
                anchor: '100%'
            },
            height: '76px',
            items: [
                {
                    xtype: 'fieldcontainer',
                    layout: 'hbox',
                    defaultType: 'textfield',
                    items: [
                        {
                            xtype: 'combobox',
                            id: 'articleNo',
                            inputWidth: 320,
                            fieldLabel: 'ARTİKEL NO',
                            fieldStyle: 'height: 26px',
                            margin: '10 15 0 0',
                            triggerAction: 'query',
                            pageSize: true
                        },
                        {
                            xtype: 'button',
                            text: 'SORGULA',
                            width: 100,
                            scale: 'medium',
                            margin: '8 0 0 0'
                        }
                    ]
                }
            ]
        },
        {
            xtype: 'fieldset',
            title: 'Artikel Bilgileri',
            height: '140px',
            layout: 'fit',
            items: [
                {
                    xtype: 'fieldcontainer',
                    layout: 'hbox',
                    defaultType: 'textfield',
                    fieldDefaults: {
                        labelAlign: 'top'
                    },
                    items: [
                        {
                            fieldLabel: 'ARTİKEL TANIMI',
                            name: 'artDesc',
                            flex: 3,
                            margins: '0 5 0 0'
                        },
                        {
                            fieldLabel: 'PAKET İÇERİĞİ',
                            name: 'artgebi',
                            flex: 1
                        }
                    ]
                },
                {
                    xtype: 'fieldcontainer',
                    layout: 'hbox',
                    defaultType: 'textfield',
                    id: 'artContainer1',
                    fieldDefaults: {
                        labelAlign: 'top'
                    },
                    items: [
                        {
                            fieldLabel: 'SUBSYS',
                            name: 'artSubsys',
                            flex: 1,
                            margins: '0 5 0 0'
                        },
                        {
                            fieldLabel: 'VARIANT',
                            name: 'artVariant',
                            flex: 1,
                            margins: '0 5 0 0'
                        },
                        {
                            fieldLabel: 'VARIANT TANIMI',
                            name: 'artVariantDesc',
                            flex: 2
                        }
                    ]
                }
            ]
        },
        {
            xtype: 'fieldset',
            title: 'Aksiyon Seviyeleri',
            id: 'article-fieldset',
            items: [
                {
                    xtype: 'button',
                    id: 'btnArticleLevelAdd',
                    text: 'LEVEL EKLE',
                    scale: 'medium',
                    width: 100,
                    style: 'float: right',
                    margin: '0 7 0 0',
                    handler: function() {
                        var getLevels = function() {
                            var count = winArticle.down('fieldset[id=article-fieldset]').items.items.length;
                            return count;
                        }
                        var count = getLevels();
                        if (count === 3) {
                            Ext.getCmp('btnArticleLevelAdd').disable();
                        }
                        var container = 'artContainer' + count;
                        //console.log(container);
                        Ext.getCmp('article-fieldset').add([
                            {
                                xtype: 'fieldcontainer',
                                layout: 'hbox',
                                id: 'artContainer' + count,
                                defaultType: 'textfield',
                                fieldDefaults: {
                                    labelAlign: 'top'
                                },
                                items: [
                                    {
                                        name: 'artLevel' + count,
                                        allowBlank: false,
                                        inputWidth: 216,
                                        fieldStyle: 'text-align: right; font-size: 13pt; background-color: #EAFFCC;',
                                        margins: '0 5 5 0'
                                    },
                                    {
                                        name: 'artValue' + count,
                                        allowBlank: false,
                                        inputWidth: 216,
                                        fieldStyle: 'text-align: right; font-size: 13pt; background-color: #EAFFCC;',
                                        margins: '0 5 0 0'
                                    },
                                    {
                                        xtype: 'button',
                                        text: 'SİL',
                                        width: 40,
                                        cls: 'btn-article-remove',
                                        handler: function() {
                                            if(count <= 3) {
                                                Ext.getCmp('btnArticleLevelAdd').enable();
                                            } else {
                                                Ext.getCmp('btnArticleLevelAdd').disable();
                                            }
                                            winArticle.down('fieldset[id=article-fieldset]').remove(container);
                                        }
                                    }
                                ]
                            }
                        ]);
                    }
                },
                {
                    xtype: 'fieldcontainer',
                    layout: 'hbox',
                    id: 'article-level-container',
                    defaultType: 'textfield',
                    fieldDefaults: {
                        labelAlign: 'top'
                    },
                    items: [
                        {
                            fieldLabel: 'LEVEL',
                            name: 'artLevel',
                            inputWidth: 216,
                            margins: '0 5 5 0',
                            allowBlank: false,
                            fieldStyle: 'text-align: right; font-size: 13pt; background-color: #EAFFCC;'
                        },
                        {
                            fieldLabel: 'VALUE',
                            name: 'artValue',
                            inputWidth: 216,
                            allowBlank: false,
                            blankText: 'zorunlu alan, boş bırakılamaz',
                            fieldStyle: 'text-align: right; font-size: 13pt; background-color: #EAFFCC;',
                            listeners: {
                                change: function(textfield, newValue, oldValue) {
                                    if(oldValue == 'undefined' || newValue == '') {
                                        Ext.getCmp('btnArticleSave').disable();
                                    } else {
                                        Ext.getCmp('btnArticleSave').enable();
                                    }
                                }
                            }
                        }
                    ]
                }
            ]
        }
    ]
}),
buttons: [
    {
        text: 'KAPAT',
        scale: 'medium',
        width: 100,
        cls: 'btn-article-close',
        listeners: {
            click: function() {
                winArticle.close();
            }
        }
    },
    '->',
    {
        text: 'EKLE',
        scale: 'medium',
        disabled: true,
        width: 100,
        margin: '0 9 0 0',
        cls: 'btn-article-save',
        id: 'btnArticleSave'
    }
]
});

EXT.ONREADY FUNCTION

Ext.onReady(function () {
Ext.QuickTips.init();
Ext.state.Manager.setProvider(new Ext.state.CookieProvider({
    expires: new Date(new Date().getTime() + (1000 * 60 * 60 * 24 * 7))
}));
var Discounts = Ext.create('Ext.form.Panel', {
    id: 'discount-types',
    bodyPadding: 10,
    width: 760,
    height: 600,
    title: 'DNR TANIMLAMA / SCREEN 0',
    layout: 'card',
    bodyStyle: 'padding:20px',
    defaults: {
        border: false,
        anchor: '100%'
    },
    style: {
        'box-shadow': '0 2px 5px rgba(0, 0, 0, 0.6)',
        '-webkit-box-shadow': '0 0 8px rgba(0, 0, 0, 0.5)'
    },
    frame: true,
    buttons: [
        {
            text: 'ÖNCEKİ ADIM',
            id: 'move-prev',
            cls: 'np-button',
            scale: 'medium',
            iconCls: 'dnr-prev-icon',
            iconAlign: 'left',
            handler: function (btn) {
                navigate(btn.up('panel'), 'prev');
                var itemd = Discounts.getLayout().getActiveItem();
                Discounts.setTitle('DNR TANIMLAMA ' + ' / ' + itemd.cardTitle);
                Ext.getCmp('dnr-submit').disable();
                Ext.getCmp('dnr-submit').setVisible(false);
            },
            disabled: true
        },
        {
            text: 'SONRAKİ ADIM',
            id: 'move-next',
            scale: 'medium',
            cls: 'np-button',
            iconCls: 'dnr-next-icon',
            iconAlign: 'right',
            handler: function (btn) {
                navigate(btn.up('panel'), 'next');
                var itemd = Discounts.getLayout().getActiveItem();
                Discounts.setTitle('DNR TANIMLAMA ' + ' / ' + itemd.cardTitle);
                var cardNum = Discounts.items.indexOf(itemd);
                if (cardNum == 3) {
                    Ext.getCmp('dnr-submit').enable();
                    Ext.getCmp('dnr-submit').setVisible(true);
                }
            },
            disabled: true
        },
        '->',
        {
            text: '&nbsp KAYDET ',
            id: 'dnr-submit',
            scale: 'medium',
            iconCls: 'dnr-submit-icon',
            iconAlign: 'right',
            cls: 'dnr-submit',
            disabled: true,
            hidden: true,
            handler: function (btn) {
            }
        }
    ],
    items: [
        {
            id: 'screen-0',
            cardTitle: 'SCREEN 0',
            layout: 'form',
            items: [
                {
                    layout: {
                        type: 'vbox',
                        align: 'center'
                    },
                    margin: '60 0 0 0',
                    items: [
                        {
                            xtype: 'combobox',
                            inputWidth: 295,
                            fieldLabel: 'DNR TİPİ',
                            fieldStyle: 'height: 26px',
                            id: 'discount-type',
                            store: discounts,
                            valueField: 'DNR_TYPE_ID',
                            displayField: 'DNR_TYPE_DESC',
                            queryMode: 'remote',
                            forceSelection: true,
                            stateful: true,
                            stateId: 'cmb_disc_type',
                            allowBlank: false,
                            emptyText: 'DNR tipini seçiniz...',
                            triggerAction: 'all',
                            listeners: {
                                select: function (e) {
                                    var discType = Ext.getCmp('discount-type').getValue();
                                    var discDetail = Ext.getCmp('discount-detail');
                                    discdetails.removeAll();
                                    if (discType != 0) {
                                        discDetail.setDisabled(false);
                                        discdetails.proxy.extraParams = { 'dnrtype': discType };
                                        discdetails.load();
                                    }
                                }
                            }
                        },
                        {
                            xtype: 'combobox',
                            inputWidth: 400,
                            fieldStyle: 'height: 26px',
                            id: 'discount-detail',
                            valueField: 'ID',
                            displayField: 'DNR_TITLE',
                            store: discdetails,
                            forceSelection: true,
                            stateful: true,
                            stateId: 'cmb_disc_detail',
                            margin: '25 0 0 0',
                            disabled: true,
                            allowBlank: false,
                            msgTarget: 'side',
                            emptyText: 'İNDİRİM TİPİNİ SEÇİNİZ...',
                            blankText: 'İndirim tipi boş olamaz!',
                            triggerAction: 'all',
                            listeners: {
                                select: function (e) {
                                    var discDetail = Ext.getCmp('discount-detail').getValue();
                                    if (discDetail != 'null') {
                                        var value = discdetails.getAt(discdetails.find('ID', discDetail)).get('DNR_DESCRIPTION');
                                        Ext.getCmp('dnr-type-desc-panel').setVisible(true);
                                        Ext.getCmp('dnr-type-desc-panel').update(value);
                                    }
                                }
                            }
                        },
                        {
                            xtype: 'textarea',
                            grow: false,
                            name: 'invoiceText',
                            fieldLabel: 'FATURA METNİ',
                            id: 'invoice-text',
                            blankText: 'Fatura metni boş olamaz!',
                            width: 400,
                            height: 60,
                            margin: '30 0 0 0',
                            allowBlank: false,
                            msgTarget: 'side',
                            listeners: {
                                change: function (e) {
                                    if (Ext.getCmp('invoice-text').getValue().length === 0) {
                                        Ext.getCmp('move-next').disable();
                                    } else {
                                        Ext.getCmp('move-next').enable();
                                    }
                                }
                            }
                        },
                        {
                            xtype: 'panel',
                            id: 'dnr-type-desc-panel',
                            layout: {type: 'hbox', align: 'stretch'},
                            height: 145,
                            width: 400,
                            cls: 'dnr-desc-panel',
                            margin: '60 0 0 0',
                            html: '&nbsp',
                            hidden: true
                        }
                    ]
                }
            ]
        },
        {
            id: 'screen-1',
            cardTitle: 'SCREEN 1',
            layout: 'form',
            items: [
                {
                    layout: 'column',
                    width: 730,
                    height: 90,
                    items: [
                        {
                            xtype: 'fieldset',
                            title: 'ARTİKEL / HEDEF GRUP / MAL GRUBU SEÇİMİ',
                            cls: 'dnr-fieldset',
                            width: 730,
                            height: 80,
                            margin: '0',
                            items: [
                                {
                                    xtype: 'buttongroup',
                                    columns: 5,
                                    columnWidth: 140,
                                    frame: false,
                                    margin: '5 0 0 18',
                                    items: [
                                        {
                                            text: 'ARTİKEL',
                                            scale: 'medium',
                                            margin: '0 18px 0 0',
                                            width: 120,
                                            height: 36,
                                            id: 'btn-article',
                                            cls: 'btn-grp-choose btn-grp-article',
                                            listeners: {
                                                click: function () {
                                                    winArticle.center();
                                                    winArticle.show();
                                                }
                                            }
                                        },
                                        {
                                            text: 'PUAR',
                                            scale: 'medium',
                                            margin: '0 18px 0 0',
                                            width: 120,
                                            height: 36,
                                            cls: 'btn-grp-choose btn-grp-puar',
                                            listeners: {
                                                click: function() {
                                                    winPuar.show();
                                                }
                                            }
                                        },
                                        {
                                            text: 'MAL GRUBU',
                                            scale: 'medium',
                                            margin: '0 18px 0 0',
                                            width: 120,
                                            height: 36,
                                            cls: 'btn-grp-choose btn-grp-choose',
                                            listeners: {
                                                click: function() {
                                                    winArticleGroup.show();
                                                }
                                            }
                                        },
                                        {
                                            text: 'HEDEF GRUP',
                                            scale: 'medium',
                                            margin: '0 18px 0 0',
                                            width: 120,
                                            height: 36,
                                            cls: 'btn-grp-choose btn-grp-target',
                                            listeners: {
                                                click: function() {
                                                    winTargetGroup.show();
                                                }
                                            }
                                        },
                                        {
                                            text: 'SUPPLIER',
                                            scale: 'medium',
                                            width: 120,
                                            height: 36,
                                            cls: 'btn-grp-choose btn-grp-supplier',
                                            listeners: {
                                                click: function() {
                                                    winSupplier.show();
                                                }
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    ]
                },
                {
                    xtype: 'gridpanel',
                    id: 'article-grid',
                    selType: 'rowmodel',
                    elStatus: true,
                    plugins: [
                        { ptype: 'cellediting', clicksToEdit: 1},
                        { ptype: 'datadrop'}
                    ],
                    /* ***************************************************************
                     * here is the tricky part! when user change any fields above
                     * this grid will dynamically generate upon user request. So that
                     * we arent sure which columns will be available.
                     * ***************************************************************/
                    columns: [
                        {
                            text: 'COLUMN A',
                            dataIndex: ''
                        }
                    ]
                }
            ]
        },
        renderTo: 'content'
})
});

更新答案

经过一些澄清,我认为答案应该很容易(至少我认为是这样)对于下面的答案,我假设你在想要获取表格的时候就在表格内;网格数据,并且只有一个Ext.form.Panel:

// Navigate up to the form:
var form = this.up('form'),
// get the form values
    data = form.getValues(),
// get the selected record from the grid
    gridRecords = form.down('grid').getSelectionModel().getSelected(),
// some helper variables
    len = gridRecords.length,
    recordData = [];
// normalize the model data by copying just the data objects into the array
for(i=0;i<len;i++) {
    recordData .push(gridRecords[i].data);
}
// apply the selected grid records to the formdata. For that you will need a property name, I will use just 'gridRecords' but you may change it
data.gridRecords = recordData;
// send all back via a ajax request
Ext.Ajax.request({
    url: 'demo/sample',
    success: function(response, opts) {
        // your handler
    },
    failure: function(response, opts) {
        // your handler
    },
    jsonData: data
});

应该是

提供更多可从网格获取的数据选项

// get all data that is currently in the store
form.down('grid').getStore().data.items
// get all new and updated records
form.down('grid').getStore().getModifiedRecords()
// get all new records
form.down('grid').getStore().getNewRecords()
// get all updated records
form.down('grid').getStore().getUpdatedRecords()

下方的旧答案(适用于更复杂的场景)

你说的:

你有一个表格的网格,也许还有网格。在需要的地方从表格中提取数据时读取网格。

在下面的答案中,我将只介绍getValues将事件绑定/取消绑定到每个网格,而不是

  • 表单加载/提交
  • 记录加载/更新
  • 设定值

我的建议是使您的表单更加智能,以便它能够处理此问题

我的意思是什么

默认表单关心插入其正文中任何位置的所有字段。在99,9%这是完全好的,但不是所有人。您的表单还需要注意插入的网格。

如何完成

第一件事是,当你将网格作为表单的一部分时,我建议给它们一个名称属性。其次,您需要了解表单如何收集和利用字段,以便能够将其复制到网格中。为此,您需要查看Ext.form.Basic类构造函数,其中重要的部分是这个

// We use the monitor here as opposed to event bubbling. The problem with bubbling is it doesn't
// let us react to items being added/remove at different places in the hierarchy which may have an
// impact on the dirty/valid state.
me.monitor = new Ext.container.Monitor({
    selector: '[isFormField]',
    scope: me,
    addHandler: me.onFieldAdd,
    removeHandler: me.onFieldRemove
});
me.monitor.bind(owner);

这里发生的情况是,一个监视器被初始化,从那时起,它将查找插入绑定组件中的任何字段,监视器将在绑定组件中调用适当的处理程序。目前监视器正在查找字段,但您需要一个正在查找网格的字段。这样的显示器看起来像:

me.gridMonitor = new Ext.container.Monitor({
    selector: 'grid',
    scope: me,
    addHandler: me.onGridAdd,
    removeHandler: me.onGridRemove
});
me.gridMonitor.bind(owner);

因为我对你的数据结构了解不多,我无法告诉你可能需要哪些网格,但你应该像一样在addHandler/removeHandler中注册/注销它们

onGridAdd: function(grid) {
    var me = this;
    me.mon(grid,'select',me.yourHandler,me);
},
onGridRemove: function(grid) {
    var me = this;
    me.mun(grid,'select',me.yourHandler,me);
}

此外,您还需要以下辅助方法

/**
 * Return all the {@link Ext.grid.Panel} components in the owner container.
 * @return {Ext.util.MixedCollection} Collection of the Grid objects
 */
getGrids: function() {
    return this.gridMonitor.getItems();
},
/**
 * Find a specific {@link Ext.grid.Panel} in this form by id or name.
 * @param {String} id The value to search for (specify either a {@link Ext.Component#id id} or
 * {@link Ext.grid.Panel name }).
 * @return {Ext.grid.Panel} The first matching grid, or `null` if none was found.
 */
findGrid: function(id) {
    return this.getGrids().findBy(function(f) {
        return f.id === id || f.name === id;
    });
},

最重要的是从网格中获取数据。这里我们需要覆盖

getValues: function(asString, dirtyOnly, includeEmptyText, useDataValues) {
    var values  = {},
        fields  = this.getFields().items,
        grids  = this.getGrids().items, // the grids found by the monitor
        f,
        fLen    = fields.length,
        gLen    = grids.length, // gridcount
        isArray = Ext.isArray,
        grid, gridData, gridStore, // some vars used while reading the grid content
        field, data, val, bucket, name;
    for (f = 0; f < fLen; f++) {
        field = fields[f];
        if (!dirtyOnly || field.isDirty()) {
            data = field[useDataValues ? 'getModelData' : 'getSubmitData'](includeEmptyText);
            if (Ext.isObject(data)) {
                for (name in data) {
                    if (data.hasOwnProperty(name)) {
                        val = data[name];
                        if (includeEmptyText && val === '') {
                            val = field.emptyText || '';
                        }
                        if (values.hasOwnProperty(name)) {
                            bucket = values[name];
                            if (!isArray(bucket)) {
                                bucket = values[name] = [bucket];
                            }
                            if (isArray(val)) {
                                values[name] = bucket.concat(val);
                            } else {
                                bucket.push(val);
                            }
                        } else {
                            values[name] = val;
                        }
                    }
                }
            }
        }
    }
    // begin new part
    for (g = 0; g < gLen; g++) {
        grid = grids[f];
        gridStore = grid.getStore();
        gridData = [];
        // You will need a identification variable to determine which data should be taken from the grid. Currently this demo implement three options
        // 0 only selected
        // 1 complete data within the store
        // 2 only modified records (this can be splitted to new and updated)
        var ditems = grid.submitData === 0 ? grid.getSelectionModel().getSelection() : 
                     grid.submitData === 1 ? gridStore.getStore().data.items : gridStore.getStore().getModifiedRecords(),
            dlen = ditems.length;
        for(d = 0; d < dLen; d++) {
            // push the model data to the current data list. It doesn't matter of which type the models (records) are, this will simply read the whole known data. Alternatively you may access the rawdata property if the reader does not know all fields.
            gridData.push(ditems[d].data);
        }
        // assign the array of record data to the grid-name property
        data[grid.name] = gridData;
    }
    // end new part
    if (asString) {
        values = Ext.Object.toQueryString(values);
    }
    return values;
}

如果它看起来像

Ext.define('Ext.ux.form.Basic', {
    extend: 'Ext.form.Basic',
    /**
     * Creates new form.
     * @param {Ext.container.Container} owner The component that is the container for the form, usually a {@link Ext.form.Panel}
     * @param {Object} config Configuration options. These are normally specified in the config to the
     * {@link Ext.form.Panel} constructor, which passes them along to the BasicForm automatically.
     */
    constructor: function(owner, config) {
        var me = this;
        me.callParent(arguments);
        // We use the monitor here as opposed to event bubbling. The problem with bubbling is it doesn't
        // let us react to items being added/remove at different places in the hierarchy which may have an
        // impact on the dirty/valid state.
        me.gridMonitor = new Ext.container.Monitor({
            selector: 'grid',
            scope: me,
            addHandler: me.onGridAdd,
            removeHandler: me.onGridRemove
        });
        me.gridMonitor.bind(owner);
    },
    onGridAdd: function(grid) {
        var me = this;
        me.mon(grid,'select',me.yourHandler,me);
    },
    onGridRemove: function(grid) {
        var me = this;
        me.mun(grid,'select',me.yourHandler,me);
    },
    /**
     * Return all the {@link Ext.grid.Panel} components in the owner container.
     * @return {Ext.util.MixedCollection} Collection of the Grid objects
     */
    getGrids: function() {
        return this.gridMonitor.getItems();
    },
    /**
     * Find a specific {@link Ext.grid.Panel} in this form by id or name.
     * @param {String} id The value to search for (specify either a {@link Ext.Component#id id} or
     * {@link Ext.grid.Panel name }).
     * @return {Ext.grid.Panel} The first matching grid, or `null` if none was found.
     */
    findGrid: function(id) {
        return this.getGrids().findBy(function(f) {
            return f.id === id || f.name === id;
        });
    },
    getValues: function(asString, dirtyOnly, includeEmptyText, useDataValues) {
        var values  = {},
            fields  = this.getFields().items,
            grids  = this.getGrids().items, // the grids found by the monitor
            f,
            fLen    = fields.length,
            gLen    = grids.length, // gridcount
            isArray = Ext.isArray,
            grid, gridData, gridStore, // some vars used while reading the grid content
            field, data, val, bucket, name;
        for (f = 0; f < fLen; f++) {
            field = fields[f];
            if (!dirtyOnly || field.isDirty()) {
                data = field[useDataValues ? 'getModelData' : 'getSubmitData'](includeEmptyText);
                if (Ext.isObject(data)) {
                    for (name in data) {
                        if (data.hasOwnProperty(name)) {
                            val = data[name];
                            if (includeEmptyText && val === '') {
                                val = field.emptyText || '';
                            }
                            if (values.hasOwnProperty(name)) {
                                bucket = values[name];
                                if (!isArray(bucket)) {
                                    bucket = values[name] = [bucket];
                                }
                                if (isArray(val)) {
                                    values[name] = bucket.concat(val);
                                } else {
                                    bucket.push(val);
                                }
                            } else {
                                values[name] = val;
                            }
                        }
                    }
                }
            }
        }
        // begin new part
        for (g = 0; g < gLen; g++) {
            grid = grids[f];
            gridStore = grid.getStore();
            gridData = [];
            // You will need a identification variable to determine which data should be taken from the grid. Currently this demo implement three options
            // 0 only selected
            // 1 complete data within the store
            // 2 only modified records (this can be splitted to new and updated)
            var ditems = grid.submitData === 0 ? grid.getSelectionModel().getSelection() : 
                         grid.submitData === 1 ? gridStore.getStore().data.items : gridStore.getStore().getModifiedRecords(),
                dlen = ditems.length;
            for(d = 0; d < dLen; d++) {
                // push the model data to the current data list. It doesn't matter of which type the models (records) are, this will simply read the whole known data. Alternatively you may access the rawdata property if the reader does not know all fields.
                gridData.push(ditems[d].data);
            }
            // add the store data as array to the grid-name property
            data[grid.name] = gridData;
        }
        // end new part
        if (asString) {
            values = Ext.Object.toQueryString(values);
        }
        return values;
    }
});

接下来是修改表单以使用此基本表单类型

Ext.define('Ext.ux.form.Panel', {
    extend:'Ext.form.Panel',
    requires: ['Ext.ux.form.Basic'],
    /**
     * @private
     */
    createForm: function() {
        var cfg = {},
            props = this.basicFormConfigs,
            len = props.length,
            i = 0,
            prop;
        for (; i < len; ++i) {
            prop = props[i];
            cfg[prop] = this[prop];
        }
        return new Ext.ux.form.Basic(this, cfg);
    }
});

注意:

这一切都未经测试!我做过类似的事情客户扩展表单的功能,我可以告诉他们这种方式会很好很快地工作。至少它应该展示如何做到这一点,并且可以很容易地进行调整,以设置表单和/或加载/更新记录。

我自己没有使用过这个,但有一个线程试图通过hasMany关系处理关联的模型。问题是,每个人对记录的写操作过程中应该发生的事情都有稍微不同的期望。Serever端ORM以一种有些难以理解的方式来处理这个问题,并且通常是新开发人员的痛点。

这是一个论坛线程,它详细介绍了一个自定义JSON编写器,用于将父记录与其子记录一起持久化。

以下是似乎至少对一些人有效的代码:

Ext.data.writer.Json.override({
/*
 * This function overrides the default implementation of json writer. Any hasMany relationships will be submitted
 * as nested objects. When preparing the data, only children which have been newly created, modified or marked for
 * deletion will be added. To do this, a depth first bottom -> up recursive technique was used.
 */
getRecordData: function(record) {
    //Setup variables
    var me = this, i, association, childStore, data = record.data;
    //Iterate over all the hasMany associations
    for (i = 0; i < record.associations.length; i++) {
        association = record.associations.get(i);
        data[association.name] = null;
        childStore = record[association.storeName];
        //Iterate over all the children in the current association
        childStore.each(function(childRecord) {
            if (!data[association.name]){
                data[association.name] = [];
            }
            //Recursively get the record data for children (depth first)
            var childData = this.getRecordData.call(this, childRecord);
            /*
             * If the child was marked dirty or phantom it must be added. If there was data returned that was neither
             * dirty or phantom, this means that the depth first recursion has detected that it has a child which is
             * either dirty or phantom. For this child to be put into the prepared data, it's parents must be in place whether
             * they were modified or not.
             */
            if (childRecord.dirty | childRecord.phantom | (childData != null)){
                data[association.name].push(childData);
                record.setDirty();
            }
        }, me);
        /*
         * Iterate over all the removed records and add them to the preparedData. Set a flag on them to show that
         * they are to be deleted
         */
        Ext.each(childStore.removed, function(removedChildRecord) {
            //Set a flag here to identify removed records
            removedChildRecord.set('forDeletion', true);
            var removedChildData = this.getRecordData.call(this, removedChildRecord);
            data[association.name].push(removedChildData);
            record.setDirty();
        }, me);
    }
    //Only return data if it was dirty, new or marked for deletion.
    if (record.dirty | record.phantom | record.get('forDeletion')){
        return data;
    }
}
});

完整线程位于此处:http://www.sencha.com/forum/showthread.php?141957-保存对象—与单个存储/页面5 有许多关系