Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide/inject for custom directives #6487

Open
ianwalter opened this issue Aug 31, 2017 · 32 comments
Open

Provide/inject for custom directives #6487

ianwalter opened this issue Aug 31, 2017 · 32 comments

Comments

@ianwalter
Copy link

What problem does this feature solve?

If a user uses a custom directive in their app in multiple places they might need to configure the directive in two or more different ways depending on the area of the app in which the directive is being used. If this configuration is used in many instances in one of these areas, providing this configuration on every instance becomes redundant and cumbersome.

What does the proposed API look like?

I think the provide/inject pattern would be a good solution to this. A user could add different configurations in the top level provider components and use the custom directive normally in their descendants.

var Provider = {
  provide: {
    foo: 'bar'
  },
  // ...
}

var OtherProvider = {
  provide: {
    foo: 'baz'
  },
  // ...
}

Vue.directive('bar', {
  inject: ['foo'],
  bind (el, binding) {
    // binding.injections.foo or binding.foo 
  }
})
<provider><div v-bar="something"></div></provider>
<other-provider><div v-bar="somethingElse"></div></other-provider>

I'm not confident on what the best place is for the actual injections to live in the directive hook arguments but there are some ideas in the code example above.

@posva
Copy link
Member

posva commented Sep 1, 2017

Can you please be more explicit about what kind of directive would need that, please?
A configuration could be extracted into one single js file and import it in different directives if that's what you mean, but I don't think that's cumbersome nor redundant

If you mean setting the configuration when using the directives (in the template), well, I think it's much better to explicitly give the directive a configuration that making it depend on an injected value somewhere else, which could lead to hard to debug bugs.

On a side note, inject/provide is meant for advanced usage on libs where we want to make things easier to use without having the user to worry about connecting explicitly many things together, but is discouraged in applications because it makes things implicit and harder to understand/debug

@jsnanigans
Copy link

I don't think this is really needed. If you really want to have a global configuration for all your directives I would do this with a vuex store object, this would even make it easy to update when you need to

@ianwalter
Copy link
Author

@jsnanigans I'm not sure what you mean. How does a plugin have access to the user's Vuex store? And this is not about global configuration. It's about context-aware configuration.

@jsnanigans
Copy link

I was thinking you could define the config something: {foo: 'bar'} in your store, then in your component you get the config and pass it to your directive <tag v-bar="something" />.

I think I misunderstood what you are trying to accomplish with provide/inject for directives..
do you want to avoid passing the config in the directive attribute?
so that you only have to write <tag v-bar /> and config is provided by the components and injected in the directive?

@ianwalter
Copy link
Author

ianwalter commented Sep 1, 2017

@posva I understand provide/inject is for advanced usage and I'm not trying to change that. All I'm saying is that provide/inject should work the same way for directives as it does already for components. And for the same reasons. Let me give you a more concrete example (apologies I should have done this from the start):

I have a plugin that provides the v-css directive. The directive takes a JavaScript function (in the example below primaryColor), executes it by passing a config object to it, and when the function returns a string with CSS styles, the directive generates a CSS class name for styles, injects the styles into the page, and appends the CSS class name to the elements class property:

primaryColor (config) {
   return `color: ${config.theme === 'fun' ? '#ff0000' : '#000'};`
}
<greetings>
  <div v-css="primaryColor">Nice</div>
  <div>to</div>
  <div v-css="primaryColor">meet</div>
  <div v-css="fontWeight700">you</div>
</greetings>

The config object that is passed to the function (the directive value) has to be passed to the plugin beforehand in some way so one way to do it is to pass it before the plugin is installed:

Vue.use(cssDirective(config))

Ok, that works globally, but what if we want to use different themes in our app:

<greetings>
  <div v-css="primaryColor">Nice</div>
  <div>to</div>
  <div v-css="primaryColor">meet</div>
  <div v-css="fontWeight700">you</div>
</greetings>
<features>
  <div v-css="primaryColor">Shiny</div>
  <div>Affordable</div>
  <div v-css="primaryColor">Durable</div>
  <div v-css="fontWeight700">Eco-friendly</div>
</features>

In my suggestion, the greetings and features components can provide config objects with different themes. The v-css directive can then inject the config object instead of getting it before install or having the user pass it in with the directive value. I was saying passing it in with the directive value is cumbersome and redundant because it would look like this:

<greetings>
  <div v-css="[config, primaryColor]">Nice</div>
  <div>to</div>
  <div v-css="[config, primaryColor]">meet</div>
  <div v-css="fontWeight700">you</div>
</greetings>
<features>
  <div v-css="[otherConfig, primaryColor]">Shiny</div>
  <div>Affordable</div>
  <div v-css="[otherConfig, primaryColor]">Durable</div>
  <div v-css="fontWeight700">Eco-friendly</div>
</features>

