FranckFreiburger / vue3-sfc-loader

Single File Component loader for Vue2 and Vue3. Load .vue files directly from your HTML. No node.js environment, no build step.
MIT License
1.03k stars 116 forks source link

this.$refs.someUserDefinedRef.$el becomes null the second time it is used #16

Closed raphael-bresson closed 3 years ago

raphael-bresson commented 3 years ago

Hi Franck,

I'm still enjoying your library !

I noticed that if I define a ref for a child component. For example:

<template>
  ...
  <hiddenFileInput
    ref="filesInput"
    @loadedFiles="storeAttachments"
  ></hiddenFileInput>
...
<template>

and use it in a method, for example:

<scripts>
  ...
  methods: {
    selectAttachments() {
      this.$refs.filesInput.$el.click();
    },
  },
  ...
</scripts>

Then the second time the method is called, I get this error:

Uncaught TypeError: this.$refs.filesInput is null
    selectAttachments https://cdn.jsdelivr.net/npm/vue3-sfc-loader@0.2.21/dist/vue3-sfc-loader.js line 22 > Function:63
...

It seems like I lose the reference, although the child component still exists. So I ended up storing the ref in a data on the first call like so:

<scripts>
  ...
  data() {
    return {
      refFileInput: null,
    };
  },
  ...
  methods: {
    selectAttachments() {
      if (!this.refFileInput) {
        this.refFileInput = this.$refs.filesInput.$el;
      }
      this.refFileInput.click();
    },
  },
  ...
</scripts>

But isn't it a bug ? Or is there something I don't understand or do right ? As a side note is using a ref the right way to reference a child component ?

Thank you for your help,

Merci

Raphaël

FranckFreiburger commented 3 years ago

Salut Raphaël,

Do you encounter the same issue without vue3-sfc-loader (just using plain Vue3) ?

raphael-bresson commented 3 years ago

I just tested it with plain Vue3. It works normally. Which is what I expected since I found many examples on the web of people using $refs this way (maybe with vue 2 though…)

raphael-bresson commented 3 years ago

I've prepared a very simple test case that reproduces the bug. If you comment the line this.fileName = "APRES"; in App.vue everything is fine and the ref is not lost.

However on changing that data and then rerendering the DOM the ref is lost. So it would have something to do with rerendering the DOM ?

Hope you can fix this easily 😁

BUG_vue3-sfc-loader.zip

FranckFreiburger commented 3 years ago

Raphaël,

I think that your issue is related to the way you load the components. Your loadComponent() creates a new fresh moduleCache each time it is called, but vue3-sfc-loader requires a common moduleCache for all modules/components it loads/use.

vue3-sfc-loader is designed to be used with the less constraints as possible. You just have to use vue3-sfc-loader's loadModule() the first time then use usual import or require() as you would have done without vue3-sfc-loader.

eg.

          <script>

          import comp from './static/components/Component.vue';

          export default {
            ...
            components: {
              comp,
            },
          };
          </script>
raphael-bresson commented 3 years ago

Franck,

Sorry, I don't really understand ? What do you mean by the first time it is called ? How do I know the function loadComponent has already been called, the component code compiled and I should import it normally ? If I add a console.log in loadComponent, I don't see it being called several times.

Could please update my example with the right way to do ?

Thank you

FranckFreiburger commented 3 years ago

Raphaël,

Use loadModule() only once, when you mount your app.

eg.

<!DOCTYPE html>
<html>
<body>
  <script src="https://unpkg.com/vue@next/dist/vue.runtime.global.prod.js"></script>
  <script src="vue3-sfc-loader.js"></script>
  <script>

    /* <!-- */
    const config = {
      files: {

        '/component.vue': `

          <template>
            <span> {{ field }} </span>
          </template>

          <script>
          export default {
            name: "comp",
            props: {
              field: { type: String, default: "" },
            },
          };
          </script>

        `,

        '/app.vue': `

          <template>
            <div @click="divClick">
              <comp :field="fileName"></comp>
              <br />
              <comp ref="ref1"></comp>
            </div>
          </template>

          <script>

          import comp from './component.vue';

          export default {
            name: "App",
            data() {
              return {
                fileName: "AVANT",
              };
            },
            methods: {
              divClick(event) {
                console.log(this.$refs.ref1.$el);
                this.fileName = "APRES";
              },
            },
            components: {
              comp,
            },
          };
          </script>
        `,

      }
    };
    /* --> */

    const options = {
      moduleCache: { vue: Vue },
      getFile: url => config.files[url],
      addStyle: () => {},
    }

    Vue.createApp(Vue.defineAsyncComponent(() => window['vue3-sfc-loader'].loadModule('/app.vue', options))).mount(document.body);

  </script>
</body>
</html>

Further calls to import or require() are automatically handled by vue3-sfc-loader.

Also note that, if you need, you can continue to use loadModule() in your components, but options.moduleCache must be shared between all calls of loadModule()

HIH

FranckFreiburger commented 3 years ago

what news?

raphael-bresson commented 3 years ago

Sorry, I totally forgot to answer you. I was indeed wondering a few days ago if I had done it. Thank you for your answer. Indeed using loadModule only when mounting my App, on my main component, and using the standard import statement worked and solved my problem. I don't know why I was repeatedly using loadModule. I must have got confused somewhere.

Anyway thanks again and you can close this issue.

Raphael