Search code examples
vue.jsvuejs2vuejs3vue-component

Removing value from dropdown when adding a new value in vue application


I am trying to create date filter in my vue project. so for this, the view is:

<div class="dropdown-item d-flex flex-column">
                                    <div
                                        v-for="(filter, index) in dateFilters"
                                        :key="index"
                                        class="dropdown-item-inner"
                                    >
                                        <div class="mb-2 d-flex">
                                            <select v-model="filter.dateType" class="form-select mr-1">
                                                <option value="startDate">Start Date</option>
                                                <option value="endDate">End Date</option>
                                            </select>
                                            <select v-model="filter.filterType" class="form-select mr-1">
                                                <option
                                                    v-for="option in dateOptions"
                                                    :value="option.value"
                                                    :key="option.value"
                                                >
                                                    {{ option.label }}
                                                </option>
                                            </select>
                                            <input type="date" v-model="filter.date" class="form-control" />
                                            <button
                                                :disabled="dateFilters.length < 2"
                                                class="button is-transparent m-0 d-flex horizontal-center vertical-center"
                                                @click="clearDateFilterRule(index)"
                                            >
                                                <i class="fas fa-trash mr-2"></i>
                                            </button>
                                        </div>
                                    </div>
                                    <div class="date-filter-buttons">
                                        <button
                                            v-if="dateFilters.length < 8"
                                            class="button is-transparent m-0 d-flex horizontal-center vertical-center"
                                            @click="addDateFilter"
                                        >
                                            <i class="fas fa-plus mr-2"></i>Add Rule
                                        </button>
                                        <button
                                            type="button"
                                            class="button is-transparent m-0"
                                            @click="clearDateFilters"
                                        >
                                            Clear
                                        </button>
                                    </div>
                                </div>

see below for actual view:

so I have dateOptions and dateFilters:

data() {
        return {
            dateFilters: [
                {
                    dateType: "startDate",
                    filterType: "eq",
                    date: "",
                },
            ],
            dateOptions: [
                {value: "eq", label: "Date is"},
                {value: "not", label: "Date is not"},
                {value: "lte", label: "Date is before"},
                {value: "gte", label: "Date is after"},
            ],
        };
    },

and this is my add event:

addDateFilter() {
            const selectedValue = this.dateFilters.map((filter) => filter.filterType);

            this.dateOptions = this.dateOptions.filter(option => !selectedValue.includes(option.value));

            if (this.dateOptions.length > 0) {
                this.dateFilters.push({
                    dateType: "startDate",
                    filterType: this.dateOptions[0].value,
                    date: "",
                });
            }
        },

so my target is when I click add rule button which is addDateFilter event, I add the same row you see in the picture to the dateFilters. but I want to remove dateOptions from the dropdown (Date is). For example, if Date is is used in first row and pushed dateFilters in the next row in the middle dropdown,Date is should be removed. This part, I am successful. But what wrong is when I push the row in dateFilters the value of the first row is gone too.

How can I solve it?


Solution

  • The problem with your addDateFilter is that it's not reactive. When dropdowns change, the other dropdowns need to react to those changes, i.e. selecting a different option in one dropdown should make that original option available in all dropdowns.

    I'll first concentrate on just explaining it assuming "Start Date" as the only available dateType as this is how I worked it out first (or if you just want to skip to the final solution that takes into account both "Start Date" and "End Date", scroll down).

    "Start Date" only

    Using computed properties we can reactively keep track of all currently selected and unselected date options in the dropdowns.

    computed: {
      // getter for all selected options
      selectedOptions() {
        return this.dateFilters.map(df => df.filterType)
      },
      // getter for all unselected options
      unselectedOptions() {
        return this.dateOptions.filter(o => !this.selectedOptions.includes(o.value))
      }
    },
    

    The addDateFilter method can then simplify to just pushing a new filter to the dateFilters array

    addDateFilter() {
      this.dateFilters.push({
        dateType: 'startDate',
        filterType: this.unselectedOptions[0].value,
        date: ''
      })
    },
    

    Each dropdown should list all unselected options + it's currently selected option. I would return this list of valid options in a new method that takes the selected option as a parameter. This method can then be used in the v-for to return all valid options for each dropdown.

    // return unused options with currently selected option
    validDateOptions(val) {
      const selected = this.dateOptions.find(o => o.value === val)
      return [ ...this.unselectedOptions, selected]
    },
    
    <select v-model="filter.filterType" class="form-select mr-1">
      <option
        v-for="option in validDateOptions(filter.filterType)"
        :key="option.value"
        :value="option.value"
      >
        {{ option.label }}
      </option>
    </select>
    

    Vue SFC Playground example


    Allowing for both "Start Date" and "End Date"

    Since both "Start Date" and "End Date" can each have one of the same selected option, we'll have to add the dateType as an additional filter and param in the above methods. The only problem is we should avoid parameters for computed properties, so these will actually need to change to methods:

    methods: {
      // getter for all selected options
      selectedOptions(type) {
        return this.dateFilters.filter(df => df.dateType === type).map(df => df.filterType)
      },
      // getter for all unselected options
      unselectedOptions(type) {
        return this.dateOptions.filter(o => !this.selectedOptions(type).includes(o.value))
      },
    

    addDateFilter by default adds a new 'startDate' filter, but if all those filters are already added, it'll need to add an 'endDate' filter instead.

    addDateFilter() {
      let type = 'startDate'
      if (this.unselectedOptions(type).length === 0) {
        type = 'endDate'
      }
      this.dateFilters.push({
        dateType: type,
        filterType: this.unselectedOptions(type)[0].value,
        date: ''
      })
    },
    

    The new validDateOptions also now needs an additional param:

    // return unused options with currently selected option
    validDateOptions(type, val) {
      const selected = this.dateOptions.find(o => o.value === val)
      return [ ...this.unselectedOptions(type), selected]
    },
    

    Which is provided in the template code:

    <select v-model="filter.filterType" class="form-select mr-1">
      <option
        v-for="option in validDateOptions(filter.dateType, filter.filterType)"
        :key="option.value"
        :value="option.value"
      >
        {{ option.label }}
      </option>
    </select>
    

    Vue Playground example

    Unresolved issue

    The above code does not take into account what should happen if there are two dropdowns with the same option but different types and you change them to the same type, e.g.

    Start Date | Date is | mm/dd/yyy

    End Date | Date is | mm/dd/yy

    Changed to:

    Start Date | Date is | mm/dd/yyy

    Start Date | Date is | mm/dd/yy

    Which would be invalid, but it's not clear how this should be handled. You would need to add an event listener to the dateType dropdown with a method that can decide whether to ignore the change, display an error, swap the date type values, or something else. Since this wasn't part of your original problem and there's multiple ways to handle it I'll leave it up to you.