Please do not get hung up on this somewhat-complex CSS-in-JS example. I can think of other use cases as well, but this is my most immediate use case.

@posva
Copy link
Member

posva commented Sep 1, 2017

I really think directives are probably not what you want to use for CSS in js.
To me, your example looks perfectly fine using the config + primaryColor. Yes, it looks cumbersome but I think adding provide/inject to directives would allow users to easily mess up things and make them really hard to debug

I understand provide/inject is for advanced usage and I'm not trying to change that.

I want to reemphasize that It's meant for libs not applications

@jsnanigans
Copy link

jsnanigans commented Sep 1, 2017

would it be so much easier to just do this with :style or even just a class?

<greetings>
  <div class="primaryColor">Nice</div>
  <div>to</div>
  <div :style="primaryColor">meet</div>
  <div :style="fontWeight700">you</div>
</greetings>
primaryColor: _ => {color: this.config.theme.primaryColor}
@ianwalter
Copy link
Author

ianwalter commented Sep 1, 2017

I really think directives are probably not what you want to use for CSS in js.

I strongly disagree. This use case fits within the guides description of "low-level DOM access on plain elements" and is a perfect example of why directives should exist in the first place. Why? Because the directive is only changing the class value on a single element. No user wants to litter their app with a bunch of Higher Order Components just to deliver a class name.

I think adding provide/inject to directives would allow users to easily mess up things and make them really hard to debug

Yes, I agree, this is a downside, but you could say the same for provide/inject functionality in components and that is already in Vue. You could also say the same about React's Context.

I want to reemphasize that It's meant for libs not applications

I'm sorry but I don't really understand this. Provide/inject can be used in components right? Components are only used in applications.

@posva
Copy link
Member

posva commented Sep 1, 2017

Yes, I agree, this is a downside, but you could say the same for provide/inject functionality in components and that is already in Vue. You could also say the same about React's Context.

As you can imagine, it's not because it's dirty that we can throw dump to it

I'm sorry but I don't really understand this. Provide/inject can be used in components right? Components are only used in applications.

Components can be added by libs
https://vuejs.org/v2/api/#provide-inject

@ianwalter
Copy link
Author

@posva That is not a fair summation of what I was saying. I'm saying that debating provide/inject's pros and cons is pretty much pointless because it is already a framework feature. It solves the same problem as React's Context: the need for context-aware data delivery. My issue is that components can receive data in this way and directives cannot. Why? You can do just as much harm if not more with components. This feature exists anyway because maintainers ( maybe even yourself :) ) have decided that the pros outweigh the cons. Is there a con that applies to directives that doesn't also apply to components?

Components can be added by libs

Yes and directives can be added by libs too.
https://vuejs.org/v2/api/#Vue-directive

@posva
Copy link
Member

posva commented Sep 1, 2017

What I'm trying to say is that adding p/i to directives could make more harm than help...

I pointed out the link because it talks about the purpose of provide-inject...
That last part of your comment was provocative and unnecessary... I found it offensive because I was trying to help...

@ianwalter
Copy link
Author

@posva Sorry, that was not my intention at all. I just wanted to clarify for anyone reading this that both directives and components can be installed via libraries and so that is not a difference between the two when considering whether one or both have a legitimate use for P/I.

@posva
Copy link
Member

posva commented Sep 1, 2017

I just wanted to clarify for anyone reading this that both directives and components can be installed via libraries and so that is not a difference between the two when considering whether one or both have a legitimate use for P/I.

But it's not like that, there's a difference.
So, do you have an other use case apart from the CSS one you mentioned?

@ianwalter
Copy link
Author

But it's not like that, there's a difference.

Ok, what is the difference you're referring to? Or what is it about what you quoted that is incorrect (I'm legitimately trying to figure out what you mean)?

I have another use case involving input validation but it's not much different from the use case I've already laid out. They are both essentially a more convenient way to set configuration so that you don't have to specify it over and over again as long as you're within a certain context. This seems to me like it is the whole point of P/I as a feature but I feel like you are dismissing my use case without giving a reason.

Just to be clear, I'm advocating for this feature as a library author. My end goal is to make using the library easier in the event that the library's API is better suited to use directives instead of components.

@matthiasg
Copy link

matthiasg commented Dec 4, 2017

I just stumbled onto this as well. I would have liked to write a directive to access a shared object that is injected in components but inaccessible in a directive.

The shared object is the main instance of our application which gives us access to some state. In this case whether or not running on a Phone,Tablet,Desktop etc. Based on that information the v-responsive directive would behave slightly differently when setting appropriate classes on the bound element.

I have to duplicate some detection code out now and still won't be able to allow a consumer of our application instance to override within that instance (there can be more than one on screen in some scenarios). Overriding is also required for SSR btw.

@LinusBorg
Copy link
Member

