How can we make external stylesheets load in the shadow DOM?

44 Views Asked by At

I built a documentation site with Angular 16 for a new CSS utility I created for responsive design. In order to demonstrate the responsive features I created a component with a resizable <iframe> that I embed the demonstration components into. I do this to take advantage of the <iframe>'s separate DOM instance so the demonstrations respond to the size of the <iframe> rather than having to resize the entire browser window which makes it convenient for the users.

I have two stylesheets that need to be used, one for my CSS utility and the other for all my color definitions. The only way I've been able to get them to work is by importing them into the main styles.css of my angular project and appending it to the head of the <iframe>.

The code for my component looks like this minus a few things that are irrelevant to the question.

@Component({
  selector        : 'responsive-window',
  standalone      : true,
  imports         : [ CommonModule ],
  templateUrl     : './responsive-window.component.html',
  styleUrls       : ['./responsive-window.component.css'],
  changeDetection : ChangeDetectionStrategy.OnPush
})

export class ResponsiveWindowComponent implements OnInit, AfterViewInit, AfterContentChecked, OnDestroy {
    
    /* I pass the components I want to embed through this Input */
    @Input() Demonstration! : Type<Component>;

    /* I use this to target the iframe in the template */
    @ViewChild('domFrame', {static: true, read: ElementRef}) DomFrame! : ElementRef;

    constructor(
        private cdr   : ChangeDetectorRef,
        private vcRef : ViewContainerRef,
        private zone  : NgZone
    ){}

    ngAfterViewInit(): void {

        /* embeds the component into the iframe after the view is loaded */
        this.embedContent();

    }

    public embedContent(): void{
      /* targets the iframe */
      const frame = this.DomFrame?.nativeElement.contentDocument || this.DomFrame?.nativeElement.contentWindow;

      /* resolves the component */
      const item : ComponentRef<any> = this.vcRef.createComponent<Component>(this.Demonstration);

      /* for setting up a broadcast channel to send data back and forth from outside the iframe */
      if(reflectComponentType(this.Demonstration)?.inputs.find(a=> a.propName === 'BroadcastName') !== undefined){
        item.instance.BroadcastName = this.BroadcastName;
        item.instance.ChannelName   = this.ChannelName;
        item.instance.TargetName    = this.TargetName;
      }

      /* for the default styling of the iframe's body element */
      const defaultStyles = document.createElement('style');
     
      /* for attaching the main stylesheet of the angular app */
      const stylesLink     = document.createElement('link');

      defaultStyles.innerText = `*{ padding : 0; margin : 0; box-sizing: border-box; overflow: hidden; } body{ display : grid; place-items: center; min-height: 0px; max-height: 100vh; grid-template-columns: 1fr; grid-template-rows: 1fr; background-color: hsl(var(--gray-100), 1); }`;
    
      stylesLink.rel = 'stylesheet';
      stylesLink.type = 'text/css';
      stylesLink.href = 'styles.css';

      /* embedding everything into the iframe */
      frame.head.appendChild(defaultStyles);
      frame.head.appendChild(stylesLink);
      frame.body.appendChild(item.location.nativeElement);
    }
}

As far as the components I pass into this component to be embedded into the <iframe>, this is an example of how they're set up.

@Component({
  selector      : 'app-example',
  standalone    : true,
  imports       : [CommonModule],
  templateUrl   : './example.component.html',
  styleUrls     : [ './example.component.css' ],
  encapsulation : ViewEncapsulation.ShadowDom
})
export class ExampleComponent {

}

I have to set the encapsulation to ShadowDom otherwise none of the component's styling will take effect nor will it read any of the styles from the styles.css file embedded into the <head>. When running ng serve everything works perfectly, however when I run ng-build and load the files into my c-panel and go to the site, none of the styling from the styles.css file take effect on the component embedded into the<iframe>.

I changed a few styles in the embedded component's stylesheet just to see if they would take effect and they did which means it recognizes styles in the component's stylesheet, but when looking at some of the definitions that come from my stylesheets with all the colors and my CSS utility, which are CSS variables, none of them were defined.

I attempted to import those stylesheets into my component's stylesheet and that didn't work. Then I tried importing them in the metadata like this.

@Component({
  selector: 'app-product',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './product.component.html',
  styleUrls: [
    '../../../../assets/utility-styles.css',
    '../../../../assets/color-defs.css',
    './product.component.css'
],
  encapsulation: ViewEncapsulation.ShadowDom
})

I made sure to import the other stylesheets first before the copmonent's stylesheet so all the values would be defined before they're used. I ran ng build again and loaded the files into my c-panel and that didn't work either. I also added those extra stylesheets to the styles array in my angular.json file to see if that would allow me to import them into the embedded components and that failed to work as well.

In the responsive-window component you'll notice there's a part of my code that makes reference to a BroadcastName, ChannelName and TargetName Inputs, those are for setting up a broadcast channel so users can experiment with the different settings which will change the value of some CSS properties in the embedded component from a dashboard outside of the <iframe>. I don't know yet if that's effected as well but I am concerned about it. Does anybody know how I can resolve this issue?

1

There are 1 best solutions below

2
Ricudo On

These two urls won't work after build the project.

'../../../../assets/utility-styles.css',
'../../../../assets/color-defs.css',

Because assets will have different path relative to the root URL of your deployed application. You should use:

'assets/utility-styles.css',
'assets/color-defs.css',