karol-f / vue-custom-element

Vue Custom Element - Web Components' Custom Elements for Vue.js
https://karol-f.github.io/vue-custom-element/
MIT License
1.97k stars 187 forks source link

Vue Devtools unable to detect child Vue components when vce used in root #150

Open wbern opened 6 years ago

wbern commented 6 years ago

Hi.

I'm wondering why I cannot inspect Vue components inside the vce on the page.

Is this a limitation with Vue Devtools, or is it something that can be fixed? Happy to hear of any work-arounds if there are any.

Thanks in advance.

karol-f commented 6 years ago

Hi, unfortunately I didn't test it with Vue DevTools, but I can see that indeed custom element's made with this library are not detected.

As this is non-critical issue, I will postpone fixing it. If you want to contribute - feel free to prepare PR. Regards!

wbern commented 6 years ago

They seem to be detected if you refresh the devtools using their refresh button, but the children are just a no go..

karol-f commented 6 years ago

It can be due to timing - e.g. if DevTools check the page for Vue components before Web Component's Custom Elements jump in. But it have to be checked with DevTools source code - https://github.com/vuejs/vue-devtools/tree/dev/src.

ankurk91 commented 4 years ago

I tried it using stand alone dev tool desktop app but no luck. https://www.npmjs.com/package/@vue/devtools

wbern commented 4 years ago

I made a small work-around, but it doesn't seem to work recursively.

Array.from(document.querySelectorAll('*')).filter(el => {
if(el.__vue_custom_element__) {
    el.__vue__ = el.__vue_custom_element__
    delete el.__vue_custom_element__;
    return true;
} else if(el.__vue__) {
    delete el.__vue__
    return true;
}
});

Now I'm trying to make the custom elements inside my root custom element show in devtool as well.

wbern commented 4 years ago

I made a script that seemingly fixes the issue without downsides. I don't know if this will lead to consequences on the Vue-side of things, but Vue Devtools now works fine.

Include this wherever you are serving the static html file and things should work.

