Loading a third-party script (an embed widget, an analytics snippet) or injecting a dynamic stylesheet at runtime, rather than at build time, needs to go through Angular's Renderer2 abstraction rather than directly touching document.
Dynamically inserting a script tag
constructor(@Inject(DOCUMENT) private document: Document, private renderer: Renderer2) {}
loadExternalScript(src: string): Promise {
return new Promise((resolve, reject) => {
const script = this.renderer.createElement('script');
script.src = src;
script.onload = () => resolve();
script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
this.renderer.appendChild(this.document.body, script);
});
}
ngOnInit(): void {
this.loadExternalScript('https://widget.example.com/embed.js')
.then(() => console.log('Widget script loaded'))
.catch(err => console.error(err));
}
Using Renderer2 and the injected DOCUMENT token, rather than the global document object directly, keeps this code compatible with server-side rendering (Angular Universal) — the raw document object isn't safely available during a server render pass, while the injected token resolves to the correct platform-appropriate implementation.
Preventing a script from being loaded more than once
private loadedScripts = new Set();
loadExternalScript(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(new Error(`Failed to load script: ${src}`));
this.renderer.appendChild(this.document.body, script);
});
}
Without this tracking set, navigating to and from a component that loads the same external script repeatedly would insert multiple duplicate <script> tags — often harmless for a simple embed, but genuinely problematic for a script that registers global event listeners or initializes a widget that doesn't expect to run more than once.
Dynamically inserting a style tag
insertDynamicStyles(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);
}
ngOnInit(): void {
this.insertDynamicStyles(`
.theme-accent { color: ${this.userThemeColor}; }
`);
}
This is a genuinely useful pattern for a user-configurable theme color or a dynamically generated set of CSS rules that can't reasonably be expressed as static component styles known at build time.
Removing a dynamically inserted tag on component destroy
private scriptElement?: HTMLScriptElement;
ngOnDestroy(): void {
if (this.scriptElement) {
this.renderer.removeChild(this.document.body, this.scriptElement);
}
}
Keeping a reference to the inserted element and removing it in ngOnDestroy() avoids leaving orphaned script or style tags in the document after the component that needed them is gone — worth doing for anything not meant to persist for the entire lifetime of the app.
Why this shouldn't be the default way to load a script
For any script actually known and needed at build time, adding it to angular.json's scripts array (or importing it as a proper npm package) is simpler and more reliable — this dynamic-insertion pattern is specifically for scripts whose need is only known at runtime, such as a widget that's conditionally shown based on a feature flag or user setting.