setting up v-autocomplete with search function - vue.js

Trying to setup v-autocomplete, without a watcher, the flow would be:
Type a string, value is accepted by function
Function searches api for string and returns a list
List is put into "entries"
Computed property "tagsFound" is reevaluated.
"tagsFound" are displayed (since they are :items)
The main difference between the docs here and my code is my attempt to do this without a watcher rather with a simple function.
Relevant code:
<v-autocomplete
v-model="newTag"
:items="tagsFound"
:loading="loading"
:search-input.sync="search"
color="white"
hide-no-data
hide-selected
:placeholder="$t('search_terms.new_tag')"
></v-autocomplete>
...
data() {
return {
newTag: '',
entries: [],
....
methods: {
...
async search(term){
this.query.term = term
this.entries = await this.searchTerms(this.query)
},
...
computed: {
tagsFound(){
return this.entries
}
}
Expected behavior is search for the term typed and display the results as a dropdown.
Actual behavior is that it does not search and therefore does not display anything.

The sync modifier effectively makes a prop behave like v-model, so just like with v-model there's a prop and an event. The value needs to be a property, not a method, so :search-input.sync="search" doesn't make sense if search is a method.
The tagsFound computed property in your example isn't really doing anything. If you're just going to return entries you might as well just use entries directly in your template.
Not sure why you would want to do this without a watch but it can be done, either by splitting search-input.sync into a prop/event pair or by using a computed property with a getter and setter. The example below uses the latter approach.
function fakeServer (search) {
return new Promise(resolve => {
setTimeout(() => {
resolve([
'Red', 'Yellow', 'Green', 'Brown', 'Blue', 'Pink', 'Black'
].filter(c => c.toLowerCase().includes(search.toLowerCase())))
}, 1000)
})
}
new Vue({
el: '#app',
data () {
return {
newTag: '',
entries: [],
queryTerm: ''
}
},
computed: {
search: {
get () {
return this.queryTerm
},
set (searchInput) {
if (this.queryTerm !== searchInput) {
this.queryTerm = searchInput
this.loadEntries()
}
}
}
},
created () {
this.loadEntries()
},
methods: {
async loadEntries () {
this.entries = await fakeServer(this.queryTerm || '')
}
}
})
<link href="https://unpkg.com/vuetify#1.5.16/dist/vuetify.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons" rel="stylesheet">
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<script src="https://unpkg.com/vuetify#1.5.16/dist/vuetify.js"></script>
<div id="app">
<v-app>
<v-autocomplete
v-model="newTag"
:items="entries"
:search-input.sync="search"
></v-autocomplete>
</v-app>
</div>

In this part, you bind search-input to an async method, this is wrong. You need to bind search-input to a data field and create a watch over it.
<v-autocomplete
:search-input.sync="search"
></v-autocomplete>
Define your component like below:
data: function(){
return {
newTag: '',
entries: [],
searchInput: null
}
},
watch: {
searchInput(val){
this.entries = await this.searchTerms(val)
}
}
And v-autocomplete template:
<v-autocomplete
v-model="newTag"
:items="tagsFound"
:loading="loading"
:search-input.sync="searchInput"
color="white"
hide-no-data
hide-selected
:placeholder="$t('search_terms.new_tag')"
></v-autocomplete>
This is a working example I created on CodePen

Related

Reset select values in v-autocomplete, in order to add multiple items

I'm creating an application that has a selection box to choose between some template data. However, the user should be able to select the same template option several times and, each time he selects the template, a new informational box appears in the screen.
My problem is that the v-autocomplete component doesn't enable this kind of behavior: we can select one option (or multiple options), but not the same option twice.
I thought about making something like this: every time the user selects the option A, the infobox would appear below and the component would reset to a empty option. Then, the user could choose option A again and, when he chooses it, another info box would appear, how many times the user needs it.
How could I do something like this using vue? I didn't found any component that would do something like this on default, so I think I'll have to tweak the component behavior, but I don't know exactly where to start.
My template:
<template>
<div class="select-wrapper" id="selectBox">
<v-autocomplete
class="select-input"
:items="items"
:name="label"
placeholder="select item"
solo
:value="value"
#change="$event => onChange($event, items)"
item-text="name"
item-value="value"
:required="required"
:rules="[
value =>
!required ||
!!value ||
"required"
]"
></v-autocomplete>
</div>
</template>
And my Vue code:
<script lang="ts">
import { defineComponent } from "#vue/composition-api";
import Vue from "vue";
interface SelectItem {
name: string,
value: string
}
interface SelectBoxProps {
items: SelectItem[];
value: string;
onSelect: ({ target }: { target?: SelectItem }) => void;
hasResetSelection: boolean;
}
export default defineComponent({
name: "SelectBox",
props: {
label: String,
items: Array,
value: [String, Number],
onSelect: Function,
disabled: Boolean,
required: {
type: Boolean,
default: false
},
hasError: Boolean,
errorMessage: String,
hasResetSelection: {
type: Boolean,
default: false
}
},
directives: {
ClickOutside
},
setup({ onSelect, hasResetSelection }: SelectBoxProps) {
const onChange = (selectedValue: string, itemsArr: SelectItem[]) => {
const targetItem = itemsArr.find(i => i.value === selectedValue);
if (hasResetSelection) {
Vue.nextTick(() => {
console.log("onselect should reset value");
return onSelect({ target: { name: "", value: "" } });
});
}
return onSelect({ target: targetItem });
};
return {
onChange
};
}
});
</script>
This was my last attempt with Vue.nextTick, I already tried to tweak the component with ref() and it didn't work as well. Do you have any suggestions?
You can use another variable just to hold the input for the autocomplete component Like this:
var app = new Vue({
el: '#app',
vuetify: new Vuetify(),
data: {
items: [{ name : 'hello', value : 1 }, { name : 'world', value : 2 }],
value : null,
values : []
},
methods: {
onChange() {
this.values.push(this.value)
this.$nextTick(() => {
this.value = null
})
},
}
})
<link href="https://cdn.jsdelivr.net/npm/vuetify#2.x/dist/vuetify.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.0"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify#2.x/dist/vuetify.js"></script>
<div id="app">
<v-app>
<v-container>
Values : {{values}}
<v-autocomplete
:items="items"
placeholder="select item"
solo
v-model="value"
item-text="name"
item-value="value"
#change="onChange"
/>
</v-container>
</v-app>
</div>

(vuetify in nuxt js) autocomplete isnt update relative to items prop

Every input in search i update the items prop but the v-autocomplete become empty
although the data in my component changed
i tried to add the no-filter prop it didnt help i guess something with the reactivity destroyed
i allso tried with computed property as an items but still same result
Every input in search i update the items prop but the v-autocomplete become empty
although the data in my component changed
i tried to add the no-filter prop it didnt help i guess something with the reactivity destroyed
i allso tried with computed property as an items but still same result
<script>
import ProductCartCard from "~/components/cart/ProductCartCard";
export default {
name: "search-app",
components: {
ProductCartCard
},
props: {
items: {
type: Array,
default: () => []
}
},
data() {
return {
loading: false,
filteredItems: [],
search: null,
select: null
};
},
watch: {
search(val) {
if (!val || val.length == 0) {
this.filteredItems.splice(0, this.filteredItems.length);
return;
} else {
val !== this.select && this.querySelections(val);
}
}
},
methods: {
querySelections(v) {
this.loading = true;
// Simulated ajax query
setTimeout(() => {
this.filteredItems.splice(
0,
this.filteredItems.length,
...this.items.filter(i => {
return (i.externalName || "").toLowerCase().includes((v || "").toLowerCase());
})
);
this.loading = false;
}, 500);
}
}
};
</script>
<template>
<div class="search-app-container">
<v-autocomplete
v-model="select"
:loading="loading"
:items="filteredItems"
:search-input.sync="search"
cache-items
flat
hide-no-data
hide-details
label="searchProduct"
prepend-icon="mdi-database-search"
solo-inverted
>
<template v-slot:item="data">
<ProductCartCard :regularProduct="data" />
</template>
</v-autocomplete>
</div>
</template>
One of the caveat of the v-autocomplete as described in the documentation:
When using objects for the items prop, you must associate item-text and item-value with existing properties on your objects. These values are defaulted to text and value and can be changed.
That may fix your issue

Using v-model on a button?

When a button is clicked I wish to push it's name to an array. When the button is not clicked I want to remove it's name from an array.
I know how to do this with an #click that pushes/splices the array.
I would like to know if there's a simple way of binding the clicks of the button to the array, just like how a checkbox works with v-model. I understand you cannot use v-model on a button but if we were to make the button it's own component and use v-model on that...
<custom-button v-model="myArray"></custom-button>
Is there a way to make this work?
I would create the structure for the custom-button components like:
...,
props: {
originalArray: {
required: true
}
},
data(){
return {
modifiedArray: this.originalArray.map(x => ({...x}))
}
},
methods: {
yourMethod()
{
//do your logic on the modifiedArray
this.$emit('changed',this.modifiedArray);
}
}
then you could use it like:
<custom-button :original-array="this.myArray" #changed="newArray => this.myArray = newArray" />
I would do it like this:
const CBtn = {
template: '#c-btn',
props: ['array', 'label'],
data(){
return {
ncTimeout: -1
}
},
computed:{
arr_proxy: {
get(){
// shallow copy to not modify parent array indices
return this.array.slice()
}
}
},
methods: {
update(){
this.notClicked()
if(!this.arr_proxy.includes(this.label))
this.$emit('update:array', this.arr_proxy.concat(this.label))
},
notClicked(){
clearTimeout(this.ncTimeout)
this.ncTimeout = setTimeout(()=>{
let index = this.arr_proxy.findIndex(v => v === this.label)
if(index>=0){
this.arr_proxy.splice(index, 1)
this.$emit('update:array', this.arr_proxy)
}
}, 1000)
}
}
}
new Vue({
components: {
CBtn
},
template: '#main',
data(){
return {arr: []}
}
}).$mount('#app')
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<template id="c-btn">
<button
#click="update"
v-on="$listeners"
v-bind="$attrs"
>
{{label}}
</button>
</template>
<template id="main">
<div>
<c-btn label="1" :array.sync="arr" ></c-btn>
<c-btn label="2" :array.sync="arr" ></c-btn>
<c-btn label="3" :array.sync="arr" ></c-btn>
{{arr}}
<div>
</template>
<div id="app"></div>
So yes you can use v-model with in model option defined value: [propName] and event: [eventName] or the .sync modifier with 'update:[propName]' event.

How to run a function in Vue.js when state changes

I'm looking to run a function when the state changes in my Vue app.
In my component I'm able to get the boolean state of isOpen. I'm looking to run a function that adds focus to my form input when the modal opens and isOpen is set to true. I've tried using a watcher but with no luck. I'm opening my modal by calling :class="{ 'is-open': search.isOpen }" in the html and showing it via css. Any help would be most appreciated.
data() {
return {
isFocussed: this.$store.state.search,
//checks the state of isOpen?
}
},
computed: {
search() { return this.$store.state.search },
},
watch: {
isFocussed() {
this.formfocus()
},
},
methods: {
formfocus() {
document.getElementById('search').focus()
},
please check my snippet which shows the good way to work in Vue.js, you can work with refs which is very helpful instead of document.getElementById()
new Vue({
el: '#app',
data: {
isOpen: false,
},
computed: {
},
watch: {
isOpen(){
if(this.isOpen){
this.$nextTick( function () {
this.formfocus();
}
);
}
}
},
methods: {
formfocus(){
this.$refs.search.focus();
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.2/vue.js"></script>
<div id="app">
<button v-on:click="isOpen = !isOpen">show modal</button>
<div v-if="isOpen">
<input ref="search" type="text" placeholder="search">
</div>
</div>
EDIT: i have added a conditional if on the watch, i hope this solves the problem
I am not sure what your template looks like but here is how I set focus on a conditional element.
element
<input type="text" class="search-input" v-model="search" :class="{'collapsed-search': hideInput}" placeholder="Enter keyword" ref="search">
notice the ref="search" on the input.
here is the method when the input condition is true
toggleSearch() {
this.hideInput = !this.hideInput
this.search = ''
setTimeout(() => {
this.$refs.search.focus()
}, 1000)
}
this.$refs.search.focus() is called after the element has been fully created which is the purpose of the setTimeout

Browser was freeze when i update array items in v-for block

I use v-autocomplete component and updating items list after http request. But browser was freeze when i set items. What's wrong?
{
mixins: [search_field_block],
data: function () {
return {
item_component: {
props: ['item'],
template: '<div v-html="item.full_name"></div>'
}
};
},
methods: {
search: function (text) {
this.search_text = text.trim();
if (this.search_text) {
this.doGET('/api-method/search_place/', {'query': this.search_text}, this.update_items);
}
},
update_items: function (data) {
this.items = data;
}
}
}
I use a mixin for other components. It contained universal template with v-autocomplete:
<field-block :label="label" :error="field_error" :description="item_description">
<v-autocomplete slot="input"
class="page-form__field required"
:class="{ focused: focused, 'not-empty': not_empty, error: field_error != null, 'list-open': is_list_open }"
v-model="value"
ref="autocomplete"
:required="required"
:inputAttrs="{ref: 'input', autocomplete: 'off'}"
:items="items"
:component-item="item_component"
:get-label="getLabel"
:min-len="2"
#update-items="search"
#item-selected="onSelect"
#input="onInput"
#blur="onBlur"
#focus="onFocus">
</v-autocomplete>
</field-block>
I was find v-autocomplete on the github. It contain a v-for block for rendering search results
I found the problem. I have "computed" property and set value to parent app:
computed: {
is_list_open: function () {
this.$parent.list_opened = this.focused && this.items.length > 0 || (this.$refs.autocomplete ? this.$refs.autocomplete.show : false);
return this.$parent.list_opened;
}
},
This is incorrect behavior.