<script>
    // problem statement, aka why this hack script exists.
    // Vue Devtools, when populating a tree of vue components, will find a root vue mount point,
    // then traverse down the vue component's "known" vue component children.
    // but for our Custom Element implementation, Vue mount points are created at each custom element.
    // This means that since Vue Devtools only searched from the first known mount point, and the first mount
    // point doesn't know anything about any descendant mount points, vue devtools won't know about them.
    //
    // in storybook, this means that we'll only see the storybook vue component
    // if we were to only mount the custom element, we would only see the custom element's vue instance,
    // but not the descendant custom elements (like atoms etc.).
    // this script aims to fix that. Hopefully Vue Devtools works as intended after this, otherwise we'll have to
    // find more ways to patch it, I guess.
    //
    // by wbern

    var extractVueComponentFromVueMountPoint = c => {
        if (c.constructor.name === 'Vue') {
            return c.$children[0];
        }
        return c;
    };

    var getVueInstance = el => el.__vue__ || el.__vue_custom_element__;

    var walk = treeNode => {
        var traverse = elements => {
            elements.forEach(el => {
                if (el.__vue_custom_element__) {
                    // we found a custom element,
                    // this _could_ be the best way to "ignore" the custom element layer.

                    el.__vue__ =
                        el.__vue_custom_element__.$children[0].$children[0];

                    // shouldn't be necessary
                    // getVueInstance(el).$options.name =
                    //   "Custom Element: " +
                    //   el.tagName.toLowerCase().replace(/^[A-z]/, a => a.toUpperCase());
                }

                if (getVueInstance(el)) {
                    // we found a vue component, extract it,
                    // create a new tree node level,
                    // and finally keep searching from it
                    var childTreeNode = {
                        ref: extractVueComponentFromVueMountPoint(
                            getVueInstance(el),
                        ),
                        tagName: el.tagName.toLowerCase(),
                        children: [],
                    };
                    treeNode.children.push(childTreeNode);

                    walk(childTreeNode);
                } else if (el.children && el.children.length > 0) {
                    // keep looking downwards for vue components
                    traverse(el.children);
                }
            });
        };

        if (treeNode.ref.$el.children) {
            traverse(treeNode.ref.$el.children);
        }
    };

    var repair = treeNode => {
        // now we "restore" the $children reference across the components
        // so that Vue Devtools can find them properly. This is hacky as hell. :-)

        treeNode.children.forEach(childTreeNode => {
            if (!treeNode.ref.$children.includes(childTreeNode.ref)) {
                treeNode.ref.$children.push(childTreeNode.ref);
            }

            // keep traversing downwards
            repair(childTreeNode);
        });
    };

    // initiates everything. can be run multiple times.
    var glueThePage = (rootSelector, forceVueDevtoolsRefresh = false) => {
        var root = extractVueComponentFromVueMountPoint(
            document.querySelector(rootSelector).__vue__,
        );

        var tree = {
            ref: extractVueComponentFromVueMountPoint(root),
            tagName: root.$el.tagName.toLowerCase(),
            children: [],
        };

        // find all the vue components, put in the "tree" variable
        walk(tree);

        // put up links between the vue components, across vue mount points.
        repair(tree);

        let hookIsAvailable = !!window.__VUE_DEVTOOLS_GLOBAL_HOOK__;
        let vueDevtoolsIsOnComponentsTab =
            hookIsAvailable &&
            window.__VUE_DEVTOOLS_GLOBAL_HOOK__.currentTab === 'components';

        if (vueDevtoolsIsOnComponentsTab || forceVueDevtoolsRefresh) {
            if (hookIsAvailable) {
                window.__VUE_DEVTOOLS_GLOBAL_HOOK__.emit('flush');
            } else {
                throw new Error(
                    'Cannot force a refresh in Vue Devtools as it does not seem to be available.',
                );
            }
        }
    };

    // this is the root element of where your first Vue mount point is
    // for storybook, it should be 'body > #root'
    var myRootElementSelector = 'body > #root';

    // lets us adapt to changes, and makes sure we don't do scanning unnecessarily early.
    window.addEventListener(
        'DOMContentLoaded',
        () => {
            // check if the root element is already available
            if (
                document.querySelector(myRootElementSelector) &&
                document.querySelector(myRootElementSelector).__vue__
            ) {
                glueThePage(myRootElementSelector);
            }

            // listen for future changes
            var subscriber = new MutationObserver((mutations, observer) => {
                mutations.forEach(mutation =>
                    mutation.addedNodes.forEach(node => {
                        let isRootElement =
                            document.querySelector(myRootElementSelector) ===
                            node;

                        let isVueInstance = !!node.__vue__;
                        let isCustomElement = !!node.__vue_custom_element__;

                        if (
                            isRootElement ||
                            isVueInstance ||
                            isCustomElement
                        ) {
                            glueThePage(myRootElementSelector);
                        }
                    }),
                );
            }).observe(document.body, { childList: true, subtree: true });
        },
        false,
    );
</script>

Edit: Updated the script to work a little better.

johannesss commented 4 years ago

I made a script that seemingly fixes the issue without downsides. I don't know if this will lead to consequences on the Vue-side of things, but Vue Devtools now works fine.

Include this wherever you are serving the static html file and things should work.

<script>
    // ..
</script>

Edit: Updated the script to work a little better.

Thank you so much for this, much appreciated!

kinoli commented 3 years ago

I made a script that seemingly fixes the issue without downsides. I don't know if this will lead to consequences on the Vue-side of things, but Vue Devtools now works fine.

Include this wherever you are serving the static html file and things should work.

