3

Is there a way to add a CSS class (say .angular-app) to every single HTML element that Angular (as in Angular 4) adds to the DOM? By "every single HTML element" I also mean cases of children and grand children and so on in the templates as well as transcluded content, i.e. not just Angular components/directives/hosts.

Background: I'm working on a huge monolithic AngularJS project with hundreds of thousands of lines of code (including tens of thousands of lines of CSS). Now, some AngularJS components of the project are supposed to be upgraded to Angular. Unfortunately, a lot of the styles of the surrounding AngularJS project leak into the Angular components and clash with styles we want to use there. One solution we're therefore considering is to add a :not(.angular-app) selector to every single selector of the AngularJS styles (via SASS or some post-processing Python script), so that they don't match any elements used within the Angular components.

Is there any way to pull this off?

Side note: Obviously this problem could easily be solved by using all: revert in an Angular component's CSS or by creating a ShadowDOM but since these techniques are barely supported by browsers yet, we need an intermediate solution. (For completeness, yet another solution would be to add a :not() selector to every selector of the AngularJS stylesheet that makes sure that we're not within an Angular component but this would require CSS 4 selectors.)

[EDIT]: I suppose this could be done by writing a custom renderer for the Angular components (see these links) but I'm not entirely sure how. Are e.g. createElement and appendElement used by Angular itself for every HTML element it processes?

balu
  • 3,500
  • 4
  • 34
  • 35

1 Answers1

2

The article you linked is a bit outdated and the implementation changed slightly.

If you want to implement a completely arbitrary implementation of the Renderer2 it is possible to do by simply implementing the RendererFactory2 with createRenderer method that returns your custom renderer.

export class MyRenderer implements Renderer2 {
    createElement(name: string, namespace?: string) {
        const el = document.createElement(name);
        el.setAttribute(name, 'marking Angular component');
        return el;
    }
    ...


export class MyRendererFactory implements RendererFactory2 {
    createRenderer(hostElement): Renderer2 {
        return new MyRenderer();
    }
    ...

@NgModule({
  providers: [ { provide: RendererFactory2, useClass: MyRendererFactory } ],
  ...
})
export class AppModule { }

See demo here.

However, if you want to extend the default functionality it doesn't seem to be supported by Angular now. The default implementation of RendererFactory2 is DomRendererFactory2 which returns 3 different renderers depending on the encapsulation mode:

export class DomRendererFactory2 implements RendererFactory2 {
  createRenderer(element: any, type: RendererType2|null): Renderer2 {
    switch (type.encapsulation) {
      case ViewEncapsulation.Emulated: {
        return new EmulatedEncapsulationDomRenderer2(this.eventManager, this.sharedStylesHost, type);;
      }
      case ViewEncapsulation.Native:
        return new ShadowDomRenderer(this.eventManager, this.sharedStylesHost, element, type);
      default: {
        return new DefaultDomRenderer2(eventManager);
      }
    }
  }

Unfortunately, neither of these classes

DomRendererFactory2 
DefaultDomRenderer2
EmulatedEncapsulationDomRenderer2
ShadowDomRenderer

are available as public API.

If you plan on using solely ViewEncapsulation.Native you can access DefaultDomRenderer2 in the defaultRenderer property of the DomRendererFactory2 that is created when the factory is initiated:

@Injectable()
export class DomRendererFactory2 implements RendererFactory2 {
  private defaultRenderer: Renderer2;

  constructor(private eventManager: EventManager, private sharedStylesHost: DomSharedStylesHost) {
    this.defaultRenderer = new DefaultDomRenderer2(eventManager);
  }

And then decorate it's createElement method:

@NgModule({
    imports: [BrowserModule],
    declarations: [AppComponent, AComponent, ADirective],
    bootstrap: [AppComponent]
})
export class AppModule {
    constructor(factory: RendererFactory2) {
        const createElement = Object.getPrototypeOf(factory.defaultRenderer).createElement;

        Object.getPrototypeOf(factory.defaultRenderer).createElement = function (...args) {
            const el = createElement(...args);
            el.setAttribute(name, 'marking Angular component');
            return el;
        }
    }
}

Here is the demo.

However, the property is private.

You can also use the first approach of providing a custom Renderer and simply copy all implementations from the sources.

Any solution still seems to be risky.

Max Koretskyi
  • 101,079
  • 60
  • 333
  • 488
  • Thanks so much for your answer! It seems that any approach to solve the original problem of CSS separation bears the risk of causing huge problems down the line – in this case, due to rolling our own renderer. For this reason, we now actually decided to keep the Angular and AngularJS apps decoupled for as long as we can and, in the meantime, incorporate the former through an iframe in the latter… While this seems like a dirty hack, it does have the advantage that it's a dead simple solution which still allows us to go for more complicated solutions later on if need be. – balu Oct 13 '17 at 14:52
  • @balu, you're welcome! maybe in the future Angular will make the classes public. Maybe create a PR with a use case? If you will, post a link here. Also check out AngularInDepth.com blog. I write many in depth articles there. Good luck! – Max Koretskyi Oct 13 '17 at 15:07