EranGrin / vue-web-component-wrapper

vue3 - web component wrapper plugin
https://erangrin.github.io/vue-web-component-wrapper/
59 stars 7 forks source link

How to pass a component's emit event? #13

Closed east-rain closed 9 months ago

east-rain commented 10 months ago

I think your idea is amazing. But there is one issue. My goal is to use vue3 components in a vue2 project.

when i use your way, then i can't receive custom event. Below is a component that can receive text input.

createWebComponent({
    rootComponent: TextField,
    elementName: 'wrap-text-field',
    plugins: pluginsWrapper,
    cssFrameworkStyles: style,
    VueDefineCustomElement,
    h,
    createApp,
    getCurrentInstance,
});

The code below uses the component created above. It is made with vue2.

<wrap-text-field :value="textVal" @update:modelValue="onInput"/>

when i input some text, then onInput() never triggering

But

i create web component this way,

const defineTextField = defineCustomElement(TextField)
customElements.define('define-text-field', defineTextField);
// use in vue2 project
<define-text-field :value="textVal" @update:modelValue="onInput" />

when i input some text, then onInput() event trigger and i can check this event is CustomEvent()

is there a way to receive events emitted from a component created through a wrapper?

EranGrin commented 10 months ago

Interesting use case, I'll try to work on a solution. but, just to be clear, you attempt to create vue3 web component and then use it in vue2 project?

east-rain commented 10 months ago

Yes, I use vue3's component in vue2 project and finally I found the way.

In render, I receive inner component's event, and emit again.

But I have a other problem. With this way, I can't receive slot.

The Project that use Web Component, pass a slot But in Web Component can't receive that slot content.

If I use a plain defineCustomElemt lib in Vue, I can get a slot content. Do you have any good solution?

EranGrin commented 10 months ago

I am not sure how did you solve the issue, but it would be nice if we could add this feature to the lib, maybe you have time to open a PR

Regarding the slots, I need to check, can you give a more detailed example of the slot use case?

east-rain commented 10 months ago

my-component.vue has slot

<template>
    <div>some text</div>
    <slot/>
</template>

and this component define web component as your solution.

