Vuetify async search is only submitting one character at a time and doesn't concat characters - vue.js

I have the following vue component:
<template>
<v-form ref="form" #submit.prevent="search">
<v-row class="pa-0">
<v-col cols="12" md="2" class="d-flex">
<v-autocomplete
:items="projects"
item-value="id"
:item-text="item => `${item.number.number} ${item.name}`"
outlined
v-model="form.project_id"
label="Project number"
:search-input.sync="project"
dense
hide-details="auto"
class="align-self-center"
clearable
/>
</v-col>
</v-row>
</v-form>
</template>
<script>
export default {
watch: {
project( value )
{
this.queryProjects( { search: '', number: value } );
}
},
data()
{
return {
project: '',
projects: [],
}
},
methods: {
async queryProjects( search )
{
console.log(search);
if(!search)
{
return;
}
let response = await fetch(route('projects.search', search));
this.projects = await response.json();
},
}
}
</script>
This component should filter projects based on project number. The queryProject function is triggered but the problem is in the value from the project watcher. After each number entered the autocomplete field is set back to null and so it doesn't concat the complete project number. So if you would like to search for 19320 each number is parsed one by one and not as a whole number.
When a character is typed the output in console is the following:
1 for watcher value
{number: "1"} for queryProject search value
null for watcher value so looks like each character resets the input or rerenders the component.

The problem was in the line
:item-text="item => `${item.number.number} ${item.name}`"
When changed to just item-text="name" everything was working again.
A related bug report: https://github.com/vuetifyjs/vuetify/issues/11370

Related

Pass one string piece of data from parent component to child. Vue.js 2

