Slot doesn't render well on child component VueJS - vue.js

I'm trying to loop over a component, I fill up slot with some data but they are not rendering well.
Weird behaviors :
Data are displayed but not visible.
In chrome if i toggle the device toolbar in the debug panel, data are now visible.
Changing font-size in the debug panel make my data visible
When i put a Child component outside the loop, the looped ones are rendered well.
Snippet from my parent Component :
<li class="cards__item" v-for="(staffMember, index) in staff">
<card-profil>
<h3 slot="title">{{staffMember.name}}</h3>
</card-profil>
</li>
Snippet from my child Component :
<template>
<section class="card-profil">
<figure class="fig-card-profil">
<figcaption class="figcaption-card-profil">
<slot name="title"></slot>
</figcaption>
</figure>
</section>
</template>
I get my data this way in my parent component:
export default {
data: function () {
return {
staff: []
}
},
mounted () {
this.getStaff()
},
methods: {
getStaff: async function () {
const staff = await axios({ url: 'https://randomuser.me/api/?results=8' })
this.staff = staff.data.results
}
}
}
Is this problem of lifehook ? Do i have to use Scoped slot instead ? V-for issue ?
Thanks for sharing your thoughts.

Related

How to add class to the parent component when the button is clicked in child in vue

I have a button in my child component and when I click this button I want to add a class in my parent component. I added child component as a slot in parent component.
parent component:
<template>
<div :class="editMode ? 'class-add' : ''">
<slot name="default"></slot>
</div>
</template>
<script>
export default {
props: {
editMode: {
type: Boolean,
required: true,
},
},
};
</script>
child component:
<button #click="addClass">Click Me!!!</button>
addClass() {
this.$emit('edit-abc', true);
},
And here how I am adding the class:
<parent-component :edit-mode="editMode">
<template #default>
<child-component #edit-abc="editAbc($event)" />
</template>
</parent-component>
The problem is as you see, I have several abcs (abcs is an object which includes several abc) to send to child the class only the one which is clicked. So I believe here #edit-abc="editMode = $event", instead of editMode = $event, I need to create a function and filter the one that I want to add the class but my logic is wrong somewhere. Here what I have done as a function.
editAbc(event) {
this.abcs.filter((a) => {
if (a.id) {
this.$nextTick(() => {
return (this.editMode = event);
});
}
});
},
You have to declare the editMode data property to use it in your event handling.
data() {
return {
editMode: false
};
}
If you need to send separate events, then simply use different events.
You intentions with "several abcs" are not really clear. And it looks for me like you have a design flaw.
Please clarify it further.
UPDATE
Here is a stackblitz with the solution.

Vue 3: access VueComponent object placed in slots

I'm working on tab component and I want to render tab labels in parent component by getting child's slot, named 'label'
In Vue 2.x I could approach that, by referring to $slots property of tab component, in Tabs.vue:
<template>
<section class="tabs">
<ul class="tabs-labels">
<li
v-for="tab in tabs"
:key="tab._uid"
:class="[{'active': tab.isActive}, 'tab-label']"
#click="selectTab(tab);"
>
{{ tab.$slots.label }}
</li>
</ul>
<div class="tabs-content">
<slot/>
</div>
</section>
</template>
<script>
export default {
name: 'Tabs',
data () {
return {
tabs: [],
};
},
mounted () {
// filter tabs in case there were additional vue components placed in slots
this.tabs = this.$children.filter(tab => tab.$options.name === 'TabContent');
},
methods: {
selectTab (selectedTab) {
// set isActive property of the tab by comparing their uids
this.tabs.forEach(tab => {
tab.isActive = (tab._uid === selectedTab._uid);
});
},
},
};
</script>
TabsContent.vue:
<template>
<div v-show="isActive" class="single-tab-content">
<slot/>
</div>
</template>
<script>
export default {
name: 'TabContent',
data () {
return {
isActive: false
};
},
};
</script>
Here, when the tab label clicked, in Tabs.vue I iterate through tabs array and setting their isActive property, comparing their uid and uid of selectedTab
But in Vue 3.x API of slots has changed, so I changed the way of getting tab contents:
from
this.tabs = this.$children.filter(tab => tab.$options.name === 'TabContent');
to
this.tabs = this.$slots.default().filter(tab => tab.type.name === 'TabContent');
but as I understand, it getting only vNodes, not actual VueComponent that rendered, so when I'm executing selectTab method
tab.isActive = (tab._uid === selectedTab._uid);
it updates only isActive properties for tabs, that were saved in tabs array, not for actual tab contents, so v-show never changes.
Is there any way to get actual rendered VueComponents from <slots>? Or maybe this approach is wrong from the beginning and I should try something else?
Edit
CodeSandboxes for both versions:
Vue 2.x -ignore the error about refering to children during render, it's a bug on CodeSandbox
Vue 3.x
Its a bit more complicated with Vue 3. You will want to look into using provide and inject. here is a good example.
https://gist.github.com/cathrinevaage/4eed410b31826ce390153d6834909436
sandbox - https://codesandbox.io/s/happy-rubin-z414h?file=/src/App.vue
The example above is using typescript however you get the idea.

