Render named scopedSlot programmatically - vue.js

I want to move the following template into the render function of my component, but I don't understand how.
This is my template:
<template>
<div>
<slot name="item" v-for="item in filteredItems" :item="item">
{{ item.title }}
</slot>
</div>
</template>
This is my component:
export default {
props: {
items: {
type: Array,
required: true,
},
search: {
type: String,
default: ""
}
},
methods: {
filterByTitle(item) {
if (!("title" in item)) { return false; }
return item.title.includes(this.search);
}
},
computed: {
filteredItems() {
if (this.search.length === "") {
return this.items;
}
return this.items.filter(this.filterByTitle);
}
},
render: function(h) {
// How can I transform the template so that it finds its place here?
return h('div', ...);
}
};
I thank you in advance.

To render scoped slots you can use $scopedSlots. See more here.
Example Code:
...
render(h) {
return h(
'div',
this.filteredItems.map(item => {
let slot = this.$scopedSlots[item.title]
return slot ? slot(item) : item.title
})
)
}
...
JSFiddle

Related

Vue v-model data is from ajax undefined value

I used the vue 2. I had a data from ajax, this is my code example:
<template>
<div>
<input type="input" class="form-control" v-model="siteInfo.siteId">
<input type="input" class="form-control" v-model="siteInfo.info.name">
<input type="input" class="form-control" v-model="siteInfo.accountData.name">
</div>
</template>
<script>
export default {
name: 'Site',
data() {
return {
siteInfo: {},
/* siteInfoName: '', */
}
},
/*computed: {
siteInfoName: function() {
return siteInfo.info.name || '';
},
...
},*/
methods: {
getData() {
// do ajax get data
this.$http.post('URL', {POSTDATA}).then(response => {
/*
response example
{ body:
data: {
sitdeId: 1,
info: { name: 'test'},
accountData: { name: 'accountTest'},
}
}
*/
this.siteInfo = response.body.data;
})
}
},
mounted() {
this.getData();
}
}
</script>
I got a warring message
[Vue warn]: Error in render: "TypeError: Cannot read property 'name'
of undefined"
I can use computed to fix it, but if I had a lot model, I should
write a lot computed.
I should create a lot data for those model?
I should not use an object to bind a lot model?
Does it have another solution for this situation? Thanks your help.
Before the data loads siteInfo.info will be undefined, so you can't access name in the v-model:
v-model="siteInfo.info.name"
Likewise for siteInfo.accountData.name.
My suggestion would be to set the initial value of siteInfo to null and then put a v-if="siteInfo" on the main div. Alternatively you could put a v-if on the individual input elements that checks for siteInfo.info and siteInfo.accountData.
You may also want to consider showing alternative content, such as a load mask, while the data is loading.
Don't be worried about too many v-models - you can do an iteration on the Object - like with Object.entries().
Vue.component('list-input-element', {
props: ['siteLabel', 'siteInfo'],
template: '<div><label>{{siteLabel}}<input type="input" class="form-control" v-model="siteInfo"></label></div>'
})
new Vue({
name: 'Site',
el: '#app',
data() {
return {
siteInfo: {},
}
},
methods: {
getData() {
// using mockup data for this example
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(json => {
console.log(json)
this.siteInfo = json
})
// do ajax get data
/*this.$http.post('URL', {
POSTDATA
}).then(response => {
this.siteInfo = response.body.data;
})*/
}
},
mounted() {
this.getData();
}
})
div {
display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<list-input-element v-for="siteInfo in Object.entries(siteInfo)" :site-label="siteInfo[0]" :site-info="siteInfo[1]" />
</div>
Rounding up
So, when you do the single file template, use a computed value, and return an Object from that.
Base your v-for on that computed, and you'll have no problems.
Something like this:
<template>
<div>
<input type="input" class="form-control" v-for="infoEl in siteInfoComputed" v-model="infoEl">
</div>
</template>
<script>
export default {
name: 'Site',
data() {
return {
siteInfo: {},
}
},
computed: {
siteInfoComputed: function() {
// you could check for all the keys-values you want here, and handle
// 'undefined' problem here
// so, actually you "create" the Object here that you're going to use
let ret = {}
// checking if this.siteInfo exists
if (Object.keys(this.siteInfo).length) ret = this.siteInfo
return ret
},
},
methods: {
getData() {
// do ajax get data
this.$http.post('URL', {POSTDATA}).then(response => {
/*
response example
{ body:
data: {
sitdeId: 1,
info: { name: 'test'},
accountData: { name: 'accountTest'},
}
}
*/
this.siteInfo = response.body.data;
})
}
},
mounted() {
this.getData();
}
}
</script>

Vue: Distinct method in foreach component

I have a child component in foreach loop. The component has two methods what I initiate with a button on the component.
How can I make these methods unique/ every foreach iteration? Becouse if I don't make them distinct even if I push the last iteration's button the first iteration's method will start.
I tried with methodName+index: function(), but I got , expected error.
Update
My parent component:
<div v-for="(card, index) in cards" v-bind:key="index">
<CardSubComponent
:card="card"
#cardSaveSuccess="cardSuccess"
#cardSaveError="cardError"
></CardSubComponent>
</div>
My subcomponent:
<template>
<div class="box">
<b-select :id="cardSequenceID" v-model="card.sequence" #input="changeSequence">
<option
v-for="sequence in sequenceArray"
:value="sequence"
:key="sequence">
{{ sequence }}
</option>
</b-select>
</div>
</template>
<script>
export default {
props:['card'],
data(){
return {
cardSequenceID = 'sequence'+card.id
sequenceArray: [1,2,3,4,5,6,7,8,9,10],
}
},
methods: {
changeSequence(rule){
axios.post('/api/card/changeSequence', {
cardID: card.id,
weight: document.getElementById('sequence'+cardID).value
},
{
headers: {
Authorization: 'Bearer ' + localStorage.getItem('token')
}
}).then(response => {
if(response.data == 'success'){
this.$emit('cardSaveSuccess')
} else {
this.$emit('cardSaveError')
}
});
}
}
}
</script>
It sounds like you need to add arguments to your method calls. Something like this:
<template>
<div>
<CardSubComponent
v-for="(card, index) in cards"
:key="index"
:card="card"
#cardSaveError="cardError(card)"
#cardSaveSuccess="cardSuccess(card)"
/>
</div>
</template>
export default {
methods: {
cardError(card) {
// TODO: Add error handler here
console.log('error called with:', card.id);
},
cardSuccess(card) {
// TODO: Add success handler here
console.log('success called with:', card.id);
},
},
};
In the child component there are two things wrong:
cardSequenceID was not properly initialized (you were using = instead of :)
You were missing this in this.card.id to identify the id for your axios call
export default {
props: ['card'],
data() {
return {
sequenceArray: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
cardSequenceID: `sequence${card.id}`,
};
},
methods: {
changeSequence(rule) {
axios
.post(
'/api/card/changeSequence',
{
cardID: this.card.id,
weight: document.getElementById(`sequence${cardID}`).value,
},
{
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
}
)
.then(response => {
if (response.data === 'success') {
this.$emit('cardSaveSuccess');
} else {
this.$emit('cardSaveError');
}
});
},
},
};

Vue.js - data value does not set from prop

What do I have: two components, parent and child.
Parent
<UserName :name=user.name></UserName>
...
components: {UserName},
data() {
return {
user: {
name: '',
...
}
}
},
created() {
this.fetchUser()
console.log(this.user) //<- object as it is expected
},
methods: {
fetchUser() {
let that = this
axios.get(//...)
.then(response => {
for (let key in response.data) {
that.user[key] = response.data[key]
}
})
console.log(that.user) //<- object as it is expected
}
}
Child
<h3 v-if="!editing" #click="edit">{{ usersName }}</h3>
<div v-if="editing">
<div>
<input type="text" v-model="usersName">
</div>
</div>
...
props: {
name: {
type: String,
default: ''
},
},
data() {
return {
editing: false,
usersName: this.name,
...
}
},
Problem: even when name prop is set at child, usersName data value is empty. I've inspected Vue debug extension - same problem.
What have I tried so far (nothing helped):
1) props: ['name']
2)
props: {
name: {
type: String
},
},
3) usersName: JSON.parse(JSON.stringify(this.name))
4) <UserName :name="this.user.name"></UserName>
P. S. when I pass static value from parent to child
<UserName :name="'just a string'"></UserName>
usersName is set correctly.
I've also tried to change name prop to some foobar. I guessed name might conflict with component name exactly. But it also didn't helped.
user.name is initially empty, and later gets a value from an axios call. usersName is initialized from the prop when it is created. The value it gets is the initial, empty value. When user.name changes, that doesn't affect the already-initialized data item in the child.
You might want to use the .sync modifier along with a settable computed, or you might want to put in a watch to propagate changes from the prop into the child. Which behavior you want is not clear.
Here's an example using .sync
new Vue({
el: '#app',
data: {
user: {
name: ''
}
},
methods: {
fetchUser() {
setTimeout(() => {
this.user.name = 'Slartibartfast'
}, 800);
}
},
created() {
this.fetchUser();
},
components: {
userName: {
template: '#user-name-template',
props: {
name: {
type: String,
default: ''
}
},
computed: {
usersName: {
get() { return this.name; },
set(value) { this.$emit('update:name', value); }
}
},
data() {
return {
editing: false
}
}
}
}
});
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
{{user.name}}
<user-name :name.sync=user.name></user-name>
</div>
<template id="user-name-template">
<div>
<input type="text" v-model="usersName">
</div>
</template>
should be passed like this...
<UserName :name="user.name"></UserName>
if data property is still not being set, in mounted hook you could set the name property.
mounted() {
this.usersName = this.name
}
if this doesn't work then your prop is not being passed correctly.
sidenote: I typically console.log within the mounted hook to test such things.