return VueDefineCustomElement({
        styles: [ cssFrameworkStyles ],
        props: rootComponent.props,
        ...........
        return () => h(rootComponent, .......) // rootComponent is <my-component>

And I use this web component this way.

<my-component><div>hi</div><div slot="default">hi</div><template #default>hi</template></my-component>

But my browser can't render as a slot. This is a same problem: https://github.com/EranGrin/vue-web-component-wrapper/issues/10

east-rain commented 10 months ago

Also I can't use web component's method. Do you have a solution?

EranGrin commented 10 months ago

I have found a solution for

  1. v-model
  2. emit event
  3. slots & named slots

    I've already spent several hours over here, and I need to divert my attention back to none open-source project 😉

    It will take a few days for me to update the docs and create code example

Also I can't use web component's method. Do you have a solution?

Please elaborate

EranGrin commented 10 months ago

New version is up with several new features. https://www.npmjs.com/package/vue-web-component-wrapper

east-rain commented 10 months ago

It looks nice. Do you have a case using provide?

This is a situation. A component provide('val', computedObject); B component inject('val')

When using a B component, dev console says '[Vue warn]: injection "val" not found.'

First time, I try to provide globally in pluginsWrapper. But, A component's computedObject is computed(). It is created when A component created.

So It's impossible to provide globally in pluginsWrapper. Do you have good idea?

EranGrin commented 10 months ago

Hi There, In case you did not notice, in the npm readme page of the plugin there are links to webpack and vite implementations here is vite https://stackblitz.com/edit/vue-web-component-wrapper?file=README.md&startScript=vite-demo

Furthermore, I might have some suggestions for how to solve your problem, but I would need to see it more clearly Please create stackblitz demo demonstrating your issue

east-rain commented 10 months ago

I'm sorry. it's little hard to implement. I try to explain detail. And if you steel confused, I will make a stackblitz.

component A ( wrapper component )

createWebComponent({
  rootComponent: {
    template: "<template><div><slot/><div></template>",
    setup(props, {}) {
      provide('myInfo', computedSomeObject);
    }
  },
  elementName: 'foo-wrapper-component',
  plugins: pluginsWrapper,
  cssFrameworkStyles: style,
  VueDefineCustomElement,
  h,
  createApp,
  getCurrentInstance,
});

component B

createWebComponent({
  rootComponent: {
    template: "<template></template>",
    setup(props, {}) {
      const val = inject('myInfo')
    }
  },
  elementName: 'foo-component',
  plugins: pluginsWrapper,
  cssFrameworkStyles: style,
  VueDefineCustomElement,
  h,
  createApp,
  getCurrentInstance,
});

And I use this web component like this.

<template>
    <foo-wrapper-component>
            <foo-component/>
            <foo-component/>
            <foo-component/>
    </foo-wrapper-component>
</template>

When load this page, [Vue warn]: injection "myInfo" not found. Because foo-wrapper-component and foo-component have own instance area. So foo component can't see foo-wrapper's "provide".

But when use this component as a normal vue component, Everything is OK.

Do you have a good idea? sharing a value each web component? (If there is no solution, I can get 'myInfo' from foo-wrapper's refs, and pass this object to foo-component's prop. But I'd like know sharing a data Each web component)

EranGrin commented 10 months ago

I found out how to add support for the provide/inject, it might take few days for me to release a new version

east-rain commented 10 months ago

Wow. Could you share a your idea?

EranGrin commented 10 months ago

You can check the latest release, 1.4.0   🎉

east-rain commented 10 months ago

I check your idea. But it can works in option API component. Can't access to Composition API's provide.

EranGrin commented 10 months ago

ok, I'll check it in the coming days

EranGrin commented 10 months ago

The provide function of the composition does have an issue as it only fire after the plugin run, which prevents the inject to get the value of the providers, the inject in the composition does work normal. I did found a work around

Inject child component example


<template>
  <div>
    <h1 class="text-xl">
      {{ testInject }}
    </h1>
  </div>
</template>

<script lang="ts">
import { ref, onMounted, defineComponent, inject } from 'vue';

export default defineComponent({
  name: 'AppChild',
  emits: ['customEventTest', 'update:modelValue'],
  props: {
    baseUri: { type: String, default: 'test.me' },
    modelValue: { type: String },
    apiToken: { type: String, required: true },
  },
  setup(props, { emit }) {
    // Reactive properties
    const configuration = ref({});
    const testInject = ref('');

    // Inject
    const testKey = inject('testKey');

    // Mounted equivalent
    onMounted(() => {
      console.log(testKey); // Outputs: 'This is test data'
      testInject.value = testKey as string;
    });

    // Methods
    const testEmit = () => {
      emit('customEventTest', { testEvent: '123456789' });
    };

    return {
      configuration,
      testInject,
      testEmit
    };
  },
});
</script>
<style>
.test-heading {
  font-size: 2em;
  margin-top: 2rem;
}
a {
  @apply text-gray-900 hover:text-gray-700;
}

header  {
  @apply font-sans;
}

main {
  @apply font-sans;
}
</style>

Provide parent component example

 <template>
  <div>
    <header class="bg-blue-300 rounded-lg shadow-lg p-4 mx-0 md:mx-20">
      <nav>
        <ul class="flex justify-between sm:mx-1 md:mx-20">
          <li>
            <router-link to="/">Home</router-link>
          </li>
          <li>
            <router-link to="/test1">Test route 1</router-link>
          </li>
          <li>
            <router-link to="/test2">Test route 2</router-link>
          </li>
          <li>
            <router-link to="/test3">Test route 3</router-link>
          </li>
        </ul>
      </nav>
    </header>
    <header class="bg-gray-800 text-white text-center p-4 mt-4 rounded-lg shadow-lg mx-0 md:mx-20">
      <div style="display: flex; justify-content: space-between;">
        <div class="text-black flex">
          <input
            class="p-2 rounded-lg shadow-lg"
          />
          <div class="text-white ml-4 align self-center">
            {{ modelValue }}
          </div>
        </div>

        <button
          class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
          @click="testEmit"
        >
          emit event outside
        </button>
      </div>
    </header>
    <main class="mt-4 p-4 mx-0 md:mx-20 bg-gray-200 rounded-lg shadow-lg">
      <router-view />
    </main>
    <footer class="bg-gray-800 text-white text-center p-4 mt-4 rounded-lg shadow-lg mx-0 md:mx-20">
      <div>
    <input v-model="reactiveProperty" placeholder="Type here...">
    <p>Value: {{ reactiveProperty }}</p>
  </div>
      <div class="mb-2">
        <slot></slot>
      </div>
      <div class="flex justify-center">
        <slot name="customName"></slot>
      </div>
    </footer>
  </div>
</template>

<script setup lang="ts">
import { ref, provide, getCurrentInstance, getCurrentScope, ComponentInternalInstance } from 'vue';

// Define props
const props = defineProps({
  apiToken: String,
  baseUri: { type: String, default: 'test.me' },
  modelValue: String
});

// Define emits
const emit = defineEmits(['customEventTest', 'update:modelValue']);

const reactiveProperty = ref('initial Value');

// Reactive properties
const configuration = ref({});
const credentials = ref({
  apiToken: props.apiToken,
  baseUri: props.baseUri
});

// Provide

interface InstWithProvides extends ComponentInternalInstance {
  provides: Record<string, any>;
}
const inst = getCurrentInstance() as InstWithProvides;
if (inst?.provides) {
  inst.provides['testKey'] = reactiveProperty;
}
console.log('inst', inst);

// Provide multiple values
console.log(provide); // Outputs: 'I am provide/inject data via default slot'
// Method
const testEmit = () => {
  emit('customEventTest', { testEvent: '123456789' });
};

console.log('getCurrentInstance', getCurrentInstance());
console.log('getCurrentScope', getCurrentScope());
// Watchers or computed properties (if any) can be added here
</script>

<style>
.test-heading {
  font-size: 2em;
  margin-top: 2rem;
}
a {
  @apply text-gray-900 hover:text-gray-700;
}

header  {
  @apply font-sans;
}

main {
  @apply font-sans;
}
</style>