vnode.context.$root

will give you the main instance as well.

@stalniy
Copy link

stalniy commented Dec 27, 2017

This is very useful for situations when you want to add contextual meaning. For example, I have a Popover which have a slot for body. And I want to dismiss/hide popover when user clicks on a particular link.

<popover>
   <h1>Not dismissable title</h1>
   <button type="button" dismiss-popover>close</button>
   <router-link to="..." dismiss-popover>go to...</router-link>
   <button type="button" dismiss-popover @click="changeStatus">make offline</button>
</popover>

In this way we have kind of contextual directive for Popover component. Popover can provide own instance for everybody inside and dismiss-popover can easily get it.

@LinusBorg
Copy link
Member

...or use a scoped slot.

@stalniy
Copy link

stalniy commented Dec 27, 2017

@LinusBorg I can't use scoped slot because then I need to bind @click or whatever event. In Vue files I can't do this (it shows error telling me that there 2 duplicated attributes). Also I don't want to make my template handlers to be complicated:

<popover>
   <template slot-scope="popover">
      <h1>Not dismissable title</h1>
      <button type="button" dismiss-popover>close</button>
      <router-link to="..." dismiss-popover>go to...</router-link>
      <button type="button" @click="changeStatus" @click="popover.close">make offline</button>
   </template>
</popover>

Update: Also if I create a scoped to Popover directive I can't use it inside provided slots. shows error:

Failed to resolve directive: v-dismiss-popover

@LinusBorg
Copy link
Member

LinusBorg commented Dec 27, 2017

it shows error telling me that there 2 duplicated attributes

<button type="button" @click="changeStatus(); popover.close()">make offline</button>

Also I don't want to make my template handlers to be complicated:

That's a valid opinion to have - personally I think scoped slots are worthy to learn and should be in everyone's arsenal.

Update: Also if I create a scoped to Popover directive I can't use it inside provided slots. shows error:

Well that shouldn't be an issue because we are trying to use scoped slots instead of this directive...

@stalniy
Copy link

stalniy commented Dec 28, 2017

@LinusBorg what about cases when Popover and its content in different components? Then you can't easily access popover to close the dialog by clicking or selecting somethid.

Use case:

<popover>
   <canned-responses />
</popover>

CannedResponses:

<acordion>
   <acordion-group>
      <survey-list />
   </acordion-group>
   <acordion-group>
      <prepared-responses-list />
   </acordion-group>
</acordion>

Then inside SurveyList I need to close Popover when user selects a survey. The only possible way currently is to emit event 2 times to reach Popover

@LinusBorg
Copy link
Member

LinusBorg commented Dec 28, 2017

That would probably be a solid usecase for provide/inject if you don't want to pass down the scoped slot't callback 2 levels, but why through a directive? do it in the component the regular way.

Personally I would probably still prefer the CannedResonses component to emit an event when something was selected so I can do:

<popover>
   <canned-responses slot-scope="{ close }" @selected="close"/>
</popover>

Or even better, simply use a v-if in the parent:

<popover v-if="show">
   <canned-responses @selected="show = false"/>
</popover>

Edit: By the way, jus to give some perspective: If it seems like we actively try to resist adding this feature, it's because we generally try to challenge each feature request thoroughly because we want to try and keep both filesize and API surface increases to a minimum.

The codebase has already grown 20% since 2.0, we want to keep an eye on that. So for every new feature request that comes in and the potential is unclear, we challenge it by arguing against you, the ones arguing for it, to see if all options to solve this adequatly with the current API have been tried and found to not suffice.

Only then will we consider a feature request to make it into core.

@designerzen
Copy link

Although this behaviour can be worked around, I agree that it would be useful to allow directives to access providers. My use case is for a wizard component where any other component can trigger an instructional animation which presents hints as tooltips that point to themselves. This could be achieved by wrapping each component in an Instruction component, but this is both verbose and unnecessary and would not easily allow for contextual help as in my opinion the instance component itself should be responsible for describing it's own help information, rather than the Instruction wrapper. I have written a library that side-steps these shortcomings, and it does work as described, but ideally would like to use the regular Vue logic to accomplish the same results.

@bebjakub
Copy link

bebjakub commented Nov 23, 2018

Till this feature will be released I found a example how to use vnode for this case https://codepen.io/Kradek/pen/zZmpNo

@snowyu
Copy link

snowyu commented May 9, 2019

Is there any workaround to get parent's provide in the directive?

Here is my user story:

The svg group has no width and height, so I build a provider(viewBox) on it. and it will be used on the the directive v-text-wrap to wrap text.

<svg-group viewBox={width: 200, height: 200}>
  <text v-text-wrap>Its very long long long text to wrap......
  </text>
</svg-group>

BTW, Defining the viewbox provider as directive to better reuse code. So when do implement this feature?

