1
votes

I want to set a displayfield in a combobox for a record (which is a historical data and is stored in the database and loaded on the form) which does not have an entry in the database.

For an example, I have 3 entries in the store as below - value : display field a : apple b : ball c : cat

As usual,It is used to prepopulate the combobox (Working fine till here).

I have historical data stored in the database, which has values for some records with id: d, which no more exists in the store but I still want to support the old data and want to display the associated display field ('doll') with it.

My query is, even though this record is not in the store associated with the combobox, How can I set the display field on getting the record from the database? Currently, we have no method such as setDisplayField in extJs but we have a method 'setValue' which changes the value in the combobox, and that is not desired.

I want to change the displayfield to 'doll' on encountering 'id =d' from database.

Please help.

Thank you

3

3 Answers

2
votes

If what you really want is a setDisplayField method, in fact it exists and it is called setRawValue.

However, it is doubtful that in itself it will suffice to solve your problem. Indeed, you may use it to get the correct display, but you will lose your value...

var legacyData = [
    {id: 'd', name: "Doll"}
];

var combo = Ext.widget({
    renderTo: Ext.getBody()
    ,xtype: 'combo'
    ,fieldLabel: "Regular combo"
    ,valueField: 'id'
    ,displayField: 'name'
    ,queryMode: 'local'
    ,value: 'd'
    ,store: {
        fields: ['id', 'name']
        ,data: []
        ,autoLoad: true
        ,listeners: {
            load: function() {
                var value = combo.value,
                    valueField = combo.valueField,
                    displayField = combo.displayField,
                    o = Ext.Array.findBy(legacyData, function(data) {
                        return data[valueField] === value;
                    });

                if (o) {
                    combo.setRawValue(o[displayField]);
                }

                // Fail! This will alert 'Doll' instead of 'd'.
                //
                // If you set forceSelection to true, the value will be cleared
                // all together.
                //
                alert(combo.getValue());
            }
        }
    }
});

Adding historical records to the combo's store, as dbring suggests, will indeed solve your problem, if and only if you're willing to let the user select the historical records. In my understanding that is not the case, because otherwise you wouldn't have any issue to begin with...

After inspecting the code of the combo class, I think that the best way to achieve what you need is to override the findRecord method. Any record returned by this method will be used to update both the combo box display and its value, independently of whether this record lives in the combo's store or not, and without affecting the values that can be selected in the combo. Perfect!

Let's say we've added a legacyStore to our combo, here's how to override the findRecord method to do just that:

findRecord: function(field, value) {
    var ls = this.legacyStore,
        record = this.callParent(arguments);
    if (record) {
        // found in current store
        return record;
    } else if (ls) {
        // try legacy value store
        var idx = ls.findExact(field, value);
        return idx !== -1 ? ls.getAt(idx) : false;
    } else {
        return false;
    }
}

Now, we're left with another couple of problems to deal with...

First, what if the legacy store is not loaded at the time findRecord is called? That won't work. In its vanilla form, the combo box will abort its setValue method if its store is not loaded, and call it back from its onLoad method. We should add some code to replicate this behaviour for our legacy store.

The second problem is more trivial, but what if the combo is initialized with a historical value, and the user changes the value of the combo but then regrets their choice? They won't be able to get the original value back without cancelling and refilling the whole form. To spare them this frustration, we can change the combo behaviour to restore its original value when cleared, instead of its last value. Since I don't know if this is desirable, we'll add it as an option.

Accounting for all this, here's the complete code of an "ux" that will handle your specific use case.

Note that it includes a global fix for a bug that kills the combo box initial value if forceSelection is set to true and the store is created with autoLoad: true. More information about that in this other question. Alternatively, you can skip the global fix and add loading: true to all the stores and legacy stores of the combos that use forceSelection: true.

/**
 * Fixes the bug with autoLoad: true and forceSelection: true.
 * 
 * See: https://stackoverflow.com/a/14440479/1387519
 */
Ext.define('Ext.ux.fix.data.Store.AutoLoadLoadingState', {
    override: 'Ext.data.Store'
    ,constructor: function() {
        this.callParent(arguments);
        if (this.autoLoad) {
            this.loading = true;
        }
    }
});

