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

Parent is mounted before child is mounted #21

Closed ITrun90 closed 3 years ago

ITrun90 commented 3 years ago

Describe the bug

The "mounted"-hook of a parent is called before "mounted"-hook of child.

I have a Component "MyParent" and a Component "MyChild". There is literally nothing in it just in the "mounted-hook" a console.log("Parent is mounted") and console.log("Child is moutned").

The result is: "Parent is mounted" "Child is mounted"

but i would expect that the result is "Child is mounted" "Parent is mounted"

To Reproduce Write the two SFCs. Maybe do something in the child that cost a bit time. Run a console.log() in both and check the order.

Expected behavior I expect that the child is mounted before the parent is mounted. Here is what i expect: https://codesandbox.io/s/peaceful-benz-m0ljx?file=/src/App.vue:275-318

Versions

FranckFreiburger commented 3 years ago

Hi ITrun90,

with the following component hierarchy :

main
  foo
    bar

I get the following result :

main created
foo created
bar created
bar mounted
foo mounted
main mounted

Isn't this expected ?

ITrun90 commented 3 years ago

Hi Franck,

thank your for reply :)

This would be the expected output. In my example, it is vice versa. Maybe there is a mistake i made, but i followed your instructions in the examples of your documentation.

I guess we can figure it out by reviewing my code.

This is my HTML-Structure:

<div id="test">
    <lbsite id="site">
        <lbcontainer id="container">
            Test
        </lbcontainer>
    </lbsite>
</div>

And here you find my javascript:

<script>
    const options = {
        moduleCache: {
            vue: Vue,
        },
        async getFile(url) {
            const res = await fetch(url);
            if ( !res.ok )
              throw Object.assign(new Error(url+' '+res.statusText), { res });
            return await res.text();
          }
    };

    const defaultOptions = { 
        data: function() {
            return {
                eventBus
            }
        },
        delimiters: ['%%', '%%']
    };

    const lbsite = loadModule('js/components/site.vue', options);
    const lbspinner = loadModule('js/components/spinner.vue', options);
    const lbcontainer = loadModule('js/components/container.vue', options);

    var newApp = createApp(defaultOptions);
    newApp.component("lbsite", Vue.defineAsyncComponent( () => lbsite ));
    newApp.component("lbcontainer", Vue.defineAsyncComponent( () => lbcontainer ));
    newApp.component("lbspinner", Vue.defineAsyncComponent( () => lbspinner ));
    newApp.mount("#test");
</script>

Here are the important Vue-SFCs:

site.vue (lbsite)

<template>
    <div :id="id">
        <lbspinner v-show="loading"></lbspinner>
        <div :class="[{hide_content: loading}]">
            <slot></slot>
        </div>
    </div>
</template>
<script>
    module.exports = {
        "props": {
            "id": {
                type: String,
                required: true
            }
        }, 
        "data": function() {
            return {
                "loading": true
            }
        },
        "mounted": function() {
            this.loading = false;
            console.log("Mount: site");
        }
    }
</script>

container.vue (lbcontainer)

<template>
    <div :id="containerID">
        <slot></slot>
    </div>
</template>

<script>
    module.exports = {
        props: {
            "id": {
                type: String,
                default: "lbcontainer" 
            }
        },
        data: function() {
            return {
                "containerID": this.id
            }
        },
        mounted: function() {
            console.log("Mount: Container");
        },
        methods: {

        }
    }
</script>

The output i would expect is: Mount: Container Mount: Site

but the output i get is: Mount: Site Mount: Container

Maybe this will help.

FranckFreiburger commented 3 years ago

Hi,

A simplified version of your example (please adjust if needed) without vue3-sfc-loader :

<!DOCTYPE html>
<html>
<body>
  <script src="https://unpkg.com/vue@next/dist/vue.global.js"></script>
  <script>
    Vue.createApp({
        template: '<foo><bar>test</bar></foo>',
        components: {
            foo: {
                template: 'foo (<slot></slot>)',
                created: () => console.log('foo created'),
                mounted: () => console.log('foo mounted'),
            },
            bar: {
                template: 'bar (<slot></slot>)',
                created: () => console.log('bar created'),
                mounted: () => console.log('bar mounted'),
            },
        },
        created: () => console.log('main created'),
        mounted: () => console.log('main mounted'),
    }).mount(document.body);
  </script>
</body>
</html>

output:

foo (bar (test))

console:

main created
foo created
bar created
bar mounted
foo mounted
main mounted
ITrun90 commented 3 years ago

Hi Franck,

thank you for your reply.

Your example is working as expected, but everytime i use vue3-sfc-loader the parent is mounted before the children. That's why i opened this issue.

FranckFreiburger commented 3 years ago

Same example with vue3-sfc-loader:

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

    /* <!-- */
    const config = {
      files: {
        '/main.vue': `
            <template>
                <foo><bar>test</bar></foo>
            </template>
            <script>
                import foo from './foo.vue'
                import bar from './bar.vue'

                export default {
                    components: {
                        foo,
                        bar,
                    },
                    created: () => console.log('main created'),
                    mounted: () => console.log('main mounted'),
                }
            </script>
        `,

        '/foo.vue': `
            <template>
                foo (<slot></slot>)
            </template>
            <script>
                export default {
                    created: () => console.log('foo created'),
                    mounted: () => console.log('foo mounted'),
                }
            </script>
        `,

        '/bar.vue': `
            <template>
                bar (<slot></slot>)
            </template>
            <script>
                export default {
                    created: () => console.log('bar created'),
                    mounted: () => console.log('bar mounted'),
                }
            </script>
        `
      }
    };
    /* --> */

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

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

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

output:

foo (bar (test))

console:

main created
foo created
bar created
bar mounted
foo mounted
main mounted

It would seem that the result is exactly the same with and without vue3-sfc-loader.

FranckFreiburger commented 3 years ago

... hovever :

<!DOCTYPE html>
<html>
<body>
  <script src="https://unpkg.com/vue@next/dist/vue.global.js"></script>
  <script>

    const app = Vue.createApp({
        template: '<foo><bar>test</bar></foo>',
        created: () => console.log('main created'),
        mounted: () => console.log('main mounted'),
    })

    app.component("foo", Vue.defineAsyncComponent({
        loader: () => Promise.resolve({
            template: 'foo (<slot></slot>)',
            created: () => console.log('foo created'),
            mounted: () => console.log('foo mounted'),
        })
    }));

    app.component("bar", Vue.defineAsyncComponent({
        loader: () => Promise.resolve({
            template: 'bar (<slot></slot>)',
            created: () => console.log('bar created'),
            mounted: () => console.log('bar mounted'),
        })
    }));

    app.mount(document.body);

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

output:

foo (bar (test))

console:

main created
main mounted
foo created
foo mounted
bar created
bar mounted
FranckFreiburger commented 3 years ago

I'm closing this as answered. Discussion can continue if needed.

ITrun90 commented 3 years ago

Hi Franck,

thanks for your help. I got an answer from the vue-team. They say that this is expected behavior when we use „defineAsyncComponent“. Is there a way how I can use your Loader without Vue.defineAsyncComponent? Maybe a non-async way?

FranckFreiburger commented 3 years ago

Yes, look closer to this : https://github.com/FranckFreiburger/vue3-sfc-loader/issues/21#issuecomment-786463201