<script>
    // problem statement, aka why this hack script exists.
    // Vue Devtools, when populating a tree of vue components, will find a root vue mount point,
    // then traverse down the vue component's "known" vue component children.
    // but for our Custom Element implementation, Vue mount points are created at each custom element.
    // This means that since Vue Devtools only searched from the first known mount point, and the first mount
    // point doesn't know anything about any descendant mount points, vue devtools won't know about them.
    //
    // in storybook, this means that we'll only see the storybook vue component
    // if we were to only mount the custom element, we would only see the custom element's vue instance,
    // but not the descendant custom elements (like atoms etc.).
    // this script aims to fix that. Hopefully Vue Devtools works as intended after this, otherwise we'll have to
    // find more ways to patch it, I guess.
    //
    // by wbern

    var extractVueComponentFromVueMountPoint = c => {
        if (c.constructor.name === 'Vue') {
            return c.$children[0];
        }
        return c;
    };

    var getVueInstance = el => el.__vue__ || el.__vue_custom_element__;

    var walk = treeNode => {
        var traverse = elements => {
            elements.forEach(el => {
                if (el.__vue_custom_element__) {
                    // we found a custom element,
                    // this _could_ be the best way to "ignore" the custom element layer.

                    el.__vue__ =
                        el.__vue_custom_element__.$children[0].$children[0];

                    // shouldn't be necessary
                    // getVueInstance(el).$options.name =
                    //   "Custom Element: " +
                    //   el.tagName.toLowerCase().replace(/^[A-z]/, a => a.toUpperCase());
                }

                if (getVueInstance(el)) {
                    // we found a vue component, extract it,
                    // create a new tree node level,
                    // and finally keep searching from it
                    var childTreeNode = {
                        ref: extractVueComponentFromVueMountPoint(
                            getVueInstance(el),
                        ),
                        tagName: el.tagName.toLowerCase(),
                        children: [],
                    };
                    treeNode.children.push(childTreeNode);

                    walk(childTreeNode);
                } else if (el.children && el.children.length > 0) {
                    // keep looking downwards for vue components
                    traverse(el.children);
                }
            });
        };

        if (treeNode.ref.$el.children) {
            traverse(treeNode.ref.$el.children);
        }
    };

    var repair = treeNode => {
        // now we "restore" the $children reference across the components
        // so that Vue Devtools can find them properly. This is hacky as hell. :-)

        treeNode.children.forEach(childTreeNode => {
            if (!treeNode.ref.$children.includes(childTreeNode.ref)) {
                treeNode.ref.$children.push(childTreeNode.ref);
            }

            // keep traversing downwards
            repair(childTreeNode);
        });
    };

    // initiates everything. can be run multiple times.
    var glueThePage = (rootSelector, forceVueDevtoolsRefresh = false) => {
        var root = extractVueComponentFromVueMountPoint(
            document.querySelector(rootSelector).__vue__,
        );

        var tree = {
            ref: extractVueComponentFromVueMountPoint(root),
            tagName: root.$el.tagName.toLowerCase(),
            children: [],
        };

        // find all the vue components, put in the "tree" variable
        walk(tree);

        // put up links between the vue components, across vue mount points.
        repair(tree);

        let hookIsAvailable = !!window.__VUE_DEVTOOLS_GLOBAL_HOOK__;
        let vueDevtoolsIsOnComponentsTab =
            hookIsAvailable &&
            window.__VUE_DEVTOOLS_GLOBAL_HOOK__.currentTab === 'components';

        if (vueDevtoolsIsOnComponentsTab || forceVueDevtoolsRefresh) {
            if (hookIsAvailable) {
                window.__VUE_DEVTOOLS_GLOBAL_HOOK__.emit('flush');
            } else {
                throw new Error(
                    'Cannot force a refresh in Vue Devtools as it does not seem to be available.',
                );
            }
        }
    };

    // this is the root element of where your first Vue mount point is
    // for storybook, it should be 'body > #root'
    var myRootElementSelector = 'body > #root';

    // lets us adapt to changes, and makes sure we don't do scanning unnecessarily early.
    window.addEventListener(
        'DOMContentLoaded',
        () => {
            // check if the root element is already available
            if (
                document.querySelector(myRootElementSelector) &&
                document.querySelector(myRootElementSelector).__vue__
            ) {
                glueThePage(myRootElementSelector);
            }

            // listen for future changes
            var subscriber = new MutationObserver((mutations, observer) => {
                mutations.forEach(mutation =>
                    mutation.addedNodes.forEach(node => {
                        let isRootElement =
                            document.querySelector(myRootElementSelector) ===
                            node;

                        let isVueInstance = !!node.__vue__;
                        let isCustomElement = !!node.__vue_custom_element__;

                        if (
                            isRootElement ||
                            isVueInstance ||
                            isCustomElement
                        ) {
                            glueThePage(myRootElementSelector);
                        }
                    }),
                );
            }).observe(document.body, { childList: true, subtree: true });
        },
        false,
    );
</script>

Edit: Updated the script to work a little better.

Does this still work? Its giving me errors and can't seem to make it work. Using Vue 2.