Reputation: 23
I am trying to build a multiselect component to nicely display tags in my app.
I receive an objectTags parameter form the parent component, but this field needs to be slightly refactored to be correctly displayed, so I need to make it a computed() object.
Here is my code:
<template>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ label }}</label>
<Field v-slot="{ field, handleChange }" :name="name" :value="veeValue">
<multiselect
v-model="value"
:clear-on-select="true"
:close-on-select="false"
:multiple="true"
:options="extendedOptions"
:placeholder="placeholder"
:tag-placeholder="tagPlaceholder"
:taggable="true"
:label="labelKey"
:track-by="trackByKey"
:open-direction="'below'"
:name="name"
@select="(option) => toggleOption(option, field, handleChange)"
@remove="(option) => toggleOption(option, field, handleChange)"
>
<template #tag="{option, remove}">
<span class="multiselect__tag" :key="option[labelKey] + '-' + option[trackByKey]"
:style="{ backgroundColor: option.color, color: '#001657' }">
<span v-text="option[labelKey]"></span>
<i tabindex="1"
@keypress.enter.prevent="removeElement(option, remove, field, handleChange)"
@mousedown.prevent="removeElement(option, remove, field, handleChange)"
class="multiselect__tag-icon"></i>
</span>
</template>
<template #option="{option}">
<!-- Child row -->
<span v-if="option[parentKey]" class="pl-3">
<span>{{ option[labelKey] }}</span>
<span class="ml-2">{{ option.category }}</span>
</span>
<!-- Parent row -->
<span v-else :class="topLevelClass">
<span>{{ option[labelKey] }}</span>
<span class="ml-2">{{ option.category }}</span>
</span>
</template>
</multiselect>
</Field>
</div>
</template>
<script setup>
import Multiselect from "vue-multiselect";
import { computed, ref, onMounted } from "vue";
import { Field } from "vee-validate";
const props = defineProps({
name: {
type: String,
required: true
},
categorizedTags: {
type: Array,
required: true
},
objectTags:{
type: Array,
default: [],
},
options: {
type: Array,
required: true
},
selected: {
type: Array,
default: [],
},
label: String,
tagPlaceholder: String | null,
placeholder: String | null,
labelKey: {
type: String,
default: "name"
},
childrenKey: {
type: String,
default: "children"
},
parentKey: {
type: String,
default: "parent_id"
},
trackByKey: {
type: String,
default: "id"
},
topLevelClass:{
type: String,
default: 'font-bold'
},
});
const value = ref(props.selected);
const veeValue = (props.selected || []).map((option) => option[props.trackByKey])
function removeElement(option, multiselectRemove, veeField, handleChange) {
multiselectRemove(option);
toggleOption(option, veeField, handleChange);
}
function toggleOption(option, veeField, handleChange) {
const isAlreadyChecked = veeField.value.includes(option[props.trackByKey]);
handleChange(
isAlreadyChecked ?
veeField.value.filter(optionId => optionId !== option[props.trackByKey]) :
[...veeField.value, option[props.trackByKey]]
);
}
const extendedOptions = computed(() => {
let _options = [];
computedTags.value.forEach((option) => {
_options.push(option);
_options.push(...option[props.childrenKey] || []);
});
return _options;
});
const computedTags = computed(() => {
const extractTags = (tags, category) => {
let allTags = [];
tags.forEach(tag => {
allTags.push({
id: tag.id,
name: tag.name,
category: category.name,
color: category.color
});
if (tag.children && tag.children.length > 0) {
allTags = allTags.concat(extractTags(tag.children, category));
}
});
return allTags;
};
let allTags = [];
props.categorizedTags.forEach(category => {
if (category.root_tags && category.root_tags.length > 0) {
allTags = allTags.concat(extractTags(category.root_tags, category));
}
});
return allTags;
});
// const objectComputedTags = computed(() => {
// get: () => {
// let allTags = [];
// if (!props.objectTags) {
// return allTags;
// }
// props.objectTags.forEach(tag => {
// allTags.push({
// id: tag.id,
// name: tag.name,
// category_id: tag.category.id,
// category: tag.category.name,
// color: tag.category.color
// });
// });
// allTags.sort((a, b) => {
// if (a.category_id < b.category_id) return -1;
// else if (a.category_id > b.category_id) return 1;
// else return a.name.localeCompare(b.name);
// });
// return allTags;
// }
// });
</script>
<style scoped>
</style>
If I try to modify my code to replace the currently used v-model by my new one, I get errors in my console about calling invalid field objectComputedValues.value
(even though this field exists).
I am new to Vue so I hope I got it right but this is apparently due to the fact that computed values cannot be passed that easily into v-models.
When I try to modify my code to replace the currently used v-model by my new one (replace props.selected
with objectComputedValues.value
), I get errors in my console about calling invalid field objectComputedValues.value
even though this field exists.
I am new to Vue so I hope I got it right but this is apparently due to the fact that computed values cannot be passed that easily into v-models.
I tried to add a getter and setter to my objectComputedValues
callback function but it did not work.
I apparently found a solution using old style Vue, where I should have defined my component in a defineComponent() and my objectComputedValues as a data() field that I would update at mount, but I would prefer to keep my current syntax.
Any help would be appreciated :)
Upvotes: 0
Views: 112