vuejs / core

🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
https://vuejs.org/
MIT License
45.65k stars 8k forks source link

Support provide/inject between Vue's Custom Elements (defineCustomElement) and Vue Components (defineComponent) #4476

Open gnuletik opened 2 years ago

gnuletik commented 2 years ago

What problem does this feature solve?

As stated in the docs (https://v3.vuejs.org/guide/web-components.html#provide-inject), the Provide/Inject API only works between Vue's Custom Elements.

It makes it harder to use injected libraries inside custom elements.

The use case is :

What does the proposed API look like?

I've thought of several approaches to implement this.

1. Deep provide function

The provide function (https://github.com/vuejs/vue-next/blob/db1dc1c63097ed62a3f683a7a11c7e819d90bb73/packages/runtime-core/src/apiInject.ts#L58-L60) could search inside parent elements :

let inst = getCurrentInstance().parent
while (inst !== null) {
  if (key in inst.provides) {
    return inst.provides[key]
  }
  inst = inst.parent
}

However, this can be a performance issue when heavily using provide/inject.

It could be enabled when needed as a function parameter like this :

inject('some-instance', { deep: true })

but many libraries use provide inside useLibrary()-like functions. So, library developers would have to add the deep parameter too.

To workaround this, this behavior may be manually enabled with a global configuration when required :

Vue.deepInject = true

2. Implement an event based provide/inject API

The provide/inject API could be reworked with CustomEvents.

There's a current discussion about implement it as a standard : https://github.com/webcomponents/community-protocols/issues/2 so most questions are covered.

There may be other possibilities I did not think about.

Thanks for your work !

yyx990803 commented 2 years ago

To clarify, what is the case that is currently not working? Can you provide a code sample demonstrating a simplified case of what you intend to do?

gnuletik commented 2 years ago

Thanks for looking into this! Here are multiple use cases I encountered :

Use case: async component

AppContext.vue

<template>
  <slot />
</template>

<script setup>
// it could be a library like vue-i18n
provide('something', 'test')
</script>

MyComponent.vue

<script setup>
// inject fails. it should not.
const val = inject('something')
</script>

main.ts

customElements.define(
  'my-app-context',
  defineCustomElement(AppContext)
)
customElements.define(
  'my-component',
  defineCustomElement(defineAsyncComponent(() => import('MyComponent.vue')))
)

index.html

<html>
  <body>
    <my-app-context>
      <my-component></my-component>
    </my-app-context>
    <script src="main.ts"></script>
  </body>
</html>

Use case: shared components

AppContext.vue

<template>
  <slot />
</template>

<script setup>
// it could be a library like vue-i18n
provide('something', 'test')
</script>

UtilComponent.vue

<script setup>
// inject fails. it should not.
const val = inject('something')
</script>

MyComponent.vue

<template>
  <UtilComponent />
</template>

main.ts

customElements.define(
  'my-app-context',
  defineCustomElement(AppContext)
)
customElements.define(
  'my-component',
  defineCustomElement(defineAsyncComponent(() => import('MyComponent.vue')))
)

index.html

<html>
  <body>
    <my-app-context>
      <my-component></my-component>
    </my-app-context>
    <script src="main.ts"></script>
  </body>
</html>

I made a reproduction of both usecases here : https://github.com/gnuletik/vue-inject-deep

Thanks!

u12206050 commented 1 year ago

Still doesn't work in Vue 3 it seems?

LinusBorg commented 1 year ago

Yes. This has no real prioity for us - the real-life usecase is still cloudy.

Personal Take: Your web components should not depend on a Vue-specific mechanism like provide/inject. Why turn your Vue Components into Web Components if you can only use them in other Vue apps because they depend on provide/inject?

gnuletik commented 1 year ago

Why turn your Vue Components into Web Components if you can only use them in other Vue apps because they depend on provide/inject?

My usecase was to integrate Vue into a MVC framework like Django, which already provides its own template system. The web components allows you to load vue components and inject variables with <my-custom-com var="{{ django_var }}"></my-custom-comp>.

Your web components should not depend on a Vue-specific mechanism like provide/inject

Using an external mechanism (implementing Vue provide/inject into the app) would work if you don't want to use the existing Vue ecosystem because you can't call the useXX() function of libraries, which usually calls inject.

dfreier commented 1 year ago

I think the use cases described by @gnuletik are very realistic.

EDIT, that actually works ⬇️

The offical vue documentation is a bit confusing in my opinon:

Vue-defined custom element won't be able to inject properties provided by a non-custom-element Vue component.

This means the parent provider component is a regular vue component and custom element is a child component which cannot inject the provided value.

But it doesn't mention that it is also not possible to provide a value from a custom element down into a sub component of the custom element which is also a regular vue component but it lives in the same project as the parent custom element.

You can get provide/inject to work if you use all child components of a custom element also as custom elements. That means you never use the components: { MySubcomponent } option, all sub components are global.

EDIT end ⬆️

Unfortunately there is still no solution to use provide/inject between custom elements if they were lazy loaded.

LinusBorg commented 1 year ago

But it doesn't mention that it is also not possible to provide a value from a custom element down into a sub component of the custom element which is also a regular vue component but it lives in the same project as the parent custom element.

That seems to work fine: Playground

dfreier commented 1 year ago

That seems to work fine: Playground

You're right! Sorry for the wrong accusations. 💐

HaNdTriX commented 2 days ago

Just to clarify.

✅ VueCustomElements share Context between each other

Example

<custom-element-1>
  <custom-element-2 />
<custom-element-1 />  

custom-element-2 gets the provide('foo', 'bar') from custom-element-1 via inject('foo')

Vue Core Code

https://github.com/vuejs/core/blob/ae97e5053895eeaaa443306e72cd8f45da001179/packages/runtime-dom/src/apiCustomElement.ts#L414-L427

🚨 VueElements do not share Context (provide/inject) with VueCustomElements

Example

<VueElement>
  <custom-element />
<VueElement/>  

custom-element does NOT get the provide('foo', 'bar') from VueElement via inject('foo')

This feels a bit inconsistent to me.