I'm trying to create a dropdown menu with a list I have populated from the backend. Here's the library in question Vue Treeselect
Once the user tries to enter something that's not inside, I want to be able to dynamically add it, and later when the request gets submited, create that value on the backend. However, the library doesn't seem to provide a way to override the default behaviour. Here's what I tried so far.
https://codesandbox.io/s/musing-sutherland-i5e8f?fontsize=14&hidenavigation=1&theme=dark
<template>
<div id="app">
<div class="container mt-4 mx-auto">
<treeselect
@search-change="handleSearch"
:multiple="true"
:options="options"
placeholder="Select your favourite(s)..."
no-results-text="No results found... Press enter to add"
v-model="value"
>
</treeselect>
<pre class="bg-gray-200 text-gray-600 rounded mt-4 p-4">{{
JSON.stringify(value, null, 2)
}}</pre>
<h5>Search text: {{ text }}</h5>
<button
@click="appendNewItem"
class="focus:outline-none text-white text-sm py-2.5 px-5 rounded-md bg-blue-500 hover:bg-blue-600 hover:shadow-lg"
>
Add
</button>
</div>
</div>
</template>
<script>
// import the component
import Treeselect from "@riophae/vue-treeselect";
// import the styles
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
export default {
name: "App",
components: {
Treeselect,
},
data() {
return {
lastId: 0,
text: "",
value: [],
options: [
{ id: 1, label: "Option #1" },
{ id: 2, label: "Option #2" },
],
};
},
methods: {
handleSearch(ev) {
this.text = ev;
},
makeId() {
return `new-item-${++this.lastId}`;
},
appendNewItem() {
this.options = [...this.options, { id: this.makeId(), label: this.text }];
},
},
};
</script>
Even my button solution doesn't work, because as soon as you leave the area of the tree-select input, the text gets reset to an empty string, so pressing the button adds an empty text.
Given the current documentation of Vue-Treeselect
how can I make it call my appendNewItem()
function when the user presses enter
and the treeselect has no results?
Ideally, I would like to do something like this:
<treeselect (other-props)>
<template #no-results={ node }>
<span>No results found for {{ node.text }}</span>
<button @click="appendNewItem">Add {{ node.text }}</button>
</template>
</treeselect>
But, unfortunately this is not supported by the library API. And it still doesn't solve the question of "creating a new field when enter key is pressed" but it would be a nice start regardless.
Have to say, this is a tough one.
The library doesn't provide a way to enable the functionality you've described but what you can do is use some low-level Vue APIs to override the library methods and try to achieve the effect you need.
Essentially, we're gonna override the select(node)
function from the library in an attempt to make it bend to our needs.
<script>
import Treeselect from "@riophae/vue-treeselect";
export default {
extends: Treeselect,
data() {
return {
overridesLastNodeId: 0,
};
},
methods: {
overridesFindValue() {
if (this.$refs.control) {
const childRefs = this.$refs.control.$refs;
if (childRefs["value-container"]) {
const valueContainer = childRefs["value-container"];
if (valueContainer.$refs.input) {
return valueContainer.$refs.input.value;
}
}
}
return null;
},
overridesCheckValueInNodes(value) {
let childHasValue = false;
this.traverseAllNodesDFS((node) => {
if (node.label === value) {
childHasValue = true;
}
});
return childHasValue;
},
select(node) {
/**
* Here we override the select(node) method from
* the library, we will inject a new node if a node
* doesn't exist and then proxy this method to the original!
*/
const value = this.overridesFindValue();
if (typeof value === "string" && value.length === 0) {
// This function gets called internally a lot, so we need
// to make sure it's proxied when there is no value
return Treeselect.mixins[0].methods.select.call(this, node);
}
if (value && value !== "") {
if (this.overridesCheckValueInNodes(value)) {
// If there is a value, we just fallback to the default function
this.resetSearchQuery();
return Treeselect.mixins[0].methods.select.call(this, node);
}
}
/**
* Finally, here's the solution to your question.
* We can emit a new node here, call your append function
* sending it the ID and making this work.
*/
const id = `new-node-${++this.overridesLastNodeId}`;
this.$emit("new-node", { value, id });
/**
* Additionally, to make the select select our value
* we need to "emit" it to v-model as well
*/
this.$emit("input", [...this.value, id]);
/**
* Finally, let's reset the input
*/
this.resetSearchQuery();
},
},
};
</script>
Then, remember to use the overridden component in your code:
<template>
<div class="container mt-4 mx-auto">
<treeselect-extended
:multiple="true"
:options="options"
placeholder="Select your favourite(s)..."
no-results-text="No results found... Press enter to add"
v-model="value"
@new-node="appendNewItem"
/>
<pre class="bg-gray-200 text-gray-600 rounded mt-4 p-4">{{
JSON.stringify(value, null, 2)
}}</pre>
</div>
</template>
<script>
import TreeselectExtended from "./overrides/TreeselectExtended";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
export default {
name: "App",
data() {
return {
value: [],
options: [
{ id: 1, label: "Option #1" },
{ id: 2, label: "Option #2" },
],
};
},
components: {
TreeselectExtended,
},
methods: {
appendNewItem({ value, id }) {
this.options = [...this.options, { id, label: value }];
},
},
};
</script>
This IS a working solution, however, I would have to recommend using this code with caution, as it creates dependencies with internal implementation with the library! That means if you update the library from package.json
you're introducing a breaking change to your project even in case of minor version updates! Because this code relies even on "private" functions from the library, and not just the public-facing API. You could try to future-proof it, but it might be smarter to just opt-in and use some different library that does provide your needs.
Here is a Codesandbox that demonstrates this: Link