How to pass props to slot, Vue 3 - vuejs3

Making a custom select box component. But having some trouble when I try to pass to slot.
<vb-select v-model="container"
title="bla bla"
multiple>
<vb-option v-for="(item, idx) in items" :key="idx" :value="item">{{item}}</vb-option>
</vb-select>
vb-option is slot and I am calling it in vb-select component.
vb-select
<ul v-if="state" class="vb-options">
<slot :state="state" :multiple="multiple"></slot>
</ul>
When I try to pass multiple to slot as a prop. I can't listen/watch it in the vb-option
vb-option
<li class="vb-option">
{{multiple}}
</li>
props:{
multiple:Boolean,
},
What is right way to achieve this? Watching props inside the slot for changes.

<slot :state="state" :multiple="multiple"></slot>
What you've done here is use scoped slots - but they don't quite work the way you're trying to use them.
Try this way:
<vb-select>
<template #default="scope"> <!-- you can also do #default="{ state, multiple }" -->
<vb-option :multiple="scope.multiple" />
</template>
</vb-select>
Slot scope isn't automatically applied to the props of the component you put inside the slot - you need to pass it explicitly as shown above.
So that's the "how slot scope works" part, but the second part is that you're trying to implement something called "compound components", and it's a little trickier than that in Vue. In React, to do this you'd use React Context, but in Vue you'd use provide/inject.
Here's a repo where I outline some usecases, including an select/option:
https://github.com/sethidden/vue-compound-components
If that's too crazy, there's really nothing wrong with doing something like this and handling option rendering in vb-select itself:
<vb-select :options="[{ value: '123', label: 'Hello' }, { value: '567', label: 'Helloooo' }]" />

Pass hasError to default slot.
<script setup>
const slots = useSlots():
const Child = defineComponent({
render: () => {
const data = slots.default?.()[0];
data!.props!.hasError = true;
return data;
},
});
</script>
<template>
<Child />
</template>

Related

why are the values of props recognised despite `props` not being used

I have a very simple vuejs component like this :
<template>
<h1>{{title}}</h1>
</template>
<script setup>
const props = defineProps({
title: String,
})
</script>
title is correctly interpreted to the prop being passed to this component. Out of curiosity: why do I not need to do {{props.title}} for this to work.
This only reason I now write {{props.title}} is in order to do away with lint errors.

Vue draggable change parent data when using KeepAlive

