Hello,

I spent few days to find how to post datas with an association, linked with a form and a grid.
I don't know why, but the associated datas aren't posted.
Because of this bug ? : https://www.sencha.com/forum/showthr...ssociated-Data

I've tried several solutions found in the forum :


Finally, here's is what i do.
May be some better way ?


The first model which has a hasMany association

Code:
Ext.define('MY_APP.model.annuaire.AnnuaireCadreModel', {
    extend: 'Ext.data.Model',
    alias: 'model.annuaire_cadre',

    requires: [
        'Ext.data.field.Integer',
        'Ext.data.field.Boolean',
        'Ext.data.field.String'
    ],

    hasMany: {     //<== we declare the hasMany association
        model: 'MY_APP.model.annuaire.AnnuaireCadreParcoursModel',
        name: 'parcours'     //<==This name will be used in the "save" function of the form
    },

    fields: [
        {
            type: 'int',
            name: 'id_cadre',
            unique: true
        },
        {
            type: 'boolean',
            defaultValue: 0,
            name: 'modification'
        },
        {
            type: 'boolean',
            defaultValue: 0,
            name: 'suppression'
        },
        {
            type: 'string',
            name: 'patronyme'
        },
        {
            type: 'string',
            name: 'date_de_naissance'
        },
        {
            type: 'string',
            name: 'photo'
        }
    ]
});
The first store
Code:
Ext.define('MY_APP.store.annuaire.AnnuaireCadre', {
    extend: 'Ext.data.Store',

    requires: [
        'MY_APP.model.annuaire.AnnuaireCadreModel',
        'Ext.data.proxy.Rest',
        'Ext.data.reader.Json',
        'Ext.data.writer.Json'
    ],

    constructor: function(cfg) {
        var me = this;
        cfg = cfg || {};
        me.callParent([Ext.apply({
            remoteFilter: true,
            remoteSort: true,
            storeId: 'annuaire.AnnuaireCadre',
            model: 'MY_APP.model.annuaire.AnnuaireCadreModel',
            proxy: {
                type: 'rest',
                url: 'php/application/app.php/annuaire_cadre',
                reader: {
                    type: 'json',
                    rootProperty: 'data'
                },
                listeners: {
                    exception: {
                        fn: me.onRestException,
                        scope: me
                    }
                },
                writer: {
                    type: 'json',
                    allDataOptions: {     //<== This should be enough to work, but the associated datas in the grid
// are not posted. See in the "save" function of the form how i proceed
                        associated: true,
                        persist: true
                    }
                }
            }
        }, cfg)]);
    },

    onRestException: function(proxy, response, operation, eOpts) {
        var reponse = Ext.JSON.decode(response.responseText);
        Ext.MessageBox.show({
            title: reponse.title,
            msg: reponse.message,
            icon: Ext.MessageBox.ERROR,
            buttons: Ext.Msg.OK
        }).center();
    }

});
The second model
Code:
Ext.define('MY_APP.model.annuaire.AnnuaireCadreParcoursModel', {
    extend: 'Ext.data.Model',
    alias: 'model.annuaire_cadre_parcours',

    requires: [
        'Ext.data.field.Integer',
        'Ext.data.field.Date',
        'Ext.data.field.String'
    ],

    fields: [
        {
            type: 'int',
            name: 'id_parcours',
            unique: true
        },
        {
            type: 'int',
            name: 'id_cadre'   //<== it can be seen as a foreign key
        },
        {
            type: 'date',
            name: 'date_parcours'
        },
        {
            type: 'string',
            name: 'grade'
        },
        {
            type: 'string',
            name: 'lieu'
        }
    ]
});
The second store
Code:
Ext.define('MY_APP.store.annuaire.AnnuaireCadreParcours', {
    extend: 'Ext.data.Store',

    requires: [
        'MY_APP.model.annuaire.AnnuaireCadreParcoursModel',
        'Ext.data.proxy.Ajax',
        'Ext.data.reader.Json'
    ],

    constructor: function(cfg) {
        var me = this;
        cfg = cfg || {};
        me.callParent([Ext.apply({
            storeId: 'annuaire.AnnuaireCadreParcours',
            model: 'MY_APP.model.annuaire.AnnuaireCadreParcoursModel',
            proxy: {     //<==No need to have an URL and no need of a writer, because it's already in the proxy of the first store
                type: 'ajax',
                reader: {
                    type: 'json'
                }
            }
        }, cfg)]);
    }
});
The form, which has the grid inside it
Code:
Ext.define('MY_APP.view.annuaire.FormAnnuCadre', {
    extend: 'Ext.form.Panel',
    alias: 'widget.annuaire.formannucadre',

    requires: [
        'MY_APP.view.annuaire.FormAnnuCadreViewModel',
        'MY_APP.view.annuaire.FormAnnuCadreViewController',
        'Ext.form.field.Display',
        'Ext.form.field.ComboBox',
        'Ext.form.field.File',
        'Ext.form.field.Date',
        'Ext.form.field.Hidden',
        'Ext.Img',
        'Ext.grid.Panel',
        'Ext.view.Table',
        'Ext.grid.column.Date',
        'Ext.grid.plugin.RowEditing',
        'Ext.toolbar.Toolbar',
        'Ext.button.Button'
    ],

    controller: 'annuaire.formannucadre',
    viewModel: {
        type: 'annuaire.formannucadre'
    },
    id: 'id_FormAnnuCadre',
    scrollable: false,
    bodyPadding: 10,
    defaultListenerScope: true,

    initConfig: function(instanceConfig) {
        var me = this,
            config = {
                items: [
                    {
                        xtype: 'displayfield',
                        anchor: '100%',
                        value: 'Some help text'
                    },
                    {
                        xtype: 'container',
                        margin: '0 0 10 0',
                        layout: 'column',
                        items: [
                            {
                                xtype: 'container',
                                columnWidth: 0.9,
                                layout: 'anchor',
                                items: [
                                    {
                                        xtype: 'combobox',
                                        anchor: '80%',
                                        fieldLabel: 'Name',
                                        labelAlign: 'right',
                                        labelWidth: 150,
                                        name: 'id_cadre',
                                        allowBlank: false,
                                        emptyText: 'Fill in 3 char...',
                                        displayField: 'libelle_utilisateur',
                                        minChars: 3,
                                        store: 'administration.utilisateur',
                                        typeAhead: true,
                                        valueField: 'id_utilisateur'
                                    },
                                    {
                                        xtype: 'filefield',
                                        anchor: '80%',
                                        id: 'id_photo_path',
                                        fieldLabel: 'Photo',
                                        labelAlign: 'right',
                                        labelWidth: 150,
                                        listeners: {
                                            change: 'onAttachmentChange'
                                        }
                                    },
                                    me.processDate_de_naissance({
                                        xtype: 'datefield',
                                        anchor: '50%',
                                        fieldLabel: 'Date de naissance',
                                        labelAlign: 'right',
                                        labelWidth: 150,
                                        name: 'date_de_naissance',
                                        showToday: false
                                    }),
                                    {
                                        xtype: 'hiddenfield',
                                        id: 'id_photo',
                                        name: 'photo'
                                    }
                                ]
                            },
                            {
                                xtype: 'image',
                                height: 120,
                                id: 'photo_thumb',
                                width: 120,
                                src: '%22%22'
                            }
                        ]
                    },
                    {
                        xtype: 'gridpanel',     //<== Here is the grid with the store for associated datas
                        reference: 'gridparcours',
                        height: 420,
                        id: 'id_grid_parcours',
                        scrollable: 'vertical',
                        title: 'Parcours',
                        enableColumnHide: false,
                        enableColumnMove: false,
                        enableColumnResize: false,
                        store: 'annuaire.AnnuaireCadreParcours',     //<==The associated store
                        columns: [
                            {
                                xtype: 'datecolumn',
                                width: 200,
                                align: 'center',
                                dataIndex: 'date_parcours',
                                text: 'Date',
                                format: 'd/m/Y',
                                editor: {
                                    xtype: 'datefield',
                                    allowBlank: false
                                }
                            },
                            {
                                xtype: 'gridcolumn',
                                width: 200,
                                dataIndex: 'grade',
                                text: 'Job title',
                                editor: {
                                    xtype: 'textfield'
                                }
                            },
                            {
                                xtype: 'gridcolumn',
                                width: 200,
                                dataIndex: 'lieu',
                                text: 'Localization',
                                editor: {
                                    xtype: 'textfield'
                                }
                            }
                        ],
                        plugins: [
                            {
                                ptype: 'rowediting'
                            }
                        ],
                        dockedItems: [
                            {
                                xtype: 'toolbar',
                                dock: 'top',
                                items: [
                                    {
                                        xtype: 'button',
                                        handler: 'onAddParcoursClick',
                                        iconCls: 'x-fa fa-plus-circle add',
                                        text: 'Add a job',
                                        scope: 'controller'
                                    }
                                ]
                            }
                        ]
                    },
                    {
                        xtype: 'container',
                        padding: 10,
                        layout: {
                            type: 'hbox',
                            align: 'middle',
                            pack: 'center'
                        },
                        items: [
                            {
                                xtype: 'button',
                                margin: '0 10 0 0',
                                width: 150,
                                iconCls: 'x-fa fa-remove',
                                text: 'Cancel',
                                listeners: {
                                    click: {
                                        fn: 'cancelEdit',
                                        scope: 'controller'
                                    }
                                }
                            },
                            {
                                xtype: 'button',
                                formBind: true,
                                disabled: true,
                                margin: '0 0 0 10',
                                width: 150,
                                iconCls: 'x-fa fa-check',
                                text: 'Save',
                                listeners: {
                                    click: {
                                        fn: 'save',
                                        scope: 'controller'
                                    }
                                }
                            }
                        ]
                    }
                ]
            };
        if (instanceConfig) {
            me.self.getConfigurator().merge(me, config, instanceConfig);
        }
        return me.callParent([config]);
    },

    processDate_de_naissance: function(config) {
        config.maxValue = Ext.Date.add(new Date(), Ext.Date.YEAR, -18);
        config.minValue = Ext.Date.add(new Date(), Ext.Date.YEAR, -67);
        config.value = Ext.Date.add(new Date(), Ext.Date.YEAR, -18);
        return config;
    },

    onAttachmentChange: function(filefield, value, eOpts) {
        var file = filefield.fileInputEl.dom.files[0],
            reader;

        if (file === undefined || !(file instanceof File)) {
            return;
        }

        reader = new FileReader();
        reader.onload = function (event) {
            Ext.getCmp('photo_thumb').setSrc(event.target.result);

        };
        reader.readAsDataURL(file);
    }

});
The form controller
Code:
Ext.define('MY_APP.view.annuaire.FormAnnuCadreViewController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.annuaire.formannucadre',

    onAddParcoursClick: function(button, e) {
        var grid = this.getReferences().gridparcours,
            record = new MY_APP.model.annuaire.AnnuaireCadreParcoursModel(),
            plugin = grid.findPlugin('rowediting');

        plugin.cancelEdit();
        grid.getStore().insert(0, record);
        plugin.startEdit(record, 0);
    },

    cancelEdit: function(button, e, eOpts) {
        this.getView().getForm().reset();
    },

    save: function(button, e, eOpts) {
        var form = this.getView().getForm(),
            values,
            model,
            store = Ext.getStore('annuaire.AnnuaireCadre');

        if (form.isValid()) {
            // Check for the various File API support.
            if (window.File && window.FileReader && window.FileList && window.Blob) {
                var file = Ext.getCmp('id_photo_path').fileInputEl.dom.files[0],
                    reader;

                if (file === undefined || !(file instanceof File)) {
                    return;
                }

                reader = new FileReader();// FileReader provient de l'API "File" Javascript. Ce n'est pas spécifique à Extjs.
                reader.readAsDataURL(file);
                reader.onload = function (event) {
                    Ext.getCmp('id_photo').setValue(event.target.result);
                    var values = form.getValues(),     //<== We get the value of the form's field, but not the value of the grid store
                        cadre = Ext.create('model.annuaire_cadre', values),     //<==We create a "first" model with the values
                        parcours = cadre.parcours(),     //<==We declare the "second" model, linked with the "first" model, for the associated datas
//cadre.parcours() : parcours is the name declared in the hasMany associations of the "first" model
                        store_parcours = Ext.getCmp('id_grid_parcours').getStore();     //<==We get the store of the grid, which contains the associated datas

                    store_parcours.each( function(c) {     //<== For each line of the grid, or for each recordset, we put it in the linked model
                        parcours.add(c);
                    });
                    store.add(cadre);     //<== We add the complete model ("first" model + associated "second" models) to the "first" store

                    store.sync({     //<==The sync sends the Create operation to the server
                        success: function(batch, options) {
                            response = Ext.decode(batch.operations[0].getResponse().responseText);
                            Ext.Msg.show({
                                title: response.title,
                                msg: response.message,
                                buttons: Ext.Msg.OK,
                                scope: this,
                                icon: Ext.Msg.INFO
                            });
                        }
                    });
                };
            } else {
                alert('The File APIs are not fully supported in this browser.');
            }
        } else {
            Ext.Msg.show({
                title: 'Error',
                msg: 'Errors in the form.',
                buttons: Ext.Msg.OK,
                animateTarget: button,
                scope: this,
                icon: Ext.Msg.ERROR
            });
        }
    }

});

The posted datas

The posted datas contains all what will be proceed on the server side.
Code:
date_de_naissance 01/08/2000
id MY_APP.model.annuaire.AnnuaireCadreModel-2
id_cadre 1
modification 0
parcours {…}
0 {…}
date_parcours 2018-08-02T00:00:00
grade :!;:!
id MY_APP.model.annuaire.AnnuaireCadreParcoursModel-2
id_cadre 0
id_parcours 0
lieu ,;:;:,
1 {…}
date_parcours 2018-08-01T00:00:00
grade n,;n,
id MY_APP.model.annuaire.AnnuaireCadreParcoursModel-1
id_cadre 0
id_parcours 0
lieu bn,;bn,
patronyme
photo data:image/png;base64,string_for_the_base64_encode_of_the_image
suppression 0