Search code examples
cssangularweb-componentangular-elements

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

  • Here's what I ended up doing:

    1. Remove the site's global style.scss from angular.json for the sub-project.

    2. 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.

    3. 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.

    4. 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.