I have a vue3 app, and one of the child component uses vue-draggable.
In the parent component I have an object (let's call it myJson) which propagates to child component with props.
So far it works as expected.
However, when adding 'KeepAlive' to the parent component, every time I drag the items, myJson is set to the drag event instead of the origin data it had.
It still occures even if I pass to the child component a copy of myJson (with JSON parse-JSON stringify). See details below
parent component:
<template>
<KeepAlive>
<component :is="activeComponent" :my-json="myJson" />
</KeepAlive >
</template>
data: () => ({
myJson: { ...someData }
})
mid component:
<template>
<list-items :items="items" />
</template>
<script>
export default {
components: { ListItems },
computed: {
items() {
return JSON.parse(JSON.stringify(this.myJson.value.items))
}
},
}
</script>
child component (ListItems):
<template>
<draggable
v-model="items"
animation="100"
handle=".dnd-handle"
item-key="product"
class="items-list"
#start="drag=true"
#end="drag=false"
>
<template #item="{ element, index }">
{{element}}
</template>
</draggable>
</template>
<script>
import draggable from 'vuedraggable'
export default {
components: { draggable },
props: ['items'],
}
</script>
The items are displayed correctly in the component.
Before dragging, myJson is an object with my data.
After dragging myJson is an event.
Any idea?
vuedraggable version is 4.1.0
--UPDATE--
In parent component there is a function "update", which gets value and updates myJson.
methods: {
update (value) {
myJson = value
}
}
I found out that every time I drag, there is a call to this function with the dragging event as value, even when I try to catch the draggable events. Thats why myJson gets wrong value.
My problem was solved when I changed the function's name. But anyone knows why this happens?

Vue3 Parent component call child component method

I can't call child component method in parent component in Vue3
In Vue2, I can call child component method like this
this.$root.$refs.ChildComponent.methodName()
But in Vue3, I receive a error like this
runtime-core.esm-bundler.js:218 Uncaught TypeError: Cannot read properties of undefined (reading 'methodName')
defineExpose could do the magic. You could do something like this:
// in Parent
<template>
<ChildComponent ref="myChild"/>
</template>
<script>
const myChild = ref(null);
function() {
myChild.childMethod();
}
</script>
// ChildComponent
<template> ... </template>
<script setup>
function childMethod() {
// do something
}
defineExpose({
childMethod
});
</script>
You might want to pass in a prop to the child, and react to a change-event by calling the method. This could look something like this:
<!-- Parent.vue -->
<script setup>
/* declare invokeChildMethod */
</script>
<template>
<Child :prop="invokeChildMethod" />
</template>
As can be seen in the code below, when the variable (here called invokeChildMethod) changes (in the parent), an event for the child will be fired.
Here's a resource on watching props in Vue3.
In addition to nocrash9000's answer (which did the trick for me), do not forget to add the import statement
import { defineExpose } from 'vue'

v-bind:src image returning null, VUE 3 + DatoCMS

So I have a component called "image", which is imported from another component called "article". The imageCover is not required so, the user can provide it or not. So I'm getting the error "ncaught (in promise) TypeError: Cannot read property 'url' of null" and the articles that don't have an image are not being rendered.
I tried (in both components) to add a v-if="imageURL" and put under data imageURL:null, what I get is the text is render but the articles who have an image are not render anymore. So I added a "!imageURL" instead, but then I went back to the beginning.
Not sure what I'm not seeing, looked simple but it seems no. I reviewed on dato just checking the image data was not required and from dato side it seems to be okay or at least I think.
Here you can see my article component
<template>
<div id="w-node-_52c4cf53-08cb-22e4-8818-0dbbacb46b70-3759ffab">
<article-cover-img :imageURL="article.imageUrl.url"></article-cover-img>
<h4 class="s-m-t-32">
<article-title :articleTitle="article.articleTitle"></article-title>
</h4>
<div class="paragraph-medium" v-html="article.articleContent"></div>
<br />
<div id="w-node-_4ae8992e-0086-2e16-a92f-a86ff611a958-3759ffab">
<article-tag :articleTags="article.articleTags"></article-tag>
</div>
<br />
<div id="w-node-_9ede0771-d38b-4b24-691f-998db4595b94-3759ffab">
<article-fav></article-fav>
</div>
</div>
</template>
<script>
import ArticleTitle from '../elements/title.vue'
import ArticleCoverImg from '../elements/image.vue'
import ArticleTag from '../elements/tag.vue'
import ArticleFav from '../elements/favorites.vue'
export default {
components: {
ArticleTitle,
ArticleTag,
ArticleCoverImg,
ArticleFav,
},
props: {
article: Object,
},
}
</script>
And here my image component for now
<template>
<img class="image" v-bind:src="imageURL" />
</template>
<script>
export default {
props: {
imageURL: {
type: String,
},
},
}
</script>
Thank you in advance for your help :)
the issue is likely right here
<article-cover-img :imageURL="article.imageUrl.url"> </article-cover-img>
if article.imageUrl is null, article.imageUrl.url will throw an error, because you can't get the url from undefined (undefined.url)
You should check if the value is null before targeting the object property
There are several options, but I'll just mention a few.
#1 Check if article.imageUrl is null using a v-if
TBH< this is my least favorite, as it's unclear from this definition whether the v-if is processed before or after the imageUrl prop, so it gets a 👎 for clarity. But more importantly, it will not render the component, which may or may not be the intended behaviour.
<article-cover-img v-if="article.imageUrl" :imageURL="article.imageUrl.url"
></article-cover-img>
#2 Logical or ternary to pass null
<article-cover-img :imageURL="article.imageUrl && article.imageUrl.url"
></article-cover-img><!-- logical -->
<article-cover-img :imageURL="!!article.imageUrl ? article.imageUrl.url : null"
></article-cover-img><!-- ternary -->
#3 Optional chaining
using object?.prop is similar to object && object.prop and adding it to both objects makes for even safer use article?.imageUrl?.url (though it's not necessary)
more info
<article-cover-img :imageURL="article?.imageUrl?.url"></article-cover-img>
However, event thought this is ES2020 syntax, all evergreen browsers support it and Vue3 dropped support for IE11, you should be 👍 to use it.

Web components: How to work with children?

I'm currently experimenting with StencilJS to create some web components.
Now I know that there is <slot /> and named slots and all that stuff. Coming from React, I guess slot is similar to children in React. You can do a lot of stuff using children in React. Things I often did:
Check if any children are provided
Iterate over children to do something to each child (e.g. wrap it in a div with a class etc.)
How would you do that using slot/web components/stencilJS?
I can get the Host Element of my web component in Stencil using
#Element() hostElement: HTMLElement;
I use my component like
<my-custom-component>
<button>1</button>
<button>2</button>
<button>3</button>
</my-custom-component>
I want to render something like
render() {
return slottedChildren ?
<span>No Elements</span> :
<ul class="my-custom-component">
slottedChildren.map(child => <li class="my-custom-element>{child}</li>)
</ul>;
}
Kind regards
Using slots you don't need to put a condition in your render function. You can put the no children element (in your example the span) inside the slot element and if no children are provided to the slot it will fall back to it.
For example:
render() {
return (
<div>
<slot><span>no elements</span></slot>
</div>
);
}
Answering the comment you wrote - you can do such a thing but with some coding and not out of the box. Every slot element has an assignedNodes function. Using that knowledge and the understanding of Stencil component life cycle you can do something such as:
import {Component, Element, State} from '#stencil/core';
#Component({
tag: 'slotted-element',
styleUrl: 'slotted-element.css',
shadow: true
})
export class SlottedElement {
#Element() host: HTMLDivElement;
#State() children: Array<any> = [];
componentWillLoad() {
let slotted = this.host.shadowRoot.querySelector('slot') as HTMLSlotElement;
this.children = slotted.assignedNodes().filter((node) => { return node.nodeName !== '#text'; });
}
render() {
return (
<div>
<slot />
<ul>
{this.children.map(child => { return <li innerHTML={child.outerHTML}></li>; })}
</ul>
</div>
);
}
}
This is not an optimal solution and it will require that the style of the slot should have display set to none (cause you don't want to show it).
Also, it will only work with simple elements that only need rendering and not requiring events or anything else (cause it only uses them as html string and not as objects).
Thank you for the answer Gil.
I was thinking of something similar before (setting state etc. - because of timing issues that might come up). I didn't like the solution though, because you're then doing a state change within componentDidLoad, which will trigger another load just after the component did load. This seems dirty and unperfomant.
The little bit with innerHTML={child.outerHTML} helped me alot though.
It seems like you can also simply do:
import {Component, Element, State} from '#stencil/core';
#Component({
tag: 'slotted-element',
styleUrl: 'slotted-element.css',
shadow: true
})
export class SlottedElement {
#Element() host: HTMLDivElement;
render() {
return (
<div>
<ul>
{Array.from(this.host.children)
.map(child => <li innerHTML={child.outerHTML} />)}
</ul>
</div>
);
}
}
I thought you might run into timing issues, because during render() the child elements of the host have already been removed to make space for whatever render() returns. But since shadow-dom and light-dom coexist nicely within the host component, I guess there shouldn't be any issues.
I don't really know why you have to use innerHTML though. Coming from React I'm used to doing:
{Array.from(this.host.children)
.map(child => <li>{child}</li>)}
And I thought that is basic JSX syntax and that since Stencil is also using JSX I could do that, too. Doesn't work though. innerHTML does the trick for me. Thanks again.
EDIT: The timing issues I mentioned will appear if you're not using shadow-dom though. Some strange things start to happen an you'll end up with a lot of duplicate children.
Though you can do (might have side effects):
import {Component, Element, State} from '#stencil/core';
#Component({
tag: 'slotted-element',
styleUrl: 'slotted-element.css',
shadow: true
})
export class SlottedElement {
children: Element[];
#Element() host: HTMLDivElement;
componentWillLoad() {
this.children = Array.from(this.host.children);
this.host.innerHTML = '';
}
render() {
return (
<div>
<ul>
{this.children.map(child => <li innerHTML={child.outerHTML} />)}
</ul>
</div>
);
}
}

Resources