6

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:

parent/child

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?

tony19
  • 125,647
  • 18
  • 229
  • 307
Liuka
  • 289
  • 2
  • 10
  • While you nicely describe your code and what you have tried, it still seems a little bit confused to me what you are trying to achieve in the first place. Maybe a few screenshots and/or sketches would help. With a better understanding of your situation, you would get more relevant support. – ghybs Jul 20 '18 at 01:56
  • I've updated the question – Liuka Jul 20 '18 at 09:08

1 Answers1

2

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>
IVO GELOV
  • 13,496
  • 1
  • 17
  • 26
  • That's the slot-scope I mentioned, but I don't think it's good for library component: this way the user, which decide to use a button to start the action, must know (and write each time) the existence of ctx and custom_event. – Liuka Jul 23 '18 at 12:25
  • Well, the other way around is for you to use an event bus - e.g. the `$root` instance. You will un/register handlers on this event bus and emit events also to this bus. – IVO GELOV Jul 23 '18 at 13:19
  • This is a great example of how to make this happen. Unfortunately you have raise an event within the template to communicate back to the child. I've never found a way to do this within a method since ctx is unknown within the scope of the slot. – Craig Nov 22 '21 at 16:17
  • Did you found a better solution for this? I found that the inject/provide method you can only communicate from the Parent to Child, not the way round. – kelvin Nov 03 '22 at 11:47
  • Vuetify uses the provide/inject method to handle form validation - so apparently it is a good way to handle their use case. – IVO GELOV Nov 03 '22 at 14:27