Vue computed property not recomputing as I would have expected

I'm trying to create a generic search component with an event that gets emitted with the search string back to the parent so the parent can actually filter the results.
With the code below why does computed.filteredDocuments not recompute when the value of this.searchCriteria changes and how can I tweak my code so that it does recompute when updatedSearchString is called?
Parent component
<template>
<search :searchCriteria="searchCriteria" #searchString="updatedSearchString" />
<div v-for="(doc, index) in filteredDocuments" v-bind:key="index">
<div>{{doc.filename}}</div>
</div>
</template>
<script>
import store from '../store/index'
import { mapState } from 'vuex'
// import _ from 'lodash'
import Search from '../components/search'
export default {
name: 'Parent',
components: {
Search: Search
},
data () {
return {
searchCriteria: ''
}
},
computed: {
...mapState({
documents: state => state.documents.items
}),
filteredDocuments () {
console.log('in computed')
return _(this.documents)
.filter(this.applySearchFilter)
.value()
}
},
methods: {
updatedSearchString (searchString) {
this.searchCriteria = searchString <-- I WOULD HAVE EXPECTED BY UPDATING THIS IT WOULD TRIGGER COMPUTED.FILTEREDDOCUMENTS TO RECOMPUTE
}
},
applySearchFilter (doc) {
console.log('in applySearchFilter')
// If no search criteria return everything
if (this.searchCriteria === null) {
return true
}
if (doc.filename.toLowerCase().includes(this.searchCriteria.toLowerCase())) {
return true
}
return false
}
}
</script>
Child component
<template>
<div>
<q-search v-model="search" placeholder="Search" />
</div>
</template>
<script>
export default {
name: 'Search',
props: {
searchCriteria: { type: String, required: true }
},
data () {
return {
search: null
}
},
mounted () {
this.search = this.searchCriteria // Clone
},
watch: {
search: function (newVal, oldVal) {
// If no search criteria return everything
if (!newVal) {
this.clearSearch()
}
this.$emit('searchString', newVal) <-- THIS EMITS THE SEARCH VALUE TO THE PARENT
}
}
}
</script>
What I ran into turning your example code into a snippet was some of the camel case/kebab case issue: HTML is case-insensitive, so the event should be kebab-case, not camelCase. This example filters as expected.
new Vue({
el: '#app',
data: {
searchCriteria: '',
documents: [{
filename: 'FirstFile'
},
{
filename: 'SecondFile'
},
{
filename: 'LastOne'
}
]
},
methods: {
updatedSearchString(searchString) {
this.searchCriteria = searchString
},
applySearchFilter(doc) {
console.log('in applySearchFilter')
// If no search criteria return everything
if (this.searchCriteria === null) {
return true
}
if (doc.filename.toLowerCase().includes(this.searchCriteria.toLowerCase())) {
return true
}
return false
}
},
computed: {
filteredDocuments() {
console.log('in computed')
return this.documents
.filter(this.applySearchFilter)
}
},
components: {
search: {
template: '#child-template',
props: {
searchCriteria: {
type: String,
required: true
}
},
computed: {
search: {
get() {
return this.searchCriteria;
},
set(value) {
this.$emit('search-string', value);
}
}
}
}
}
});
<link href="https://unpkg.com/quasar-extras#latest/material-icons/material-icons.css" rel="stylesheet"/>
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<link rel="stylesheet" type="text/css" href="https://unpkg.com/quasar-framework#latest/dist/umd/quasar.mat.min.css">
<script src="https://unpkg.com/quasar-framework#latest/dist/umd/quasar.mat.umd.min.js"></script>
<div id="app">
<search :search-criteria="searchCriteria" #search-string="updatedSearchString"></search>
<div v-for="(doc, index) in filteredDocuments" v-bind:key="index">
<div>{{doc.filename}}</div>
</div>
</div>
<template id="child-template">
<div>
<q-search v-model="search" placeholder="Search" />
</div>
</template>