/**
 * A combo box with support for legacy values.
 *
 * Legacy values will be displayed if their value is assigned to the 
 * combo, but they won't be selectable (nor queryable) by the user.
 */
Ext.define('Ext.ux.form.field.LegacyValueCombo', {
    extend: 'Ext.form.field.ComboBox'

    ,alias: 'widget.legacyvaluecombo'

    ,requires: [
        'Ext.ux.fix.data.Store.AutoLoadLoadingState'
    ]

    /**
     * The store that provides legacy values.
     *
     * @cfg {Ext.data.Store/Object/String}
     */
    ,legacyStore: undefined

    /**
     * If `true` and {@link #forceSelection} is also `true`, the
     * {@link #originalValue original value} of the field will be restored when
     * the field is cleared, instead of the last value.
     *
     * This may be useful considering that if the combo original value is a
     * legacy one, and the user changes this value, they will have no other
     * means to get it back afterward.
     *
     * @cfg {Boolean}
     */
    ,restoreOriginalValue: true

    /**
     * Ensure the legacy store is a store instance before initializing the
     * combo box.
     */
    ,initComponent: function() {

        var legacyStore = this.legacyStore;
        if (legacyStore) {
            this.legacyStore = Ext.data.StoreManager.lookup(legacyStore);
        }

        this.callParent(arguments);
    }

    /**
     * Overridden to return a legacy record if the value is not found in the
     * regular store.
     *
     * This method will only work if both stores have been loaded.
     */
    ,findRecord: function(field, value) {
        var ls = this.legacyStore,
            record = this.callParent(arguments);
        if (record) {
            // found in current store
            return record;
        } else if (ls) {
            // try legacy value store
            var idx = ls.findExact(field, value);
            return idx !== -1 ? ls.getAt(idx) : false;
        } else {
            return false;
        }
    }

    /**
     * This method is overridden to support the case where the legacy store
     * loads after the regular store.
     */
    ,setValue: function(value) {
        var store = this.store,
            legacyStore = this.legacyStore;

        if (legacyStore) {

            // If the legacy store has not been loaded, trigger it.
            if (!legacyStore.lastOptions) {
                if (!legacyStore.loading) {
                    legacyStore.load();
                }
            }

            // Legacy store is loading, we must wait for it to be done before
            // processing the value.
            if (legacyStore.loading) {

                // This is done in the parent setValue method, but we are not
                // sure it will be called in time so we must replicate this 
                // code here.
                this.value = value;
                this.setHiddenValue(value);

                // If the regular store is loading, then this method will be
                // called again when it is done. We will see at this time if we
                // still have to wait for the legacy store. In the other case,
                // we have to simulate a call to onLoad when the legacy store
                // has loaded.
                if (!store.loading) {
                    legacyStore.on({
                        single: true
                        ,scope: this
                        ,load: function(legacyStore, records, success) {
                            this.onLoad(store, store.getRange(), success);
                        }
                    });
                }
            }
            // Legacy store is loaded, that's OK to continue.
            else {
                this.callParent(arguments);

                // Implements restoreOriginalValue.
                if (this.restoreOriginalValue) {
                    this.lastSelection = this.originalValue;
                }
            }

            // setValue must return this
            return this;
        }
        // No legacy store, just defer to the super method.
        else {
            return this.callParent(arguments);
        }
    }
});

Finally, here's a live demo with plenty of examples demonstrating that this ux works with all combinations of synchronous and asynchronous stores and forceSelection... While setRawValue does not.

0
votes

I think you can do this by creating an Ext.XTemplate, and setting that template as the displayTpl property on the combobox. Then in your template you can have a method to test for the records that should be displayed differently.

The displayTpl config is not documented, but if you look at the ComboBox Source Code and search for me.displayTpl you can see that they just create a default displayTpl if one does not exist.

0
votes

You just need to add that record to the store after it loads using store.add({id:'d',name:'doll'})

Here is an example that is not what you need but shows the concept:

Ext.define('MyApp.store.Users', {
    extend:'Ext.data.Store',
    model:'MyApp.model.User',
    listeners:{
        load:function (store, recs) {
            store.add({uid:'', name:''});  //adding empty record to enable deselection of assignment
        }
    }
});