I'm writing a small collection of Vue components to create a library to use in future projects, but I'm quite confused about this topic; maybe I need a completely different approach, but I don't know...
I'm taking inspiration from (I think that's how is called) the policy pattern: you create a template component whose behaviour depends on nested components you pass as argouments. For example I created a Preview component which owns a method to change the bkg image and I want to nest in this component an overlay with the ability to call this method. Since this overlay can be everything I thought it would be nice if it has been nested through a slot:
<template>
<div class="preview" :class="{active: active}">
<div class="content">
<slot name="content"></slot>
</div>
<div class="overlay"><slot></slot></div>
</div>
</template>
(I'll make the content a v-for through a img list)
And the js:
props: {
content: {default: function () { return [] }}
},
data: function () {
return {
preview: null
}
},
methods: {
setPreview: function (e) {
this.preview = e
}
}
}
Then there's the child component which triggers the change onmouseover:
<template>
<div @mouseover="set">
<slot></slot> <!-- some random content -->
</div>
</template>
<script>
export default {
props: ['target']
methods: {
set: function () {
// figure a way to call "parent" setPreview
}
}
}
</script>
And then I would use this component like this:
<preview>
<template slot="content">... a bounch of v-if bound images</template>
<template>
<change-preview-onover target="first-img">...</change-preview-onover>
<change-preview-onclick target="second-img">...</change-preview-onclick> <!-- different policy -->
</template>
</preview>
I've tried two different approaches: scoped slots and provide/inject. With scoped slots I get something like this:
//preview
<template>
<div class="preview" :class="{active: active}">
<div class="content">
<slot name="content"></slot>
</div>
<div class="overlay" :callback="{setPreview}"><slot></slot></div>
</div>
</template>
//js...
//overlay element
<template>
<div @mouseover="set">
<slot></slot> <!-- some random content -->
</div>
</template>
<script>
export default {
props: ['target', 'callback']
methods: {
set: function () {
this.callback.setPreview(this.target)
}
}
}
</script>
//usage
<preview>
<template slot="content">... a bounch of v-if bound images</template>
<template slot-scope={callback}>
<change-preview-onover :callback="callback" target="first-img">...</change-preview-onover>
<change-preview-onclick :callback="callback" target="second-img">...</change-preview-onclick>
</template>
</preview>
I don't like this way because it breaks encapsulation (the user must know the existence of callback and pass it through all the change-previews components) and got a lot of redundant code. I've tried to move the slot-scope inside the overlay component but without any luck. So I've read about provide/inject and basically now I do like this:
//preview.js
provide: function () {
return {
setPreview: this.setPreview
}
}
//overlay.js
inject: ['setPreview'],
props: ['target'],
methods: {
set: function () {
this.setPreview(this.target)
}
}
This look pretty cool but I've not understand if that's the way provide/inject is meant to be used, or if it's ok to use it everywhere (mainly performance wise, I'll literally abuse it) to create a parent <-> slot communication, where, of course, slot is something semantically linked to the parent
EDIT 1
In Vue.js there's a standard way to handle parent child communication:
But this doesn't work whit slot because of how Vue handles the scope of components. Given my example Preview is not the parent of overlay since it's not directly nested inside the component template. Instead if I write something like this:
<template>
<div class="preview" :class="{active: active}">
<content>...<content> <!-- changes here -->
<overlay>...</overlay> <!-- and here -->
</div>
</template>
Overlay and Content are free to communicate with Preview simply emitting events. But throught slot, like in the first example I proposed before, content and overlay (and preview) are all children of a generic App content, and so emit doesn't fire to Preview, but to App (or whatever contains the preview component); so I need a new way to communicate from slot to parent and vice versa.
The main thread about this subject: https://github.com/vuejs/vue/issues/4332 Here they use scoped slot (ok, but awful) or $parent, which I can't use because it needs that slot is a direct children of the parent which is not always true, maybe I want to add a transition or something else, getting something like this:
//Modal
<div>
<tr-fade> <!-- tr-fade is a registered comopnent and so it's the $parent of slot -->
<slot></slot>
</tr-fade>
</div>
My question is: Is the provide/inject a good way to handle this cases? Does slot-scope suit better, even if imho breaks encapsulation and it's verbose? Or there are other ways to achieve this "policy pattern" without giving up the level of customization the slot offers?
Using the slot is very easy, we should just write the <slot> component (which is a reserved word in Vue. js) inside the child component's template, where we want to receive data. A child component uses slot.
v-slot is used on an HTML element. v-slot can only be used on a <template> element or the component receiving the slot. Otherwise, Vue throws a compile error. See Example 1 below. v-slot is used on a component, but the component has another <template v-slot> as a child.
You can simply inject the context of your child inside the slot and then emit events from this context:
// the child
<template>
<div>
<slot :context="thisContext"/>
</div>
</template>
<script>
export default
{
computed:
{
thisContext()
{
return this;
}
}
}
</script>
// the parent
<template>
<child @custom_event="handleCustom">
<template slot-scope="ctx">
<button @click="sendClick(ctx)">Click me</button>
</template>
</child>
</template>
<script>
export default
{
methods:
{
sendClick(ctx)
{
ctx.$emit('custom_event', {custom_data: 3});
},
handleCustom(payload)
{
console.log("Custom payload:", payload);
}
}
}
</script>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With