How can I duplicate slots within a Vuejs render function? - vue.js

I have a component which is passed content via a slot. I'm using a render function to output the content. The reason I'm using a render function is because I want to duplicate the content multiple times. When I use this code, everything works fine:
render(createElement){
return createElement('div', {}, this.$slots.default);
}
When I data that is being passed changes, the output changes as well.
However, since I want to duplicate the slot content, I'm now trying this:
return createElement(
'div', {},
[
createElement('div', { }, this.$slots.default),
createElement('div', { }, this.$slots.default)
]
)
Now the problem is, when the slot content changes from outside the component, only the content in the second div gets updated, the content in the first div stays the same..
Am I missing something here?

I can't explain why it happens. But the doc does mention that "VNodes Must Be Unique" in a render function. See https://v2.vuejs.org/v2/guide/render-function.html#Constraints.
Anyway, this is a VNode cloning function, which works, which I discovered from https://jingsam.github.io/2017/03/08/vnode-deep-clone.html.
function deepClone(vnodes, createElement) {
function cloneVNode(vnode) {
const clonedChildren = vnode.children && vnode
.children
.map(vnode => cloneVNode(vnode));
const cloned = createElement(vnode.tag, vnode.data, clonedChildren);
cloned.text = vnode.text;
cloned.isComment = vnode.isComment;
cloned.componentOptions = vnode.componentOptions;
cloned.elm = vnode.elm;
cloned.context = vnode.context;
cloned.ns = vnode.ns;
cloned.isStatic = vnode.isStatic;
cloned.key = vnode.key;
return cloned;
}
const clonedVNodes = vnodes.map(vnode => cloneVNode(vnode))
return clonedVNodes;
}
How to use it:
render(createElement) {
return createElement('div', {}, [
createElement('div', {}, this.$slots.default),
createElement('div', {}, [...deepClone(this.$slots.default, createElement)])
])
}
Demo: https://jsfiddle.net/jacobgoh101/bz3e0o5m/

I found this SO question searching for a way to render the content of a slot multiple times like e.g. for a generic list that can have a template for the content of a list row, which is used for each item.
As of 2020 (in fact earlier) multiple rendering of a slot can be achieved using scoped slots. This is documented here:
https://v2.vuejs.org/v2/guide/components-slots.html#Other-Examples
The documentation says:
Slot props allow us to turn slots into reusable templates that can render different content based on input props
(obviously, if we can use the template to render different content based on props, we can also use it to render the same content)
The example given right there uses a template instead of a render function, but how to use scoped slots in a render function is fortunately also documented:
https://v2.vuejs.org/v2/guide/render-function.html#Slots

Related

VueJS Use VNode inside render function with createElement?

I am trying to render a Vnode inside my render function. In such a way that I can still give that element children.
I know you can have an array of Vnodes as the third argument of createElement(tag, data, vnode[]) but since I want to give this specific Vnode children still I seem to be a little stuck,
I have tried doing something like:
const vnodeObj = {tag: vnode.tag, data: vnode.data}
//Skip to inside render function
createElement(vnodeObj.tag, vnodeObj.data, []).
Which worked well to create the tag but I have found that the data object returend from Vnode.data is not the same for the data object createElement expect for example:
If I were to add a class "test" and a ref "test2" to a element using create element the data object would look like so:
{
attrs: { class: "test", ref: "test2"},
}
while the Vnode.data would return something like:
{
"ref":"test2",
"staticClass":"test"
}
Leading me to believe there must be a better way to render a single Vnode and its data and continue giving it children with createElement.

How to automatically construct watch property based on data attributes in Vue.js?

I have standard Vue.js component and I'd like to convert attributes in data property to watcher or in other words I want to construct a watch object based on data property attributes automatically
my idea looks something like this
watch: {
...(() => {
const watchers = {}
Object.keys(this.$data).forEach(key => {
watchers[key] = () => {
new ProductNutrientUpdate(this).run()
}
})
return watchers
})(),
},
the problem with this approach is that this.$data is not constructed yet
maybe there is some way how I can add watchers in created hook for example??
Vue already watches properties of the data object (note if any of these values are themselves objects, I think you need to update the whole object, i.e. change its value to a shallow copy with the desired nested key-values).
Refer to: https://v2.vuejs.org/v2/guide/reactivity.html
You can then use the update lifecycle hook to watch for all changes to data: https://v2.vuejs.org/v2/api/#updated
I was able to resolve a challenge using the following approach
created() {
Object.keys(this.$data).forEach(key => {
this.$watch(key, function() {
// ... some logic to trigger on attribute change
})
})
}

Vuejs: How to trigger render function?