@snowyu
Copy link

snowyu commented May 10, 2019

From the source there is no collected providers in vm. So currently the only workaround is :

import { DirectiveOptions } from 'vue';

export const M6yDirective: DirectiveOptions = {
  bind(el, bind, vnode) {
      // todo: native element hasn't component instance.
      const vm = vnode.componentInstance;
      // cache the providers to speedup.
      const providers = vm._providers = getProviders(vm);
     if (providers.viewBox) ....
  },
  update(el, bind, vnode){
      const vm = vnode.componentInstance;
      // use the cached vm._providers  directly
     vm._providers['viewBox']
  },
};

function cloneExclude(dest, src, excludes: string|string[]) {
  let vKeys = Object.keys(src);
  if (typeof excludes === 'string') excludes = [excludes];
  vKeys = vKeys.filter(value => -1 === excludes.indexOf(value));
  vKeys.forEach(key => dest[key] = src[key]);
  return dest;
}

function getProviders(vm) {
  const providers:Object = {};
  while (vm) {
    if (vm._provided) {
      cloneExclude(providers, vm._provided, Object.keys(providers));
    }
    vm = vm.$parent
  }
  return providers;
}
@jbmikk
Copy link

jbmikk commented Jul 29, 2020

On a side note, inject/provide is meant for advanced usage on libs where we want to make things easier to use without having
the user to worry about connecting explicitly many things together, but is discouraged in applications because it makes things
implicit and harder to understand/debug

I agree with this, however, I don't understand the reasoning for having inject/provide available in components but not directives.
Why can those advanced usages on libraries be implemented with components but not with directives? I always get the feeling directives are second class citizens in Vue for no particular reason.

@albanm
Copy link

albanm commented Jan 17, 2021

From ealier in the conversation by @posva

But it's not like that, there's a difference.

Possible to have some justification on this phrase ? I really don't get what the difference is.

What I want to do is to expose register/unregister methods in a parent component that needs to be aware of some of its children elements whereabouts. Similar to form validation in vuetify that uses a registrable mixin, but where the children are very non-intrusive directives, not components. I really don't see why this pattern would be suitable for components but not directives.

@daniele-pelagatti
Copy link

daniele-pelagatti commented Nov 29, 2021

Just my two cents on this.

I'm a Vue developer since 2017 (maybe more?) I recently started the transition of my company to Vue 3 and I'm trying to port some useful tools we've been using in Vue 2 to Vue 3 (Composition API) , one of these is the Flip Toolkit which was previously ported to Vue 2 here.

I'm trying to do a pure composition API of such a library and, given setup functions don't have access to the this.$el element anymore, I'm facing a dilemma.

Let me explain: the core of the Flip toolkit are Flipper and a Flipped elements, the Flipper is a container for Flipped elements and Flipped elements register their $el with the Flipper element in order for the magic to work.

Pseudo-code looks like this:

Flipper

import { Flipper } from 'flip-toolkit'
[...]
const flipInstance = new Flipper({
  element: this.$el,
  [...]
})

const addFlippedElement = () => {
  flipInstance.addFlipped([...])
}

provide('addFlippedElement', addFlippedElement)

Flipped

const addFlippedElement = inject('addFlippedElement')
addFlippedElement({
  element: this.$el
})

Usage

Root of the app, maybe?

<template>
  <Flipper>
    <router-view />
  </Flipper>
</template>

RandomComponent.vue (inside a router view)

<template>
  <Flipped>
    <something />
  </Flipped>
</template>

The system supports nested Flippers and Flipped element, this is why provide/inject works well, and this is how the vue 2 version of the library was developed.

The main concern of the Flipper components is:

  1. provide a way for child Flipped to register themselves
  2. access the $el of itself and its contained Flipped in order to register them with the library
  3. do the two points above with nesting support

The main concern of the Flipped component is:

  1. find its nearest parent of type Flipper
  2. give the nearest Flipper a reference to its $el

A directive would be a much better fit for these kind of component in my opinion, the original vue-flip-toolkit uses this.$el in both of them in order to accomplish this task, and that's ok. But how would you translate a situation like this with composition API Components?

Isn't a Directive more suited for accessing and manipulating DOM elements than a Component in these cases?

In this particular case, given I didn't write the APIs of the library I'm using, I find that a directive that can provide to its children would be very useful, probably also more ergonomic to use (but that's a highly subjective opinion)

@marco-quintella
Copy link

It can be achieved now with

vnode.ctx.provides[InjectionKey]

@tada-tsu
Copy link

tada-tsu commented Aug 8, 2023

vnode.ctx.provides[InjectionKey]

ctx is not found in typescript declaration.
if ignore warning, worked...

@Giwayume
Copy link

declare module 'vue' {
    interface VNode {
        ctx: {
            provides: Record<symbol | string, any>;
        }
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment