Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wbern opened this issue Oct 22, 2018 · 8 comments
Open

Comments

@wbern
Copy link

wbern commented Oct 22, 2018

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
Copy link
Owner

karol-f commented Oct 22, 2018

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
Copy link
Author

wbern commented Oct 22, 2018

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

@karol-f
Copy link
Owner

karol-f commented Oct 22, 2018

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
Copy link

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

@wbern
Copy link
Author

wbern commented Feb 10, 2020

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
Copy link
Author

wbern commented Feb 12, 2020

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
Copy link

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
Copy link

kinoli commented Oct 21, 2021

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants