'How to encapsulate Angular app's global CSS within exported web component?
Goal
I have an Angular 11 site with a large form component using Angular Material -- let's call it myExportableComponent
. I want to use myExportableComponent
in my site like normal, but also export it as a generic web component (a.k.a. "custom element", a.k.a. "Angular Element"). That way, it can be used on any 3rd party site, regardless of their technology stack, and it would work the exact same way it does on my site.
myExportableComponent
should work just like a normal Angular component within my site - it should be styleable via both global CSS found in styles.scss
and component-specific CSS found in my-exportable.component.scss
.
When used as a web component, myExportableComponent
should also be affected by global and component-specific CSS like normal, but none of that CSS should leak out and affect the rest of the 3rd party page on which it is placed. This is the part I'm struggling with.
The Setup
I have myExportableComponent
set up in its own sub-project of my main project. I can build it separately and take the generated js/css files and successfully use the web component in another site. Something like this:
<link rel="stylesheet" href="myExportableComponentStyles.css">
<script type="text/javascript" src="myExportableComponent.js"></script>
<div class="mat-app-background mat-typography">
<my-exportable-component></my-exportable-component>
</div>
I've made myExportableComponent
share all style with my main site by going in angular.json
, and pointing it at the main app's theme.scss
file (generated Angular material theme) and styles.scss
(other global styles). Something like this...
angular.json:
"mainApp": {
...
"styles": [
"src/theme.scss",
"src/styles.scss"
],
"myExportableComponentApp": {
...
"styles": [
"src/theme.scss", //NOT "projects/myExportableComponentApp/src/theme.scss"
"src/styles.scss" //NOT "projects/myExportableComponentApp/src/styles.scss"
],
The Problem
Global CSS is not encapsulated when the web component is used on another site. For example, if I add a global link style for my app in styles.scss
, it affects all links on the consuming site, too. The theme.scss
(Material) styles don't seem to leak out, but I think that's because that stuff uses custom selectors already. I fear it would leak out if the consuming site happened to use Angular Material as well.
Idea #1: encapsulation settings
I thought to try using encapsulation: ViewEncapsulation.ShadowDom
on myExportableComponent
but A) that breaks some of the Material styling and icons, and B) it's not really what I want anyway because that is component-level encapsulation and my problem is with global CSS. And actually, my component-specific CSS already seems to be encapsulated with the default setting.
Idea #2: target global CSS to my HTML only
If I added a class to my app's HTML tag and had consumers of the web component add it around the component as well, I could target all my global CSS rules to only affect things inside those containers, like:
.special-wrapper a {
/* some style*/
}
This actually does work to encapsulate my global styles from the 3rd party page, but it somehow flips the styles around so that global styles take precedence over component-specific styles when they shouldn't.
Idea #3: something with :host or :host-context selectors on my global CSS?
Honestly, I'm having a hard time grasping these, and I couldn't get them to do anything in my app.
Idea #4: Build-time scripts?
When the sub-project for myExportableComponent
is built, if I could automatically cut all global CSS from theme.scss
and styles.scss
and paste it at the top of my-exportable.component.scss
, I think that would work. My "global" styles would then be encapsulated to my web component because they'd be in the component-specific file.
Is it possible to do something like this when I run ng build myExportableComponentApp --prod
? I wouldn't know where to start.
I'm open to suggestions!
Solution 1:[1]
Here's what I ended up doing:
Remove the site's global
style.scss
fromangular.json
for the sub-project.Create a new component that inherits from
myExportableComponent
, but otherwise has no code. It also does not have its own template -- it shares that of the base component. You don't actually even need to export this component from the module in the sub-project. That way the main app can only use the base one by default, which is good.The new child component's styleUrls has two files. First is a component-specific stylesheet. It's set up like normal, but inside it all it has is an import referencing the main site's global stylesheet, something like
@import '../../../../../../src/app/styles.scss';
. I wanted to reference this directly but kept getting odd build errors, so this trick of importing it inside a "normal" component-specific stylesheet works. The second file is a reference to the base component's normal component-specific stylesheet.When doing
createCustomElement
for the web component, use the child component instead of the parent.
The bottom line is that when building the generic web component, the main site's global css will be included at the top of the component-specific css for myExportableComponent
, instead of globally. The global styles are then encapsulated, and can also still be overridden by component-specific styles like normal.
I wanted to do the same with theme.scss
containing the Material theme, but it seems to only work if it's truly in the global scope (https://github.com/angular/components/issues/3386). For now this isn't a dealbreaker for me.
PROs:
- No special setup or disruption of main site project. It and
myExportableComponent
can be worked with in a totally standard way within our main app. - Style encapsulation works the same way in exported web component as it does in our main project, and does not leak out to 3rd party sites.
- No special build scripts needed
CONs:
- It's a bit unorthodox, and you need to make sure other developers don't put anything in the dummy/inherited files that serve as the plumbing for this build solution.
- Not yet possible for Material theme css, as far as I can tell.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 |