I currently have a component with a render function, to which I send data using a slot.
In short my component looks as follows:
export default {
render(createElement){
return createElement(
'div', {
'class' : 'className'
},
this.$slots.default
)
}
}
There is a bit more inside the render function that creates multiple elements and puts the slot content in each of the element (which is the reason I'm using a render function), but that's not relevant for this example.
I have another component which has this template:
<Component1>
<div>foo</div>
</Component1>
(component 1 being the component with the render function).
This all works nicely, but the problem is that when the word 'foo' changes, the component doesn't get updated. I can send a prop to the component to check wether the content gets changed (by putting a watcher on the prop), but how can I force the component to run the render function again?
Thanks!

VueJS2: Update data pattern?

So, in one of my VueJS templates, I have a left sidebar that generates buttons by iterating (v-for) through a multidimensional items array.
When one of these buttons is clicked, a method is run:
this.active.notes = item.notes
active.notes is bound to a textarea in the right content section.
So, every time you click one of the item buttons, you see the (active) notes associated with that item.
I want to be able to have the user edit the active notes in the textarea. I have an AJAX call on textarea blur which updates the db. But the problem is, the items data hasn't changed. So if I click a different item, then click back to the edited item, I see the pre-edited notes. When I refresh the page, of course, everything lines up perfectly.
What is the best way to update the items data, so that it is always consistent with the textarea edits? Should I reload the items data somehow (with another AJAX call to the db)? Or is there a better way to bind the models together?
Here is the JS:
export default {
mounted () {
this.loadItems();
},
data() {
return {
items: [],
active: {
notes: ''
},
}
},
methods: {
loadItems() {
axios.get('/api/items/'+this.id)
.then(resp => {
this.items = resp.data
})
},
saveNotes () {
...api call to save in db...
},
updateActive (item) {
this.active.notes = item.notes;
},
}
}
i can't find items property in your data object.
a property must be present in the data object in order for Vue to convert it and make it reactive
Vue does not allow dynamically adding new root-level reactive properties to an already created instance
maybe you can have a look at this:
Vue Reactivity in Depth
It doesn't seem like this.items exists in your structure, unless there is something that isn't shown. If it doesn't exist set it as an empty array, which will be filled on your ajax call:
data() {
return {
active: {
notes: ''
},
items: [],
},
Now when you ajax method runs, the empty array, items, will be filled with your resp.data via this line:(this.items = resp.data). Then you should be able to iterate through your items array using v-for and your updateActive method should work as you intend it to.
use PUSH
this.items.push(resp.data);
here is a similar question
vue.js http get web api url render list

Deeply nested data objects in VueJS

I've got a VueJs front end that fetches some data from an API. The app uses vue-router.
The data fetched for one component is similar to the following:
{
name: ...,
email: ...,
order: {
data: {
line_items: [
{
quantity: ...
}
]
}
}
}
The component is instantiated with a data object called info:
data () {
return {
info: {}
}
}
In the beforeRouteEnter hook, the data is fetched by a vue-resource http.get and info is set to the body of the result like this:
vm.info = result.body
When the component renders, the following errors are produced:
TypeError: undefined is not an object (evaluating _vm.order.data.line_items')
In the template, the data is referenced in curly braces as per usual, however, if I just reference info in the template like this:
{{ info }}
it will output all of the data and not complain at all.
What is the correct way to assign a deeply nested data object?
If you are finding #saurabh answer is not working then you may need to check how you are assigning the new values to your object.
Firstly is the data being accidiently set as a string? hence {{ info }} working (or appearing to). May be worth using response.json() to set the data.
If thats not it then the error may be produced as the data you have set is not reactive. As you are assigning a nested object you may need to use different methods to make it reactive, i.e
Vue.set(vm.someObject, 'b', 2)
or
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
check out: https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
because your response is an object you may want to break out your data into corresponding params, i.e.
data () {
return {
info: {
name: '',
email: '',
order: {},
},
}
}
then you can assign name & email as you expected (info.email = ...).
For info.order you'd use Vue.set:
Vue.set(this.info, 'order', result.body.order)
The actual issue here is a life cycle one. The route guard beforeRouteEnter is called after the component is created so the error is thrown because the data isn’t there when the component tries to access it.
You have to use condition rendering here, which you can easily do with help of Vue directive v-if. It may give error if the data is not populated and you try to access it, so v-if will render that part of HTML only when data is present.
You need to do something like following:
<div v-if="info.order">
<span>
{{ info.order }}
</span>
</div>
In my scenario, I had to use one Vue.set that wrapped an Object.assign:
I'm trying to set state.workouts[state.workoutDate].isDone in a Vuex mutation
toggleWorkout(state, isDone) {
Vue.set(
state.workouts,
state.workoutDate,
Object.assign({}, state.workouts[state.workoutDate], { isDone: isDone })
);
},
Force update object setting a new object with same content
<button #click="my.object.nested.variable = 2; my = Object.assign({}, my);">
or
this.my.object.nested.variable = 2;
this.my = Object.assign({}, this.my);