Access shadow DOM from Vue Component - vue.js

I'm currently working on a custom component with Vue3. I defined it like:
import { defineCustomElement } from 'vue';
import Panel from '~/components/panel_custom/Panel.ce.vue';
const CustomElement = defineCustomElement(Panel);
window.customElements.define('panel-custom', CustomElement);
Now I'm trying to access the shadowRoot to get some scroll-to-bottom function working.
How would I get access to the element to call the shadowRoot from Like: element.shadowRoot;?
I don't find anything in the vue documentation.

From within the Vue component, the shadow root is the parent node, so you could access it via this.$el.parentNode:
export default {
mounted() {
// assuming this Vue component is mounted as a custom element
// via `defineCustomElement()` and `window.customElements.define()`
console.log({ shadowRoot: this.$el.parentNode })
}
}
Or you can query the document for the custom element, and access the shadowRoot property directly from the reference. The following example assumes the custom element exists in the light DOM at the time of query, and not within the shadow DOM. document.querySelector() does not pierce the shadow DOM, so if the element is within another custom element with a closed-mode shadow root, the query will return undefined.
import { defineCustomElement } from 'vue'
import HelloWorld from '#/components/HelloWorld.vue'
const CustomElement = defineCustomElement(HelloWorld)
window.customElements.define('hello-world', CustomElement)
// assume element is in light DOM
const el = document.querySelector('hello-world')
console.log({ shadowRoot: el?.shadowRoot })
demo

Related

Vue.js 3 : Accessing a dynamically created component

I have a vue.js 3 application that needs to dynamically create components and access them.
The proper way to dynamically create them and injected them in the DOM seems to be th wrap them in an app using createApp, and this part works, when I call test() a new copy of the components appears at the bottom of the DOM.
Now I need to be able to get a reference to that component son I can call public (exposed) methods on it.
In my (actual) code below, what would allow me to get a reference to my component ?
import { createApp, h } from "vue";
import ConfirmModal from '#/views/components/ui/confirm-modal.vue';
function test()
{
let componentApp = createApp({
setup() {
return () => h(ConfirmModal, { title: "Fuck yeah!", type: "primary" });
}
});
const wrapper = document.createElement('div');
wrapper.setAttribute('id', 'confirm-modal-0');
componentApp.mount(wrapper);
document.body.appendChild(wrapper);
let _confirmModal: ConfirmModal = ???????????????????
_confirmModal.show();
}
EDIT : Here's my use-case: I have a helper function in a service which is used to confirm actions (like deletes) using a Modal dialog. This helper/service is pure TS, not a Vue component, but needs to instanciate the modal (which is a vue component), have it injected in the DOM and have its public methods callable (by my helper/service).
Right now, the only thing that works is to have a sungle copy of the modal component in my root layout and have to root layout foward the ref to my service, which can then use the component. But this isn't great because I need multiple instances of the dialog, and isn't good SOC to have to root layout handle a modal dialog.

How can I import an other custom component using Vue composition API?