Updating a prop inside a child component so it updates on the parent container too

So I have a simple template like so:
<resume-index>
<div v-for="resume in resumes">
<resume-update inline-template :resume.sync="resume" v-cloak>
//...my forms etc
<resume-update>
</div>
<resume-index>
Now, inside the resume-updatecomponent I am trying to update the prop on the inside so on the outside it doesn't get overwritten, my code is like so;
import Multiselect from "vue-multiselect";
import __ from 'lodash';
export default {
name: 'resume-update',
props: ['resume'],
components: {
Multiselect
},
data: () => ({
form: {
name: '',
level: '',
salary: '',
experience: '',
education: [],
employment: []
},
submitted: {
form: false,
destroy: false,
restore: false
},
errors: []
}),
methods: {
update(e) {
this.submitted.form = true;
axios.put(e.target.action, this.form).then(response => {
this.resume = response.data.data
this.submitted.form = false;
}).catch(error => {
if (error.response) {
this.errors = error.response.data.errors;
}
this.submitted.form = false;
});
},
destroy() {
this.submitted.destroy = true;
axios.delete(this.resume.routes.destroy).then(response => {
this.resume = response.data.data;
this.submitted.destroy = false;
}).catch(error => {
this.submitted.destroy = false;
})
},
restore() {
this.submitted.restore = true;
axios.post(this.resume.routes.restore).then(response => {
this.resume = response.data.data;
this.submitted.restore = false;
}).catch(error => {
this.submitted.restore = false;
})
},
reset() {
for (const prop of Object.getOwnPropertyNames(this.form)) {
delete this.form[prop];
}
}
},
watch: {
resume: function() {
this.form = this.resume;
},
},
created() {
this.form = __.cloneDeep(this.resume);
}
}
When I submit the form and update the this.resume I get the following:
[Vue warn]: Avoid mutating a prop directly since the value will be
overwritten whenever the parent component re-renders. Instead, use a
data or computed property based on the prop's value. Prop being
mutated: "resume"
I have tried adding computed to my file, but that didn't seem to work:
computed: {
resume: function() {
return this.resume
}
}
So, how can I go about updating the prop?
One solution:
simulate v-model
As Vue Guide said:
v-model is essentially syntax sugar for updating data on user input
events, plus special care for some edge cases.
The syntax sugar will be like:
the directive=v-model will bind value, then listen input event to make change like v-bind:value="val" v-on:input="val = $event.target.value"
So the steps:
create one prop = value which you'd like to sync to parent component
inside the child component, create one data porperty=internalValue, then uses Watcher to sync latest prop=value to data property=intervalValue
if intervalValue change, emit one input event to notice parent component
Below is one simple demo:
Vue.config.productionTip = false
Vue.component('container', {
template: `<div>
<p><button #click="changeData()">{{value}}</button></p>
</div>`,
data() {
return {
internalValue: ''
}
},
props: ['value'],
mounted: function () {
this.internalValue = this.value
},
watch: {
value: function (newVal) {
this.internalValue = newVal
}
},
methods: {
changeData: function () {
this.internalValue += '#'
this.$emit('input', this.internalValue)
}
}
})
new Vue({
el: '#app',
data () {
return {
items: ['a', 'b', 'c']
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<div>
<p>{{items}}
<container v-for="(item, index) in items" :key="index" v-model="items[index]">
</container>
</div>
</div>
or use other prop name instead of value (below demo use prop name=item):
Also you can use other event name instead of event name=input.
other steps are similar, but you have to $on the event then implement you own handler like below demo.
Vue.config.productionTip = false
Vue.component('container', {
template: `<div>
<p><button #click="changeData()">{{item}}</button></p>
</div>`,
data() {
return {
internalValue: ''
}
},
props: ['item'],
mounted: function () {
this.internalValue = this.item
},
watch: {
item: function (newVal) {
this.internalValue = newVal
}
},
methods: {
changeData: function () {
this.internalValue += '#'
this.$emit('input', this.internalValue)
this.$emit('test-input', this.internalValue)
}
}
})
new Vue({
el: '#app',
data () {
return {
items: ['a', 'b', 'c']
}
},
methods: {
syncChanged: function (target, index, newData) {
this.$set(target, index, newData)
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<div>
Event Name=input
<p>{{items}}</p>
<container v-for="(item, index) in items" :key="index" :item="item" #input="syncChanged(items, index,$event)">
</container>
</div>
<hr> Event Name=test-input
<container v-for="(item, index) in items" :key="index" :item="item" #test-input="syncChanged(items, index,$event)">
</container>
</div>
I usually use vuex to manage variables that I will be using in multiple components and like the error says, load them in the various components using the computed properties. Then use the mutations property of the store object to handle changes
In component files
computed: {
newProfile: {
get() {
return this.$store.state.newProfile;
},
set(value) {
this.$store.commit('updateNewProfile', value);
}
},
In the vuex store
state: {
newProfile: {
Name: '',
Website: '',
LoginId: -1,
AccountId: ''
}
},
mutations: {
updateNewProfile(state, profile) {
state.newProfile = profile;
}
}