Hoping someone can see a simple mistake I'm making and help me correct it.
I'm trying to pass one string variable as a prop from a parent to a child. This string reveals itself on a checkbox select. There are three and depending which is selected, the name associated will be passed.
Basically, showing some components if different checkboxes are checked. I can clearly see the variable I'm passing shows up in the parent component just fine. I log it out in the method provided. However, it doesn't pass to the child component. I log out there and get undefined as the result.
I've tried multiple solutions, and read about passing props, but nothing is working.
Parent component looks like this.
<template>
<v-col>
<v-checkbox
label="LS"
color="primary"
value="ls"
v-model="checkedSensor"
#change="check($event)"
hide-details
/></v-checkbox>
</v-col>
<v-col cols="2" class="mx-auto">
<v-checkbox
label="DG"
color="primary"
value="dg"
v-model="checkedSensor"
#change="check($event)"
hide-details
/>
</v-col>
<v-col cols="2" class="mx-auto">
<v-checkbox
label="CR"
color="primary"
value="cr"
v-model="checkedSensor"
#change="check($event)"
hide-details
/>
<ls-sensor v-if="lsSensor" :sensorType="checkedSensor" />
</template>
<script>
export default{
data() {
return {
checkedSensor: " ",
lsSensor: false,
dgSensor: false,
crSensor: false,
};
},
methods: {
check: function (e) {
this.showDeviceComponent(e);
},
showDeviceComponent(e) {
if (e == "ls") {
this.lsSensor = true;
this.crSensor = false;
this.dgSensor = true;
console.log("file input component", this.checkedSensor)
} else if (e == "dg") {
this.dgSensor = true;
this.lsSensor = false;
this.crSensor = false;
} else {
this.crSensor = true;
this.dgSensor = false;
this.lsSensor = false;
}
},
},
}
</script>
Child component (just putting the script here as I don't have it's place in the template yet. In the mounted method, I should be seeing the value from the prop. It's showing up as undefined. Gahh.. what silliness I have made a mistake on? Your help is greatly appreciated.
export default {
props:['sensorType'],
data() {
return {
coating: false,
};
},
mounted() {
console.log("required input component, sensor type", this.sensorType)
},
components: {
coatings: require("#/components/shared/FormData/Coatings.vue").default,
"ls-sensor-panel": require("#/components/shared/FormData/LS_SensorPanel.vue").default,
},
};
The only thing that stands out to me is when you create camel cased property names, I am pretty sure that those have to be written as dash (-) separated attributes on a component?
<ls-sensor v-if="lsSensor" :sensor-type="checkedSensor" />
So prop: ['sensorType'] is passed as :sensor-type=""

Value of v-text-field returns null when text area has a value

I'm trying to get the value from vuetify's v-text-field. The problem I am having is that I thought by typing something in, the field will automatically assign whatever is typed to vue-text-field's value. However this doesn't seem to be the case. Below is the code directly from vuetify with some additional code attempting to extract the value:
<template>
<v-card
class="overflow-hidden"
color="purple lighten-1"
dark
>
<v-toolbar
flat
color="purple"
>
<v-icon>mdi-account</v-icon>
<v-toolbar-title class="font-weight-light">
{{setterTitle}}
</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn
color="purple darken-3"
fab
small
#click="isEditing = !isEditing"
>
<v-icon v-if="isEditing">
mdi-close
</v-icon>
<v-icon v-else>
mdi-pencil
</v-icon>
</v-btn>
</v-toolbar>
<v-card-text>
<v-text-field ref="rowCount"
:disabled="!isEditing"
:value="2"
autofocus
color="white"
label="Number of Rows"
></v-text-field>
<v-autocomplete
:disabled="!isEditing"
:items="states"
:filter="customFilter"
color="white"
item-text="name"
label="Number of Columns"
></v-autocomplete>
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
:disabled="!isEditing"
color="success"
#click="save"
>
Save
</v-btn>
</v-card-actions>
<v-snackbar
v-model="hasSaved"
:timeout="2000"
absolute
bottom
left
>
Your profile has been updated
</v-snackbar>
</v-card>
</template>
<script>
export default {
props: {
setterTitle: String
},
data () {
return {
hasSaved: false,
isEditing: null,
model: null,
states: [
{ name: 'Single', abbr: 'S', id: 1 },
{ name: 'Double', abbr: 'D', id: 2 },
{ name: 'Triple', abbr: 'T', id: 3 },
{ name: 'Custom', abbr: 'C', id: 0 },
// { name: 'New York', abbr: 'NY', id: 5 },
],
}
},
methods: {
customFilter (item, queryText, itemText) {
const textOne = item.name.toLowerCase()
const textTwo = item.abbr.toLowerCase()
const searchText = queryText.toLowerCase()
console.log(textOne, textTwo,itemText)
return textOne.indexOf(searchText) > -1 ||
textTwo.indexOf(searchText) > -1
},
save () {
this.isEditing = !this.isEditing
this.hasSaved = true
console.log(this.$refs.rowCount.value)
},
},
}
</script>
As you can see, in the v-text-field tag I added a ref. I then tried to use that reference to extract the value from the save() method. Provided that I also have :value="2" assigned, this.$refs.rowCount.value always return 2. However if I don't have the value property set as 2 then this.$refs.rowCount.value returns null even though I've typed something in the v-text-field. I'm guessing I am misunderstanding something here. Maybe when I type to the field, it doesn't automatically assign what is typed to the value? Thank you for your help.
I've attached the api for v-text-field https://vuetifyjs.com/en/api/v-text-field/#component-pages
v-text-field is designed as common custom input control in Vue - it has a value prop and a input event which means it can by used with v-model - see the docs how that works
As a user, you do this:
<template>
<v-text-field v-model="text" />
</template>
<script>
export default {
data: function() {
return {
text: '' // initial value
}
}
}
</script>
To your question - value is a prop. All props in Vue are one way only - parent can send data to the component (and change that data in the future) but child component cannot change value of the prop it receives. That's why when you type something into the textbox the v-text-field must use an input event to tell the parent component that something has changed and that parent should update "it's own source" of value prop...
v-model is Vue's "syntactic sugar" how to work with these two more easily...

Update data from local copy of Vuex store

I am implementing a user profile edit page that initially consists of the data loaded from the vuex store. Then the user can freely edit his data and finally store them in the store.
Since the user can also click the cancel button to revert back to his original state, I decided to create a 'local' view copy of the user data fetched from the store. This data will be held in the view and once the user presses save, they will be saved in the store.
The view looks as following:
<template class="user-profile">
<v-form>
<template v-if="profile.avatar">
<div class="text-center">
<v-avatar width="120" height="120">
<img
:src="profile.avatar"
:alt="profile.firstname"
>
</v-avatar>
</div>
</template>
<div class="text-center mt-4">
<v-btn
color="primary"
dark
#click.stop="showImageDialog=true"
>
Change Image
</v-btn>
</div>
<v-row>
<v-col>
<v-text-field
label="First name"
single-line
disabled
v-model="profile.firstname"
></v-text-field>
</v-col>
<v-col>
<v-text-field
label="Last name"
single-line
disabled
v-model="profile.lastname"
></v-text-field>
</v-col>
</v-row>
<v-text-field
label="Email"
single-line
v-model="profile.email"
></v-text-field>
<v-text-field
id="title"
label="Title"
single-line
v-model="profile.title"
></v-text-field>
<v-textarea
no-resize
clearable
label="Biography"
v-model="profile.bio"
></v-textarea>
<v-dialog
max-width="500"
v-model="showImageDialog"
>
<v-card>
<v-card-title>
Update your profile picture
</v-card-title>
<v-card-text>
<v-file-input #change="setImage" accept="image/*"></v-file-input>
<template v-if="userAvatarExists">
<vue-cropper
ref="cropper"
:aspect-ratio="16 / 9"
:src="profile.avatar"
/>
</template>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="green darken-1"
text
#click="showImageDialog=false"
>
Cancel
</v-btn>
<v-btn
color="green darken-1"
text
#click="uploadImage"
>
Upload
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<div class="mt-8">
<v-btn #click="onUpdateUser">Update</v-btn>
</div>
</v-form>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
import VueCropper from 'vue-cropperjs';
import 'cropperjs/dist/cropper.css';
export default {
components: { VueCropper},
mounted() {
this.profile = this.getUserProfile ? this.getUserProfile : {}
},
data() {
return {
profile: {},
avatar: null,
userAvatarExists: false,
showImageDialog: false,
}
},
watch: {
getUserProfile(newData){
this.profile = newData;
},
deep: true
},
computed: {
...mapGetters({
getUserProfile: 'user/me',
})
},
methods: {
...mapActions({
storeAvatar: 'user/storeAvatar',
updateUser: 'user/update'
}),
onUpdateUser() {
const data = {
id: this.profile.id,
email: this.profile.email,
title: this.profile.title,
bio: this.profile.bio,
avatar: this.profile.avatar,
}
this.updateUser(data)
},
uploadImage() {
this.$refs.cropper.getCroppedCanvas().toBlob((blob => {
this.storeAvatar(blob).then((filename => {
this.profile.avatar = filename.data
this.$refs.cropper.reset()
}));
this.showImageDialog = false
}));
},
setImage(file) {
this.userAvatarExists = true;
if (file.type.indexOf('image/') === -1) {
alert('Please select an image file');
return;
}
if (typeof FileReader === 'function') {
const reader = new FileReader();
reader.onload = (event) => {
this.$refs.cropper.replace(event.target.result);
};
reader.readAsDataURL(file);
} else {
alert('Sorry, FileReader API not supported');
}
}
}
}
</script>
Issues/Questions:
As you can see from the code, after the user changes his profile
picture, the image should be rendered based on the
v-if="profile.avatar". The issue is that after the
profile.avatar is set in the uploadImage function, the
template does not see this change and no image is rendered.
However if I change the code so that the profile.avatar becomes
just avatar (it is no longer within the profile object), the
template starts to see the changes and renders the image
correctly. Why so? Does it have something to do with making a
copy from the store in the watch function?
Is it in general a good approach to keep the profile just as a local
view state or should it rather be stored in the vuex store even if
it is just a temporary data?
As you can see in the mounted function, I am setting the profile
value based on the getUserProfile getter. This is because the
watch function does not seem to be called again when switching
routes. Is there any other way how to do this?
The issue is due to the reactivity of data properties
You have used profile as an object, default it doesn't have any properties like avatar or firstname, its just empty
In vue js, If you are declaring an object, whatever the key mention in the declaration is only the part of reactivity. Once the keys inside profile changes, it rerenders the template
But still you can add new properties to a data property object by using $set
lets say in data you have declared
profile: {}
if you want to set avatar as new reactive property in runtime use
this.$set(this.profile, key, value)
which is
this.$set(this.profile, avatar, imageData)
In your above code, the setIuploadImage function
uploadImage() {
var self = this;
self.$refs.cropper.getCroppedCanvas().toBlob((blob => {
self.storeAvatar(blob).then((filename => {
self.$set(self.profile, "avatar", filename.data)
self.$refs.cropper.reset()
}));
self.showImageDialog = false
}));
},
this won't work inside arrow function in vuejs, so just preserved the this inside another variable "self" and used inside arrow function
Also in mounted function, if this.getUserProfile returns empty object, then as per javascript empty object is always truthy and directly assigning object to profile doesn't make the object reactive
mounted() {
this.profile = this.getUserProfile ? this.getUserProfile : {}
},
above code can be written as
mounted() {
if (this.getUserProfile && Object.keys(this.getUserProfile).length) {
var self = this;
Object.keys(this.getUserProfile).map(key => {
self.$set(self.profile, key, self.getUserProfile[key])
});
} else {
this.profile = {};
}
}

Prevent vuetify checkbox from checking itself?

I want the v-checkbox to remain unchanged after I write in the text field.
What is happening:
The checkbox checks itself whenever I click outside the box or type in the text field below.
What is expected:
The checkbox to remain unchecked until the use checks it.
Code:
export default {
props: ['answer'],
data() {
return {
newAnswer: 'Your answer here...',
multiChoiceAnswers: {
answers: ['test1', 'test2'],
selected: [],
}
}
},
created() {
if (this.answer.answers.length > 1) {
this.multiChoiceAnswers.answers = this.answer.answers
this.multiChoiceAnswers.selected = this.answer.selected
} else {
console.log('Answer Template Generated')
}
},
methods: {
selectedAnswer(clickEvent, index) {
console.log(clickEvent, index)
//Checking wether or not the answer is a value or null
//in order to push or remove it from the selected answers
if (clickEvent === this.multiChoiceAnswers.answers[index]) {
this.multiChoiceAnswers.selected.push(clickEvent)
} else {
const selectedPosition = this.multiChoiceAnswers.selected.indexOf(this.multiChoiceAnswers.answers[index])
this.multiChoiceAnswers.selected.splice(selectedPosition, selectedPosition + 1)
}
this.$emit('newAnswer', this.multiChoiceAnswers)
},
changedAnswer(changedAnswer, index) {
console.log(changedAnswer)
//Getting previous selected answer position to replace later
const selectedPosition = this.multiChoiceAnswers.selected.indexOf(this.multiChoiceAnswers.answers[index])
//Changing the current value of answer[index] to input value in answers
this.multiChoiceAnswers.answers.splice(index, index + 1, changedAnswer)
//Changing the current value of answer[index] to input value in selected
this.multiChoiceAnswers.selected.splice(selectedPosition, selectedPosition + 1, changedAnswer)
this.$emit('newAnswer', this.multiChoiceAnswers)
},
Here is the template code:
<v-container>
<div :key="(answer, index)" v-for="(answer, index) in multiChoiceAnswers.answers">
<v-layout align-center>
<v-checkbox hide-details class="shrink mr-2" #click.prevent #change="selectedAnswer($event, index)" :value="answer"></v-checkbox>
<v-text-field class="checkbox-input" #input="changedAnswer($event, index)" :placeholder="answer"></v-text-field>
<v-btn #click="removeAnswer(index)">Remove</v-btn>
</v-layout>
</div>
</v-container>
Seems like I must have been to tired or drunk when I posted this ;)
Here is the completely new revised code I did to fix this solution:
<template>
<div>
<v-container>
<div :key="(answer, index)" v-for="(answer, index) in multiChoiceAnswers.answers">
<v-layout align-center>
<v-checkbox hide-details class="shrink mr-2" v-model="answer.selected"></v-checkbox>
<v-text-field class="checkbox-input" v-model="answer.answerText" :placeholder="answer.answerText"></v-text-field>
<v-btn #click="removeAnswer(index)">Remove</v-btn>
</v-layout>
</div>
</v-container>
<v-btn #click="newAnswerOption">Add Answer</v-btn>
</div>
<script>
export default {
props: ['answer'],
data() {
return {
newAnswer: { answerText: 'Your answer here...', selected: false },
multiChoiceAnswers: {
answers: [
{ answerText: 'test1', selected: false },
{ answerText: 'test2', selected: false }
],
},
}
},
created() {
if (this.answer.answers.length > 1) {
this.multiChoiceAnswers.answers = this.answer.answers
} else {
console.log('Answer Template Generated')
}
},
methods: {
newAnswerOption() {
this.multiChoiceAnswers.answers.push(this.newAnswer)
this.$emit('newAnswer', this.multiChoiceAnswers)
},
removeAnswer(index) {
//Removing the answer
this.multiChoiceAnswers.answers.splice(index, 1)
this.$emit('newAnswer', this.multiChoiceAnswers)
}
}
}
</script>
What has changed?
I deleted all the previous code that was broken and necessarily complex.
I created a new data array with the objects answers. Each object now has the answerText (string) and the selected (boolean).
The checkbox is now connected to change the answers.selected with v-model
The input is now connected to change the answers.answerText with v-model

Validate vuetify textfield only on submit

temp.vue
<v-form ref="entryForm" #submit.prevent="save">
<v-text-field label="Amount" :rules="numberRule"r></v-text-field>
<v-btn type="submit">Save</v-btn>
</v-form>
<script>
export default {
data: () => ({
numberRule: [
v => !!v || 'Field is required',
v => /^\d+$/.test(v) || 'Must be a number',
],
}),
methods: save () {
if (this.$refs.entryForm.validate()){
//other codes
}
}
}
</script>
What happens here is while typing in the text field itself the rule gets executed. I want to execute the rule only on submit. How to do that in vuetify text field?
Vuetify rules are executed when the input gets value,
But if you want that to happen only on the form submit, you have remodified the rules that are being bound to that input,
Initially, rules should be an empty array, when you click on the button you can dynamically add/remove the rules as you wanted, like this in codepen
CODEPEN
<div id="app">
<v-app id="inspire">
<v-form ref="entryForm" #submit.prevent="submitHandler">
<v-container>
<v-row>
<v-col
cols="12"
md="6"
>
<v-text-field
v-model="user.number"
:rules="numberRules"
label="Number"
required
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-btn type="submit" color="success">Submit</v-btn>
</v-row>
</v-container>
</v-form>
</v-app>
</div>
new Vue({
el: '#app',
vuetify: new Vuetify(),
data: () => ({
valid: false,
firstname: '',
user: {
number: ''
},
numberRules: []
}),
watch: {
'user.number' (val) {
this.numberRules = []
}
},
methods: {
submitHandler () {
this.numberRules = [
v => !!v || 'Field is required',
v => /^\d+$/.test(v) || 'Must be a number',
]
let self = this
setTimeout(function () {
if (self.$refs.entryForm.validate()){
//other codes
alert('submitted')
}
})
}
}
})
If you're like me and just want to prevent validation from running on every key stroke, apply validate-on-blur prop on your text fields and now validation will only be perform after user has completed typing the whole input.
So not an exact answer to the OP, but I think this is what most of us want to achieve. This prop has been documented here.
I have another way to solve this problem without setting up watchers:
<v-form lazy-validation v-model="valid" ref="form">
<v-text-field
class="w-100"
light
label="Nome"
v-model="form.nome"
:rules="[rules.required]"
rounded
required
outlined
hide-details="auto"
></v-text-field>
<v-btn
rounded
height="50"
width="200"
:disabled="!valid"
:loading="isLoading"
class="bg-btn-secondary-gradient text-h6 white--text"
#click="submitContactForm()"
>
Enviar
</v-btn>
</v-form>
There is a prop called lazy-validation on vuetify, as you can see on the docs: https://vuetifyjs.com/en/api/v-form/#functions
So, the v-form has a method that you can see through $refs called validate(), and it can return true or false, based on your form rules.
And, the function that will trigger the validation on submit will be like this:
submitContactForm() {
const isValid = this.$refs.form.validate();
if (isValid) {
alert("Obrigado pelo contato, sua mensagem foi enviada com sucesso!");
this.form = {
nome: "",
celular: "",
email: "",
mensagem: ""
};
this.$refs.form.resetValidation(); // Note that v-form also has another function called resetValidation(), so after we empty our fields, it won't show the validation errors again.
}
},