Vuejs v-model binding with a select box inside a slot - vue.js

Good afternoon,
I seem to have occured an issue with v-model binding in scoped slots.
I've tried to create a universal API form that would allow me to hook any URL to it, add any amount and any type of DOM elements inside a scoped slot and use data fetched from API accordingly.
So far I've managed to do the first part - fetch data and pass it to elements inside; the issue I'm having now is thanks to one-way data flow - selecting an option inside of <select></select> doesn't seem to update selectId accordingly - and I get it, that's the pattern it follows... now, how do I work around it?
edit:
If I copy this select directly into the component (without passing via scoped slot) and replce props.* with just * (props.selectedId to selectedId) - it'll work flawlessly.
It only doesn't work because props are one-way.
<template>
<form :action="action" :method="method">
<slot :selectedId="selectedId" :results="results"></slot>
</form>
</template>
<script>
export default
{
props: ['action', 'method', 'url'],
data: () =>
({
results: [],
selectedId: 0,
}),
created()
{
setTimeout(() =>
{
axios.get(this.url).then(response => (this.results = response.data))
}, 500)
},
}
</script>
and HTML:
<api-form action="/blog" method="POST" url="/api/v1/blog">
<template slot-scope="props">
<select class="form-control mb-3" v-model="props.selectedId">
<option v-for="entry, i in props.results" :value="entry">#{{ entry.title }}</option>
</select>
<button class="btn btn-danger">Delete</button>
</template>
</api-form>

Gosh, I keep posting here and finding an answer afterwards.
4 hours of googling - nothing, then I post here and suddenly come up with a solution.
For anyone having the same issue, this is caused by the fact that all non-components, if you apply $emit to it, it'll be called from <Root>, meaning you have to edit your:
created()
{
this.$on('update:selectedId', (value) =>
{
this.selectedId = value
})
},
And change it to this:
created()
{
this.$root.$on('update:selectedId', (value) =>
{
this.selectedId = value
})
},

You can pass a callback as a slot prop that will modify the data being passed also as a slot prop, here's an answer of mine on a similar question v-model and scoped slots not working?

Related

Then I reload page v-if(with computed data) not work

im search on everywhere information about my questions - not help me.
I would like to solve this issue using v-if (not v-show)
I get orders data from Vuex store
all code work ,then page loaded and mounted and then I click other tabs and going back to current tab
code not work then I reload page on current tab
code work then I reload page on current tab , but without all v-if
I would like to solve without using third-party plugins if possible
<template>
<div class="w-full">
<!--show message div-->
<div v-if="orders.length===0" class="flex flex-wrap">
...
</div>
<!--show table div-->
<div v-if="orders.length!==0" class="flex flex-wrap">
...
</div>
</div>
</template>
computed data
computed: {
orders() {
return this.$store.state.dataList.orders
}
}
// in console.log i get 4 my orders objects (4) [{…}, {…}, {…}, {…}, ob: Observer]
actions:
fetchORDERS ({ commit }) {
return new Promise((resolve, reject) => {
axios.get('/api/data-list/orders')
.then((response) => {
commit('SET_ORDERS', response.data)
resolve(response)
})
.catch((error) => { reject(error) })
})
}
mutation:
SET_ORDERS (state, order) {
state. orders = order
}
one of the forums they wrote that the matter is in mutation, but I cannot understand what exactly and where is the error, pls help
Your basic setup is correct. That v-if should work if the data-flow is correct.
From what you're giving us, I see the following:
In your computed property you're looking at: this.$store.state.dataList.orders
In your mutation you're changing state. orders. This should be state.dataList.orders.
In other words: you should check whether the state has the shape you expect it to have. You can do this with a console.log in the computed property, or one in the mutation.

Use moment().fromNow() with a vue component in array

