v-model not always updating in Vue - vue.js

Short question
The v-model which binds a string to an input field won't update in some cases.
Example
I am using Vue within a Laravel application. This is the main component which contains two other components:
<template>
<div>
<select-component
:items="items"
#selectedItem="updateSelectedItems"
/>
<basket-component
:selectedItems="selectedItems"
#clickedConfirm="confirm"
#clickedStopAll="stopAll"
/>
<form ref="chosenItemsForm" method="post">
<!-- Slot for CSRF token-->
<slot name="csrf-token"></slot>
<input type="text" name="chosenItems" v-model="selectedItemsPipedList" />
</form>
</div>
</template>
<script>
export default {
props: ["items"],
data: function() {
return {
selectedItems: [],
selectedItemsPipedList: ""
};
},
methods: {
updateSelectedItems: function(data) {
this.selectedItems = data;
this.selectedItemsPipedList = this.selectedItems
.map(item => item.id)
.join("|");
},
confirm() {
this.$refs.chosenItemsForm.submit();
},
stopAll() {
this.updateSelectedItems([]);
this.confirm();
}
}
};
</script>
The method updateSelectedItems is called from the select-component and it works fine. In the end, the selectedItemsPipedList contains the selected items from the select-component, which looks like "1|2|3" and this value is bound to the input field in the chosenItemsForm. When the method confirm is called from the basket-component, this form is posted to the Laravel backend and the post request contains the chosen items as piped list. So far, so good.
The method stopAll is called from the basket-component and it will remove all the selected items from the array. Therefore it will call the method updateSelectedItems with an empty array, which will clear the selectedItems array and then clear the selectedItemsPipedList. After that, confirm is called which will post the form again. But, the post value still contains the selected items (e.g. '1|2|3'), instead of "". It looks like the v-model in my form is not updated, which is strange because it does work when selecting items. Why is it working when adding items, and doesn't when removing all items?

I believe you have a timing issue here. The value of the properties haven't been propagated to the DOM yet, so the form submission is incorrect. Try this instead:
stopAll() {
this.updateSelectedItems([]);
//NextTick waits until after the next round of UI updates to execute the callback.
this.$nextTick(function() {this.confirm()});
}

Related

Vue JS - Trigger Method if input lost focus

Quick question about Vue JS.
On my website, I have got a shopping cart and the user can enter any quantity. I am saving this quantity to the database table when he enters it...
issue is, the input field keeps firing save method every single digit of user types. For example, if the user types 123, the save method called 3 times. 1 and 12 and 123
I just want to call this save method when this input loses the focus, so I can save only once, not every single digit.
Component
Vue.component('product-counter', {
props: ['quantity'],
data: function () {
return {
count: this.quantity
}
},
template: `
<div class="input-group ">
<input v-bind:value="quantity" v-on:input.focus="$emit('input', $event.target.value)" type="text" class="form-control col-2">
</div>
`
})
Component Call
<product-counter
v-bind:productcode="item.productCode"
v-bind:quantity="item.quan"
v-on:input="item.quan=updateItemQuantity($event,item.productCode)"
v-on:increment-quantity="item.quan=updateItemQuantity(item.quan,item.productCode)"
v-on:decrement-quantity="item.quan=updateItemQuantity(item.quan,item.productCode)"></product-counter>
Vue: Method
"updateItemQuantity": function (totalquantity, pcode) {
if (totalquantity != '') {
... Update Database...
}
}
You're listening to the input event, which is triggered every time the value of the input changes, so every time a character is typed or removed.
Instead, you should listen to the blur event, which only fires when an input loses focus.
You can pass this along through your component, the same way you pass through the input event.
TLDR: Couple UpdateItemQuantity to v-on:blur instead of v-on:input, and make sure to $emit the blur event from your products-counter component.
Tip: Separate the client-side state (item.quan) and your server-side 'state' (your database) into two different methods. You want the value to reflect what the user is typing in real-time (input), which conflicts with what you want for updating the database (not real-time, only on blur). Otherwise you may get cases where users can't see what they type, as item.quan is never updated.
I think you just need to use #change instead of #input
It could be that you should use blur event.
Vue:
new Vue({
el: "#app",
data: {
quantity: ''
},
methods: {
printConsole: function () {
console.log('blured');
}
}
})
html:
<div id='app'>
<input v-bind:value="quantity" v-on:blur="printConsole" type="text" class="form-control col-2">
</div>
see jsfiddle for reference: https://jsfiddle.net/erezka/h8g62xfr/11/
blur will emit your change only after you focus out of the input

V-model binding to generated input text field