I'm trying to create a Vue 3 component library using composition API:
https://github.com/hyperbotauthor/vue3complib
In one of the components I would like to import an other composition API component ( https://github.com/hyperbotauthor/vue3complib/blob/main/src/components/ChessboardExt.vue ):
import { Perscombo } from "../index"
const PerscomboE = (Perscombo as any).setup
const e = PerscomboE({id: "variant", options: variants}, context)()
const vertContainer = h(
"div",
{
},
[e, outerContainer]
);
This almost works, because the component's node is created with its setup function, and it is even rendered on the page correctly, however its onMounted function does not get called properly and I get the warning
onMounted is called when there is no active component instance to be associated with.
Lifecycle injection APIs can only be used during execution of setup().
If you are using async setup(), make sure to register lifecycle hooks before the first await statement.
Not only a warning, but unfortunately I need this for initializing the component, so it is not fully functional without its onMounted function as it should be persistent and its state cannot be initialized from localStorage.
How do I import an other composition API component into my composition API component's setup properly?
EDIT:
Managed to remove onMounted from the child component and I can pass a callback in props for the case when its state changes. So for this case I solved the issue. In general I still don't know the solution.
You can pass an imported custom component via the h function, like an HTML tag. Event handling works the same way as with HTML elements: You prefix the handler name with on, use camel case, and pass the handler as a property with this name:
const variantCombo = h(Perscombo, {id: props.id + "/variant", options: variants, onPerscombochanged: (event:any) => {
setVariant()
}})
const sizeCombo = h(Perscombo, {id: props.id + "/size", options: sizes, onPerscombochanged: (event:any) => {
resize(event.value)
}})
const upperControls = h(
"div",
{
},
[variantCombo, sizeCombo]
);

Nuxt send data between 2 components

welcome to this topic. i recently tried to use the Nuxt framework to make my web-application but i ran into a problem.
In my default layout i have two components. a header component and a sidebar component. if i click on the hamburger icon in the header component the sidebar needs to get smaller or bigger depending on the hamburger icon state (true or false)
so to make it more complicated i don't want to use a prop to send it through the other component. i want to make it as a template so people can use it easy. can i transform a local component variable to a global variable other components can use?
so the code i have now is like this:
this is the index page
this is the header component
this is the sidebar component
as you can see i trigger the hamburgerstate on the header component page.
i want to access that state in the sidebarcomponent to so i can adjust the sidebar
the one thing that's IMPORTANT is that it needs to be as simple as possible so people who use this template later don't have to add unnecessary work
any possibilities this can work?
The simplest way to achieve a global variable is to set it as a state element and have a mutation for changing it. As your 'hambuger' is a boolean there is no need to pass parameters to the mutation making it all the easier.
You may want to have a named module in you store to handle this but I'll just put it in store/index.js for now.
export const state = () => ({
hamburger: true
})
export const mutations = {
changeHamburger (state) {
state.hamburger = !state.hamburger
}
}
Then in any page or component you can access that state element:
Component.vue
<script>
import { mapMutations } from 'vuex'
export default {
computed: {
hamburger () {
return this.$store.state.hamburger
}
},
methods: {
...mapMutations({
hamburgerChange: 'changeHamburger'
})
}
}
</script>
So this means you can now use the computed property 'hamburger' in your component and can change it by calling 'hamburgerChange', eg <v-btn #click="hamburgerChange">.

Access the component name from a Vue directive

I want to create a custom Vue directive that lets me select components on my page which I want to hydrate. In other words, this is what I want to archive
I render my Vue app on the server (ssr)
I attach a directive to some components, like this:
<template>
<div v-hydrate #click="do-something"> I will be hydrated</div>
</template>
I send my code to the client and only those components that have the v-hydrate property will be hydrated (as root elements) on the client.
I want to achieve this roughly this way:
I will create a directives that marks and remembers components:
import Vue from "vue";
Vue.directive("hydrate", {
inserted: function(el, binding, vnode) {
el.setAttribute("data-hydration-component", vnode.component.name);
}
});
My idea is that in my inserted method write a data-attribute to the server-rendered element that I can read out in the client and then hydrate my component with.
Now I have 2 questions:
Is that a feasible approach
How do I get the component name in el.setAttribute? vnode.component.name is just dummy code and does not exist this way.
PS: If you want to know why I only want to hydrate parts of my website: It's ads. They mess with the DOM which breaks Vue.
I could figure it out:
import Vue from "vue";
Vue.directive("hydrate", {
inserted: function(el, binding, vnode) {
console.log(vnode.context.$options.name); // the component's name
}
});
I couldn't get the name of my single file components using the previously posted solution, so I had a look at the source code of vue devtools that always manages to find the name. Here's how they do it:
export function getComponentName (options) {
const name = options.name || options._componentTag
if (name) {
return name
}
const file = options.__file // injected by vue-loader
if (file) {
return classify(basename(file, '.vue'))
}
}
where options === $vm.$options

how can component delete itself in Vue 2.0

as title, how can I do that
from offical documentation just tell us that $delete can use argument 'object' and 'key'
but I want delete a component by itself like this
this.$delete(this)
I couldn't find instructions on completely removing a Vue instance, so here's what I wound up with:
module.exports = {
...
methods: {
close () {
// destroy the vue listeners, etc
this.$destroy();
// remove the element from the DOM
this.$el.parentNode.removeChild(this.$el);
}
}
};
Vue 3 is basically the same, but you'd use root from the context argument:
export default {
setup(props, { root }){
const close = () => {
root.$destroy();
root.$el.parentNode.removeChild(root.$el);
};
return { close };
}
}
In both Vue 2 and Vue 3 you can use the instance you created:
const instance = new Vue({ ... });
...
instance.$destroy();
instance.$el.parentNode.removeChild(instance.$el);
No, you will not be able to delete a component directly. The parent component will have to use v-if to remove the child component from the DOM.
Ref: https://v2.vuejs.org/v2/api/#v-if
Quoted from docs:
Conditionally render the element based on the truthy-ness of the expression value. The element and its contained directives / components are destroyed and re-constructed during toggles.
If the child component is created as part of some data object on parent, you will have to send an event to parent via $emit, modify (or remove) the data and the child component will go away on its own. There was another question on this recently: Delete a Vue child component
You could use the beforeDestroy method on the component and make it remove itself from the DOM.
beforeDestroy () {
this.$root.$el.parentNode.removeChild(this.$root.$el)
},
If you just need to re-render the component entirely you could bind a changing key value to the component <MyComponent v-bind:key="some.changing.falue.from.a.viewmodel"/>
So as the key value changes Vue will destroy and re-render your component.
Taken from here