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?
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).
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>
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>
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.