xhaque
xhaque

Reputation: 31

Meteor Autoform - disable select options in an array of Object fields

I'm wanting to disable an option if it has already been selected in one of the object groups.

So, if I selected "2013" then added another sample, "2013" would not be available in that group, unless that option is changed in the original group.

Is there an easy way to do this that I'm missing? Do I need to reactively update the schema when a selection is made?

samples:{
    type: Array,
    optional: true,
    maxCount: 5
},
"samples.$":{
    type: Object,
    optional: true
},

"samples.$.sample":{
    type:[String],
    autoform: {
        type: "select",
        options: function () {
            return [
            {
            optgroup: "Group",
            options: [
             {label: "2013", value: 2013},
             {label: "2014", value: 2014},
             {label: "2015", value: 2015}
             ]
            }
            ];
        }   
    }

},

Upvotes: 3

Views: 784

Answers (1)

Jankapunkt
Jankapunkt

Reputation: 8423

Proof of Concept

I know this post is about 3 years old. However, I came across the same issue and want to provide an answer for all those who also stumbled over this post.

This answer is only a proof of concept and does not provide a full generic and performant solution, that could be used on production apps.

A fully generic solution would require a deep change in the code of how select field options are generated and updated in AutoForm.


Some prior notes.

I am using Autoform >=6 which provides a good API to instantly obtain field and form values in your SimpleSchema without greater trouble. SimpleSchema is included as npm package and Tracker has to be passed to it in order to ensure Meteor reactivity.

Functions like AutoForm.getFieldValue are reactive, which is a real great improvement. However, reactively changing the select options based on a reactive value causes a lot of update cycles and slows the performance (as we will see later).

Using AutoForm.getFormValues is not working, when using it within options of an Object field. While working within Array field, it will not behave reactively in Object fields, thus not update the filtering on them.


Manipulating Options for Arrays of Select Inputs (failing)

You can't use it with array types of fields. It's because if you change the select options, it applies for all your select instances in the array. It will therefore also apply to your already selected values and strips them away, too. This makes your select looks like it is always "not selected"

You can test that yourself with the following example code:

new SimpleSchema({
    samples:{
        type: Array,
        optional: true,
        maxCount: 5
    },
    "samples.$":{
        type: String,
        autoform: {
            type: "select",
            options: function () {
                const values = AutoForm.getFormValues('sampleSchemaForm') || {};
                const samples = values && values.insertDoc && values.insertDoc.samples
                    ? values.insertDoc.samples
                    : [];
                const mappedSamples = samples.map(x => x.sample);
                const filteredOpts =  [
                    {label: "2013", value: "2013"},
                    {label: "2014", value: "2014"},
                    {label: "2015", value: "2015"}
                ].filter(y => mappedSamples.indexOf(y.value) === -1);

                return [
                    {
                        optgroup: "Group",
                        options:filteredOpts,
                    }
                ];
            }
        }
    },
}, {tracker: Tracker});


Using fixed values on an Object Field

when taking a closer look at the schema, I saw the maxCount property. This made me think, that if you anyway have a list of max options, you could solve this by having fixed properties on a samples object (by the way: maxCount: 5 makes no sense, when there are only three select options).

This causes each select to have it's own update, that does not interfere the others. It requires an external function, that keeps track of all selected values but that came out be very easy.

Consider the following code:

export const SampleSchema = new SimpleSchema({
    samples:{
        type: Object,
        optional: true,
    },
    "samples.a":{
        type: String,
        optional:true,
        autoform: {
            type: "select",
            options: function () {
                const samples = AutoForm.getFieldValue("samples");
                return getOptions(samples, 'a');
            }
        }

    },
    "samples.b":{
        type: String,
        optional:true,
        autoform: {
            type: "select",
            options: function () {
                const samples = AutoForm.getFieldValue("samples");
                return getOptions(samples, 'b');
            }
        }

    },
    "samples.c":{
        type: String,
        optional:true,
        autoform: {
            type: "select",
            options: function () {
                const samples = AutoForm.getFieldValue("samples");
                return getOptions(samples, 'c');
            }
        }

    },
}, {tracker: Tracker});

The code above has three sample entries (a, b and c) which will let their options be computed by an external function.

This function needs to fulfill certain requirements:

  • filter no options if nothin is selected
  • filter not the option, that is selected by the current samples select
  • filter all other options, if they are selected by another select

The code for this function is the following:

function getOptions(samples={}, prop) {

    // get keys of selections to
    // determine, for which one
    // we will filter options
    const sampleKeys = Object.keys(samples);

    // get sample values to
    // determine which values
    // to filter here
    const sampleValues = Object.values(samples);

    const filteredOptiond = [
        // note that values are stored as strings anyway
        // so instead of parsing let's make them strings
        {label: "2013", value: "2013"},
        {label: "2014", value: "2014"},
        {label: "2015", value: "2015"}
    ].filter(option => {

        // case 1: nothing is selected yet
        if (sampleKeys.length === 0) return true;

        // case2: this selection has a
        // selected option and current option
        // is the selected -> keep this option
        if (sampleKeys.indexOf(prop) > -1 && option.value === samples[prop])
            return true;

        // case 3: this selection has no value
        // but others may have selected this option
        return sampleValues.indexOf(option.value) === -1;
    });


    return [
        {
            optgroup: "Group",
            options: filteredOptiond,
        }
    ]
};

Some Notes on this Concept

Good: -it works -you can basically extend and scale it to your desired complexity (optgroups, more fields on samples, checking against other fields with other fields etc.)

Bad: - performance - bound to a given (or the nearest) form context (see here) - much more code to write, than for an array.

Upvotes: 0

Related Questions