All: Using Primevue inside a custom element
Describe the bug
I am having difficulty using Primevue inside a custom element (webcomponent). The css and js for primevue gets loaded inside the html header and not inside the shadow dom/root. Would like to only load the js and css needed for the custom element within it's shadow root.
Reproducer
PrimeVue version
latest
Vue version
3.x
Language
ALL
Build / Runtime
Vite
Browser(s)
No response
Steps to reproduce the behavior
No response
Expected behavior
No response
Apart from CSS that is injected in the body, I also have a problem with dialogs, autocomplete lists, combo boxes, etc (all floating containers) that is injected in the body of the html. When a custom element is used, it is supposed to live in the shadow root and not the body.
And if I want to export the custom element then there should be a way to install PrimeVue inside custom element as well, I wouldn't want host application to install it.
Currently if I export my custom element, it complains Property "$primevue" was accessed during render but is not defined on instance.
I met the same problem when I wanted to add content-script(use shadow dom,It doesn't work properly now, so I have to give it up) to the chrome extension,
looking for a solution to same , trying to use it in a web extension
looking for a solution to same , trying to use it in a web extension
I made this content script method that copies all PrimeVue style elements from the <head> (and keeps the variable defining style elements in the <head>).
We need to copy, not move because things like dropdowns are injected outside the shadow dom by PrimeVue.
This worked for my extension...
export function movePrimeVueStyles(shadowDOM: ShadowRoot | HTMLElement) {
// Observe if any new styles are added by PrimeVue
observeHeadForStyles(shadowDOM)
const primeStyles = document.querySelectorAll('head > style[type="text/css"][data-primevue-style-id]')
// Move all styles that aren't for definining variables into the shadow dom
primeStyles.forEach((styleEl) => {
if (styleEl.id && !styleEl.id.includes('variables')) {
const clonedStyleEl = styleEl.cloneNode(true) as HTMLStyleElement
shadowDOM.prepend(clonedStyleEl)
}
})
}
function observeHeadForStyles(shadowDOM: ShadowRoot | HTMLElement) {
// Create a MutationObserver to watch for new <style> elements in <head>
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node instanceof HTMLStyleElement && node.hasAttribute('data-primevue-style-id')) {
const styleId = node.getAttribute('data-primevue-style-id')
if (styleId && !styleId.includes('variables')) {
// Move other styles to the shadow DOM
const clonedStyleEl = node.cloneNode(true) as HTMLStyleElement
shadowDOM.appendChild(clonedStyleEl)
}
}
})
})
})
// Observe changes to the <head> element
observer.observe(document.head, { childList: true, subtree: false })
}
Will this be solved anytime soon? Is there an "approved" workaround for the moment? I don't know how to inject the PrimeVue css into the custom element. All the solutions found so far mentioned including the primevue/resources/primevue.min.css but this appears to be deprecated now. Please help!
Will this be solved anytime soon? Is there an "approved" workaround for the moment? I don't know how to inject the PrimeVue css into the custom element. All the solutions found so far mentioned including the primevue/resources/primevue.min.css but this appears to be deprecated now. Please help!
Have you tried my workaround yet?
Will this be solved anytime soon? Is there an "approved" workaround for the moment? I don't know how to inject the PrimeVue css into the custom element. All the solutions found so far mentioned including the primevue/resources/primevue.min.css but this appears to be deprecated now. Please help!
Have you tried my workaround yet?
Yes, it worked fine! Thank you!
Will this be solved anytime soon? Is there an "approved" workaround for the moment? I don't know how to inject the PrimeVue css into the custom element. All the solutions found so far mentioned including the primevue/resources/primevue.min.css but this appears to be deprecated now. Please help!
Have you tried my workaround yet?
Where does this code go and how do you call it?
Will this be solved anytime soon? Is there an "approved" workaround for the moment? I don't know how to inject the PrimeVue css into the custom element. All the solutions found so far mentioned including the primevue/resources/primevue.min.css but this appears to be deprecated now. Please help!
Have you tried my workaround yet?
Where does this code go and how do you call it?
Anywhere in your app that has access to the DOM, for chrome extensions that is the content script, for regular websites you can call it just about anywhere. Here it is broken down if you are not very familiar with typescript/javascript:
1: Add the code to your project
Make a new ts file, I will call it prime-workaround.ts and paste my code into it, and then import the file.
(you can just remove the export keyword and put it where you want to call it without creating a new file),
2: Find the shadow root element
You probably already have a variable that you use to store the shadow root. However if you don't you can use querySelector to find it.
3: Call movePrimeVueStyles with the shadow root variable as the parameter.
I stumbled upon another issue when using this method:
- In my custom element I'm customizing let's say the primary color to red. -> My buttons will display the text in red inside the custom component
- In the parent app that is using my custom element, I also use Primevue and in this app the primary color is set to white -> Also the buttons used in this parent app will have the text color white.
ISSUE: 1. the PrimeVue css for the Button component gets override because it is used in both places. Does anybody know how I can prevent this? 2. When moving the styles into the shadow-root, I might move also the styles from the parent app, not only the one I need in the custom element.
@patrikb42222 Any help would be highly appreciated!
This is supposed to be fixed according to https://github.com/primefaces/primevue/issues/3037 but i think the change wasn't applied in the 4.x branch.
@1Map, support for custom elements, that should fix all of your problems, was added in following MRs https://github.com/primefaces/primeuix/pull/47 and https://github.com/primefaces/primevue/pull/7351, but for the time being @cagataycivici rejected to merge those changes, and I am planning to release this support in a fork.
@1Map, support for custom elements was implemented and published in a fork here https://www.npmjs.com/search?q=@primeuix-ce and here https://www.npmjs.com/search?q=@primevue-ce.
Fork will be kept up to date with the upstream repositories.
@montgomery1944 Hi, I didn’t quite understand — how do I use this? I even implemented the solution shown by the friend above (@patrikb42222 ), the components work, but dark mode doesn’t work.
@capoia, here is a Gist showing how to use the fork: https://gist.github.com/montgomery1944/4616643bca5854b0bfcf914ea3873b40. No other changes are needed to make it work, in case of any questions, please let me know.
@montgomery1944 Hmm, my use case seems to be different. I'm trying to use PrimeVue with the package for building Chrome extensions called WXT. WXT creates a shadow root and everything is done inside it in an isolated way. However, since PrimeVue always uses document.body, it ends up referencing the body of the site where the extension is installed (in this case, WhatsApp). Am I correct?
It would be much smarter to target the closest head to where the Vue app is mounted. I don't understand why this feature was blocked.
But the strangest part is that copying those styles into the shadow root works — except for dark mode. I believe it might have something to do with :host and :root...
@capoia, issues that you are mentioning, like attaching to shadow root and not using document.body or supporting the :host selector, were fixed in the https://www.npmjs.com/search?q=@primevue-ce fork. The only difference from using the baseline PrimeVue is that you need to use the wrapCustomElement function, which is shown here: https://gist.github.com/montgomery1944/4616643bca5854b0bfcf914ea3873b40.
The full list of the issues fixed to support custom elements can be seen here: https://github.com/primefaces/primeuix/pull/47 and here: https://github.com/primefaces/primevue/pull/7351.
I also don't fully understand why those pull requests were rejected, but I think that the interest in support for custom elements is not big enough for PrimeVue team to review and accept those pull requests, knowing that those are quite huge, since some of the fundamental parts of PrimeVue were written in an incompatible way with custom elements.
@montgomery1944 I see. I'm trying to get it working here with WXT but haven't had success yet — I'll keep trying! If it's not too much to ask, could you provide an example of how it would work with WXT? I made a gist with the base WXT config in case you want or are able to help: https://gist.github.com/capoia/960fe8cabf666ad341dc3a0a584375de
@capoia, indeed, you have a different set up here, but you can still use the fork to work with shadow root by adding one line to the code:
const app = createApp(App)
.provide('appContainer', container)
app._root = shadow
I tested it locally in WXT project and it works.
Hello @montgomery1944 , thank you so much for getting back to me — I managed to make it work now!
It's very curious that it says the _root property doesn't exist, but it worked anyway!!
I just have a few questions:
Dialogs/Popovers are still rendered outside of the shadow DOM unless I use append-to. Is that the expected behavior?
The same goes for the Drawer component — since it doesn't have an appendTo option, it ends up not working properly and gets rendered in the main page (outside the shadow DOM).
If I import ToastService from 'primevue/toastservice', it doesn't throw any errors, but the toast doesn't render. However, if I import it from '@primevue-ce/primevue/toastservice', I get the error: Uncaught (in promise) Error: No PrimeVue Toast provided!
I'd also like to know if there's any repository for @primevue-ce, so I don't flood this space with questions/fixes.
@capoia,
Starting with @primevue-ce/primevue version 4.3.7 components using Portal will properly append the elements to Shadow DOM instead of body.
I wasn't able to reproduce the issue with "Uncaught (in promise) Error: No PrimeVue Toast provided" when following properly the documentation from https://primevue.org/toast/ using "@primevue-ce/primevue".
Here are the repositories where we store forked version: https://github.com/Cambridge-Academic-Technology/primeuix and https://github.com/Cambridge-Academic-Technology/primevue. Feel free to raise any issues there.
@1Map I faced the same issue when using primevue in custom elements, but the solution proposed in #8253 is simpler than the one proposed in #7351 by @montgomery1944. For handling the issue mentioned by @capoia when using components that use the Portal, the solutions are to copy the styles to the document head or add a template ref inside the custom element and pass it to the portal appendTo prop, for example:
<script setup lang="ts">
import { ref, useTemplateRef } from 'vue'
import { Button, Dialog } from 'primevue'
const visible = ref(false)
const portal = useTemplateRef('portal')
const openDialog = () => {
visible.value = true
}
const closeDialog = () => {
visible.value = false
}
</script>
<template>
<div class="fixed right-8 bottom-8">
<Button label="Open Dialog" @click="openDialog" raised size="large" />
<Dialog
:visible="visible"
:appendTo="portal"
header="I'm a dialog"
@update:visible="closeDialog"
class="w-full max-w-md"
>
<p class="f-body-md text-surface-700">This is the dialog body</p>
<template #footer>
<Button label="Close" @click="closeDialog" outlined />
</template>
</Dialog>
</div>
<div ref="portal"></div>
</template>
We love PrimeVue, but we are migrating to WebComponents and PrimeVue currently gives us headaches. The work around, do work, but are far from perfect (rather bloated!). There is a fork available that fixes this, but we want to remain on the main branch.