Dynamic V-models created during an ajax request doesn't update when I try inputting a value
I'm using vue2.x and axios. I want to get the value set in generated input when user submit the form. I managed to set v-model on this input during ajax request
I receive this HTLM as response:
<input type="text" value="" v-model="generatedcode">. But after submitting the form the value is still empty. Looks like Vue ignore the v-model directive. How can I fix it ?
Here is my code :
VUE
var app = new Vue({
el: '#subcribtionform',
data: {
generatedform:'',
generatedcode:''
},
methods:{
OnSuccess(response){
this.generatedform = response.data;
},
OnclickSub(){
axios.post('/submitformURL',{
lastname: this.lastname,
generatedcode: this.generatedcode,
})
}
created: function () {
axios.get('/generate_inputURL').then(this.OnSuccess);
}
HTML
<div v-html="generatedform"></div>
GENERATED INPUT
<input type="text" value="" v-model="generatedcode"/>
Try:
created: function () {
axios.get('/generate_inputURL').then(res => this.OnSuccess(res));
}
Component data must be function not object. You should be seeing warning about this in console. I guess your component is not reactive data then, which means that is not redrawn after on request done.
data(): {
return {
generatedform:'',
generatedcode:''
}
}
As I can see, you want to change the vue-app template using v-html attr - I think this is not possible. While mounting the application, the template compiles into render function, so your trick does not make any sense. You can try to do the following:
Construct the template (using html recieved from server as you want ) as string or as hidden el in the DOM
Set it in your app object - template:your_html_template
Create vue app

Saving the select filters values of items in list component after going back from viewing an item component

After the user goes back from viewing the item (question) component, I am trying to keep the user's filtered list of questions based on two selects (category and sub category) and selected page from pagination in the questions list component.
I am new to Vue environment and not sure if I am following the best practice but here what I have tried to do:
1- I tried using the event bus, but I reached a point where I am able to get an object that contains the question category object to the main component and then it would be used as an attribute inside of the Onchange method which normally be triggered after the category select event happens. I was able to show the user the same list of filtered items, however, the problem with this approach that:
a- I can not update the select values to show the selected options which gave the questions list.
I have tried to get the selected element using the ref attributes, however the select element is undefined even though I put it in the mounte stage of the life cycle. It works with an ugly hack by using a setTimeout method and it shows the element after one second.
b- The filtered list of items has pagination and this approach does not show the user the same page that they picked the item from.
c- Calling the server again
2- I tried to store a global value in mixins file, however after saving the value in the mixins file and even though the object value is received in the mixins file but after updating the mixins data and then calling it from the questions list, it returns an empty object.
3- I tried using keep-alive
Code for approach 1:
The category object is eager loaded with the question using an event emit. After the user goes back from viewing the question I pass the category object with the event using beforeDestroy method:
beforeDestroy () {
EventBus.$emit('backToQuestions', this.question.category);
}
This is how the category object look like:
{…}=>
__ob__: Object { value: {…}, dep: {…}, vmCount: 0 }
created_at:
id:
parent_id:
title:
updated_at:
This is how I populate the filtered questions list
created () {
EventBus.$on('backToQuestions', category => {
this.onChange(category)
});
this.fetch(`/categories`);
}
My select:
<div class="col-md-4">
<select ref="main" class="form-control" v-model="mainSelected" #change="onChange(mainSelected)">
<option disabled value="">Questions Category (All)</option>
<option v-for="option in parentCategories" :value="option">{{ option.title }}</option>
</select>
</div>
<div v-if="subCategory" class="btn-group">
<select class="form-control" v-model="subSelected" #change="onChange(subSelected)">
<option disabled value="" selected >{{ categoryTitle }} Qs Categories</option>
<option v-for="option in childCategories" :value="option">{{ option.title }}</option>
</select>
</div>
The following is my onChange method just for reference:
onChange(option) {
this.categoryOption = option;
this.dataReady = false;
this.subCategory = true;
this.questions= {};
this.questionsArray =[];
this.categoryTitle = option.title;
this.categoryId = option.id;
if(option.children){
this.childCategories = option.children;
}
axios.get(`/categories/${this.categoryId}`)//getting the category questions
.then(({data}) => {
this.questions = data;
this.questionsArray.push(...data.data);
this.nextUrl = data.next_page_url;
this.dataReady = true;
this.emptyCheck(this.questionsArray);
})
.catch((err) => {
swal("Failed!", err.response.data.message, "info");
this.$router.push('dashboard') ;
})
}
$refs of the select divs always return undefined unless I used setTimeout.
Code for approach 2:
After including the mixins file in both components I put the following code in mixins:
setCategory (questionCategory) {
console.log("TCL: setCategory -> questionCategory", questionCategory)
this.category = questionCategory;
console.log("TCL: setCategory -> this.category", this.category)
}
,
getCategory () {
return this.category ;
}
The value of the object received by the set method is correct but after updating the mixins data method this.category is returning the following only:
__ob__: Object { value: {…}, dep: {…}, vmCount: 0 }
i.e. without the category object details. I tried to stringify the object and then call it, this.category shows an empty variable.
3- Using keep-alive, however it does not work, I tried to wrap both the router-view and router-link.
<keep-alive include="questions-list">
<router-link to="/questions-list" class="nav-link">
<i class="nav-icon fas fa-question-circle orange"></i>
<p>
Questions
</p>
</router-link>
</keep-alive>
I even tried using include the name of the questions list component with no result.
Sorry for the long question but I have been stuck for a while with this and it is the core of my application.
I am now using keep-alive approach. The silly mistake was that I only named the component when I declared my routes.
{ path: '/questions-list', name:"questions-list", component: require('./components/qa/Questions.vue')}
For those who are facing the same problem, you should declare the name of the component inside of the component export object like the following:
export default {
name: 'questions-list',
data () {
return {
}
}

How to reset form elements with VueJS and Vuex

I have a "Question and Answer" component written in VueJs, with a Vuex store. Each answer is a <textarea> element, such as the following:
<textarea class="form-control" rows="1" data-answer="1" :value="answer(1)" #change="storeChange"></textarea>
As you can see the value of the control is set by calling an answer() method and passing the question number as a parameter.
When the answer is changed the storeChange method is called and the changes are cached in a temporary object (this.changes) per the following code:
props : [
'questionnaire'
],
methods : {
answer(number) {
if (this.questionnaire.question_responses &&
(number in this.questionnaire.question_responses)) {
return this.questionnaire.question_responses[number];
}
return null;
},
storeChange(e) {
Vue.set(this.changes, e.target.dataset.answer, e.target.value);
},
save() {
// removed for clarity
},
reset() {
// what to do here?
},
}
If the user clicks the save button I dispatch an action to update the store.
If the user wants to reset the form to its original state, I need to clear this.changes, which is no problem, but I also need to 'refresh' the values from the store. How do I do this?
Note that the source of the initial state, questionnaire, comes via a prop, not a computed property that maps directly to the store. The reason for this is that there can be multiple "Question and Answer" components on one page, and I found it easier to pass the state this way.
we can by using refs reset form , example
form textarea
<form ref="textareaform">
<textarea
class="form-control"
rows="1"
data-answer="1"
:value="answer(1)"
#change="storeChange"
>
</textarea>
<button #click="reset">reset</button>
</form>
reset
reset() {
// ref='textareaform'
// reset() method resets the values of all elements in a form
// document.getElementById("form").reset();
this.$refs.textareaform.reset()
},

Multiple instances of a vue.js component and a hidden input file

I am experiencing a weird behavior using Vue.js 2.
I have a component that I reference twice in a single html page.
This component contains an input file control called attachment_file. I hide it using the Bootstrap class hidden and I open the file selection using another button. When a file is selected, I put in a variable called attachment_filename a certain string just like so:
<template>
<div>
<button #click="selectAttachement"><span class='glyphicon glyphicon-upload'></span></button>
<input id="attachment_file" type="file" class="hidden" #change="attachmentSelected">
{{attachment_filename}}
</div>
</template>
<script>
export default {
data () {
return: {
attachment_filename: null,
}
},
methods: {
selectAttachement () {
$('#attachment_file').click();
},
attachmentSelected () {
this.attachment_filename = 'some file here';
},
}
}
</script>
Problem With the class hidden and when a file is selected from the 2nd instance of the component, the value of this.attachment_filename is updated but in the data of 1st instance of the component!
If I remove the class hidden, it updates the value in the correct instance.
Possible solution use css opacity or width instead of the class hidden.
But is there a reason for this behavior?
Not sure why it is not working specifically with .hidden, but you have an inherent problem in the code that i think is the cause of the problem.
You are selecting the input using jquery with an id, that creates a couple of problems:
When you use the component twice or more, all these inputs generated by these components will have the same id, which is not what you want since id should be unique
Even if you change it to a class instead of id, it won't work properly since you are selecting the element using jquery, and that will select all the elements with this class, while you want to select just the input in that component.
The solution is to use refs:
<input id="attachment_file" type="file" class="hidden" #change="attachmentSelected" ref="fileInput">
selectAttachement () {
$(this.$refs.fileInput).click();
},