Loading a third-party script (an analytics tag, a payment widget's SDK) or injecting a style block dynamically is occasionally necessary in an Angular app — Renderer2 is the framework-recommended way to do this safely, rather than reaching for document directly.
Dynamically inserting a script tag
import { Renderer2, RendererFactory2, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
export class ScriptLoaderService {
private renderer: Renderer2;
constructor(
rendererFactory: RendererFactory2,
@Inject(DOCUMENT) private document: Document
) {
this.renderer = rendererFactory.createRenderer(null, null);
}
loadScript(src: string): Promise {
return new Promise((resolve, reject) => {
const script = this.renderer.createElement('script');
script.type = 'text/javascript';
script.src = src;
script.onload = () => resolve();
script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
this.renderer.appendChild(this.document.body, script);
});
}
}
this.scriptLoader.loadScript('https://js.stripe.com/v3/').then(() => {
// Stripe.js is now loaded and available on window.Stripe
});
Using Renderer2 rather than directly manipulating document.body keeps the code compatible with server-side rendering (Angular Universal) and other non-browser rendering targets, where a direct document reference wouldn't work the same way.
Why the DOCUMENT injection token, not a raw document reference
Injecting DOCUMENT (Angular's platform-agnostic token) instead of referencing the global document object directly is what allows this same service to work correctly in a server-side rendering context, where Angular provides a different document implementation behind the same token.
Dynamically inserting a style tag
injectStyle(css: string): void {
const style = this.renderer.createElement('style');
const text = this.renderer.createText(css);
this.renderer.appendChild(style, text);
this.renderer.appendChild(this.document.head, style);
}
this.scriptLoader.injectStyle('.highlight { background: yellow; }');
Loading a script only once, even if the service method is called multiple times
private loadedScripts = new Set();
loadScript(src: string): Promise {
if (this.loadedScripts.has(src)) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
const script = this.renderer.createElement('script');
script.src = src;
script.onload = () => {
this.loadedScripts.add(src);
resolve();
};
script.onerror = reject;
this.renderer.appendChild(this.document.body, script);
});
}
Tracking already-loaded script URLs prevents accidentally injecting the same third-party script multiple times across different components that each independently call the loader — genuinely useful for a widely-shared service like a payment SDK that multiple unrelated pages might each try to load.
Removing a dynamically added script or style when no longer needed
removeElement(element: any): void {
this.renderer.removeChild(this.document.body, element);
}
Keeping a reference to the created element (returned from createElement) is necessary if it needs to be removed later — for a component-scoped script that should only be active while that specific component is displayed, cleaning it up in ngOnDestroy avoids it lingering on the page after navigation.