How can I use Momentjs to format a date that is inside my vue component? The moment().fromNow() function works fine when I manually put in a date, but I want to use the date that is fetched from my API.
This is currently what it looks like, I put the part where I need help with in *.
Vue.js
<div class="post d-flex flex-row" v-for="(post, i) in Post" :key="i">
<h6 class="card-subtitle mb-2 text-muted">Posted **{{moment(datePosted).fromNow()}}** by {{post.user}}</h6>
</div>
data() {
return{
Post: []
}
},
async created() {
try{
const res = await axios.get(url)
this.Post = res.data;
} catch(err){
console.log(err)
}
}
Variable datePosted seems undefined. The format function should be something like {{ moment(post.datePosted).fromNow() }}. Also don't use variables which differ just by the case: post and Post.
In my opinion, you could map res.data inside created and append attribute with formatted data to the object.
E.g:
this.Post = res.data.map(post => ({
...post,
datePost: moment(post.date).fromNow()
})
Obviously, I cannot know if post.date is proper attribute. You have to type there the proper one. Also make sure you imported moment js module.
Then in the template, you could use:
{{ post.datePost }}

Vue.js this.$refs empty due to v-if

I have a simple Vue component that displays an address, but converts into a form to edit the address if the user clicks a button. The address field is an autocomplete using Google Maps API. Because the field is hidden (actually nonexistent) half the time, I have to re-instantiate the autocomplete each time the field is shown.
<template>
<div>
<div v-if="editing">
<div><input ref="autocomplete" v-model="address"></div>
<button #click="save">Save</button>
</div>
<div v-else>
<p>{{ address }}</p>
<button #click="edit">Edit</button>
</div>
</div>
</template>
<script>
export default {
data() {
editing: false,
address: ""
},
methods: {
edit() {
this.editing = true;
this.initAutocomplete();
},
save() {
this.editing = false;
}
initAutocomplete() {
this.autocomplete = new google.maps.places.Autocomplete(this.$refs.autocomplete, {});
}
},
mounted() {
this.initAutocomplete();
}
}
I was getting errors that the autocomplete reference was not a valid HTMLInputElement, and when I did console.log(this.$refs) it only produced {} even though the input field was clearly present on screen. I then realized it was trying to reference a nonexistent field, so I then tried to confine the autocomplete init to only when the input field should be visible via v-if. Even with this, initAutocomplete() is still giving errors trying to reference a nonexistent field.
How can I ensure that the reference exists first?
Maybe a solution would be to use $nextTick which will wait for your DOM to rerender.
So your code would look like :
edit() {
this.editing = true;
this.$nextTick(() => { this.initAutocomplete(); });
},
Moreover if you try to use your this.initAutocomplete(); during mounting it cannot work since the $refs.autocomplete is not existing yet but I'm not sure you need it since your v-model is already empty.
I think it's because your "refs" is plural
<input refs="autocomplete" v-model="address">
It should be:
<input ref="autocomplete" v-model="address">

Push not updating array in DOM Vue

I am using Vue and am trying to make live search. But on updating the content of search, it doesn't get updated.
Data do get update in array, when checked in dev tools. But DOM don't get updated.
template
<div class="dropdown">
<input type="text" v-model="input" placeholder="Search" #keyup="searching" data-toggle="dropdown">
<span class="caret"></span>
<ul class="dropdown-menu">
<li v-for="(data,index) in availSearchData" :key="index">
{{data.name}}
</li>
</ul>
</div>
method
searching() {
if (this.input) {
let url = this.domain + "search";
axios
.get(url, {
params: {
table: this.table,
data: this.input
}
})
.then(res => {
this.availSearchData = [];
res.data.forEach(doc => {
this.availSearchData.push(doc);
});
});
}
}
I don't know where I am doing wrong.
Please help out if possible.
To add an item to the back of an array and get it to be reactive in Vue, below is what worked for me:
this.$set(this.items,
this.items.length,
JSON.parse(JSON.stringify(this.item))
);
The this.$set is Vue's inbuilt array manipulation function that guarantees reactivity.
The this.items is the array, this.items.length (NOTE: it is items.length NOT items.length - 1) is to push a new index to the back of the array and finally, JSON.parse(JSON.stringify(this.item)) is to clone the this.item into a new object before pushing into the array. The cloning part may not be applicable to you and I used this in variables because all the variables are declared in my data() function.
Use a computed property in your component and use that for parsing the template like this
<li v-for="(data,index) in availSearch" :key="index">
{{data.name}}
</li>
and computed property will be then
availSearch() {
return this.availSearchData;
},
so this computed property always return the array if it is updated.
Also if your response is the array that you want to use exactly, try this
searching() {
if (this.input) {
let url = this.domain + "search";
axios
.get(url, {
params: {
table: this.table,
data: this.input
}
})
.then(res => {
this.availSearchData = [];
Vue.set(this, 'availSearchData', res.data);
});
}
}
Possible explanations for this might be:
You don't declare the property in the component and thus normal
reactivity doesn't work.
You are using index as the key in your array. This might confuse the
reactivity system, so it does not necessarily know if the item
changed. Try using the name of the item as the key instead.
Try calling your function from mounted hook. I think the problem is that you are trying to show data when the DOM is not rendered yet. By calling your function in mounted you get data back after DOM has been rendered.
mounted() {
this.searching();
}
from Vue website "mounted: Called after the instance has been mounted, where el is replaced by the newly created vm.$el. If the root instance is mounted to an in-document element, vm.$el will also be in-document when mounted is called."

Interpolation doesn't rerender in parent component after custom event from child

I have select displayed in v-for loop:
<div v-for="(n, key) in selectedLanguages">
<select class="input input__col"
v-model="currentLang[key]"
#change="changeLanguage(currentLang[key], key)"
id="lang_select">
<option value="pl">Polski</option>
<option value="en">Angielski</option>
<option value="es">Hiszpański</option>
</select>
</div>
To each select I'm adding changeLanguage method which is:
<script>
export default {
data() {
return {
currentLang: []
}
},
methods: {
changeLanguage(value, key) {
let data = { value, key };
this.$nuxt.$emit('change::language', data);
}
},
props: ['selectedLanguages']
}
</script>
and it is in child component. In parent I'm listening for this change::language event:
this.$nuxt.$on('change::language', res => {
console.log(res);
this.selectedLanguages[res.key] = res.value;
console.log(this.selectedLanguages);
Although it's working correctly and it's updating selectedLanguages array just fine it doesn't rerender interpolation {{ selectedLanguages }} in parent. However it's correctly rerendering interpolation {{ selectedLanguages }} in child where it's passed by props. Why?
It seems like vue doesn't "catch" that selectedLanguages array have been changed. It only see when I .push or .pop this array. Is there something like apply method in vue?
I found this link in documentation: https://v2.vuejs.org/v2/guide/list.html#Caveats and added this.$set(this.selectedLanguages, res.value, res.key); in parent below my assignment but it didn't fix.
I found solution in Vue docs:
Due to limitations in JavaScript, Vue cannot detect the following changes to an array:
When you directly set an item with the index, e.g. vm.items[indexOfItem] = newValue
I was doing exacly like above.
The solution is instead
this.selectedLanguages[res.key] = res.value;
use
this.$set(this.selectedLanguages, res.key, res.value);
which is basically a bit weird but it works.