Vue `vm.$on()` callback not working in parent when event is emitted from dynamic component child

I'm experiencing a problem where a custom event (swap-components) emitted from a dynamic component (A.vue or B.vue) is not being listened to correctly in the parent of the dynamic component (HelloWorld.vue).
Here is the source on GitHub (created using vue cli 3).
Here is a live demo showing the problem.
In the live demo, you'll see that clicking the button in the dynamic component with background color DOES NOT change the dynamic component. But when clicking the button below the background color (which originates in the HelloWorld.vue parent), the dynamic component DOES INDEED change.
Why is this happening and how to fix it?
Below I'll copy over the contents of the 3 main files of interest into this post:
HelloWorld.vue (the parent)
A.vue (sub component used in dynamic component)
B.vue (sub component used in dynamic component)
HelloWorld.vue:
<template>
<div>
<h1>The dynamic components ⤵️</h1>
<component
:is="current"
v-bind="dynamicProps"
></component>
<button
#click="click"
>Click me to swap components from within the parent of the dynamic component</button>
</div>
</template>
<script>
import A from "./A.vue";
import B from "./B.vue";
export default {
data() {
return {
current: "A"
};
},
computed: {
dynamicProps() {
return this.current === "A" ? { data: 11 } : { color: "black" };
}
},
methods: {
click() {
this.$emit("swap-components");
},
swapComponents() {
this.current = this.current === "A" ? "B" : "A";
}
},
mounted() {
this.$nextTick(() => {
// Code that will run only after the
// entire view has been rendered
this.$on("swap-components", this.swapComponents);
});
},
components: {
A,
B
},
props: {
msg: String
}
};
</script>
A.vue:
<template>
<section id="A">
<h1>Component A</h1>
<p>Data prop sent from parent: "{{ data }}"</p>
<button #click="click">Click me to swap components from within the dynamic component</button>
</section>
</template>
<script>
export default {
props: ["data"],
methods: {
click() {
this.$emit("swap-components");
}
}
};
</script>
B.vue:
<template>
<section id="B">
<h1>Component B</h1>
<p>Color prop sent from parent: "{{ color }}"</p>
<button #click="click">Click me to swap components from within the dynamic component</button>
</section>
</template>
<script>
export default {
props: ["color"],
methods: {
click() {
this.$emit("swap-components");
}
}
};
</script>
I'm guessing this is because the event listener is listening for a swap-components event emitted by the parent component itself. Perhaps you can fix that by listening for a swap-components event from the child component then emitting an event on the parent component.
<template>
<div>
<h1>The dynamic components ⤵️</h1>
<component
:is="current"
v-bind="dynamicProps"
#swap-components="$emit('swap-components')"
></component>
<button
#click="click"
>Click me to swap components from within the parent of the dynamic component</button>
</div>
</template>
Or you can call your method directly when the event is emitted by the child component ..
<template>
<div>
<h1>The dynamic components ⤵️</h1>
<component
:is="current"
v-bind="dynamicProps"
#swap-components="swapComponents"
></component>
<button
#click="click"
>Click me to swap components from within the parent of the dynamic component</button>
</div>
</template>
this is not bound to the context anymore when you use function. It is only limited to the function scope. Use arrow function to let this bound to the parent context.
Change:
this.$nextTick(function() {
With:
this.$nextTick(() => {

Renderless Vue component with a click listener

I have read this post which goes in depth about renderless components:
https://adamwathan.me/renderless-components-in-vuejs/
A renderless component would pretty much look like this:
export default {
render() {
return this.$scopedSlots.default({})
},
}
Now I would like to use this renderless component but also add a click listener to whatever is being passed into the slot.
In my case it would be a button. My renderless component would simply wrap a button and add a click listener to it, which in turn performs an AJAX request.
How would I go about adding a click listener to the element that is being passed into the slot?
Assuming you want to bind the click handler within the renderless component, I think from this post that you need to clone the vnode passed in to renderless, in order to enhance it's properties.
See createElements Arguments, the second arg is the object to enhance
A data object corresponding to the attributes you would use in a template. Optional.
console.clear()
Vue.component('renderless', {
render(createElement) {
var vNode = this.$scopedSlots.default()[0]
var children = vNode.children || vNode.text
const clone = createElement(
vNode.tag,
{
...vNode.data,
on: { click: () => alert('clicked') }
},
children
)
return clone
},
});
new Vue({}).$mount('#app');
<script src="https://unpkg.com/vue#2.6.11/dist/vue.js"></script>
<div id="app">
<renderless>
<button type="button" slot-scope="{props}">Click me</button>
</renderless>
</div>
Here's one way to go about this.
Your renderless component wrapper would consist of a single action (i.e. the function to issue the AJAX request) prop.
Vue.component('renderless-action-wrapper', {
props: ['action'],
render() {
return this.$scopedSlots.default({
action: this.action,
});
},
});
Then another component which uses the aforementioned wrapper would enclose a customisable slot with a #click handler, which invokes the action that is passed in when triggered.
Vue.component('clickable', {
props: ['action'],
template: `
<renderless-action-wrapper :action="action">
<span slot-scope="{ url, action }">
<span #click="action()">
<slot name="action"></slot>
</span>
</span>
</renderless-action-wrapper>
`,
});
Finally, wire up the specialised version of the wrapper.
<clickable :action="doAjaxRequest">
<button type="button" slot="action">Button</button>
</clickable>
Here's a live example of the above suggestion you can play around with.

How can I update data from parent component when I click child component on the vue component?

My first component (child component) like this :
<template>
...
</template>
<script>
export default {
...
methods: {
addPhoto() {
const data = { id_product: this.idProduct}
const item = this.idImage
this.$store.dispatch('addImage', data)
.then((response) => {
this.$parent.$options.methods.createImage(item, response)
});
},
}
}
</script>
If the method addPhoto called, it will call ajax and then it will get response ajax
I want to send response ajax and another parameter to method createImage. Method createImage is located in parent component (second component)
My second component (parent component) like this :
<template>
<div>
<ul class="list-inline list-photo">
<li v-for="item in items">
<div v-if="clicked[item]">
<img :src="image[item]" alt="">
<span class="fa fa-check-circle"></span>
</div>
<a v-else href="javascript:;" class="thumb thumb-upload"
title="Add Photo">
<span class="fa fa-plus fa-2x"></span>
</a>
</li>
</ul>
</div>
</template>
<script>
export default {
...
data() {
return {
items: [1,2,3,4,5],
clicked: [], // using an array because your items are numeric
test: null
}
},
methods: {
createImage(item, response) {
console.log(item)
this.$set(this.clicked, item, true)
this.test = item
},
}
}
</script>
If the code executed, it success call createImage method on parent component. The console log display value of item
But my problem is the data on parent component not success updated
How can I solve this problem?
You really should get in the habit of using events instead of directly accessing the parent component from a child.
In your case, it would be simple to emit an event in the then handler of the child's async request:
.then((response) => {
this.$emit('imageAdded', item);
});
And listen for it in the parent scope:
<child-component #itemAdded="createImage"></child-component>
That way, any component that uses that child component can react to its imageAdded event. Plus, you won't ever need to spend time debugging why the createImage method is firing when it's never being called in the Vue instance.
Your code isn't working because the way you are invoking the createImage method means that the this inside the function will not be referencing the parent component instance when it is called. So setting this.clicked or this.test will not affect the parent instance's data.
To call the parent component's function with the right context, you would need to do this:
this.$parent.createImage(item, response)