Reputation: 3831
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?
Upvotes: 3
Views: 996
Reputation: 105497
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 { }
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;
}
}
}
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.
Upvotes: 2