Good day everyone!

My task is to load two external modules in one component. I load external scripts through directives rendering. The problem is that only the last one is loaded, the first one is "overwritten" at boot. I see that the whole thing is in rendering when I create a new element. How to fix it?

home.component.html

<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="nav-home-tab"> <searchTourSimple [script]="'src/app/scripts/homeSearch.js'"></searchTourSimple> </div> <div class="shop-window"> <div id="shopwindow-container"> <shopWindow [script]="'src/app/scripts/shopwindow.js'"></shopWindow> </div> </div> 

dir1.directive.ts

 @Directive({ selector: 'searchTourSimple' }) export class SearchModuleSimpleDirective implements OnInit { @Input('script') param: any script: any constructor(private renderer:Renderer2) { } ngOnInit() { this.script = this.renderer.createElement('script') this.script.type = 'text/javascript' this.script.src = this.param this.script.async = true this.renderer.appendChild(document.head, this.script) document.write = function(input: string) { document.getElementById('home').innerHTML += input } } } 

dir2.directive.ts

 @Directive({ selector: 'shopWindow' }) export class ShopWindowDirective implements OnInit { @Input('script') param: any script: any constructor(private renderer:Renderer2) { } ngOnInit() { this.script = this.renderer.createElement('script') this.script.type = 'text/javascript' this.script.src = this.param this.script.async = true this.renderer.appendChild(document.head, this.script) document.write = function(input: string) { document.getElementById('shopwindow-container').innerHTML += input } } } 
  • What is this document.write = function(input: string) ? - overthesanity
  • input - this is the module - lzhec
  • I also noticed such a thing: the second directive is executed twice. First, for some reason, it loads the first module again, and then the second one. This can be seen from the console: - lzhec
  • which module? and why input is a module if it is a string type parameter? - overthesanity
  • This is the result of executing the script, iframe, which needs to be inserted into the DOM, which I do with the function document.write - lzhec

1 answer 1

Your problem is related to the fact that the second directive also patches document.write , thus the first directive uses the method patched by the second directive and the innerHTML sett in document.getElementById('shopwindow-container') .

You do not need directives here, if I correctly understood from the source code, then you are using directives to try to load external .js resources, you need to isolate this business logic altogether in some service, let's call it ScriptService :

 import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { tap, switchMap } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class ScriptService { private loadResource(src: string): Observable<void> { return new Observable((observer) => { const script = document.createElement('script'); script.async = true; script.src = src; script.addEventListener('load', () => { observer.next(); observer.complete(); }); document.head.appendChild(script); }); } private patchDocumentWrite(id: string): void { document.write = (input: string): void => { document.getElementById(id).innerHTML += input; }; } public loadExternalResources(): Observable<void> { return of(null).pipe( tap(() => this.patchDocumentWrite('home')), switchMap(() => this.loadResource('src/app/scripts/homeSearch.js')), tap(() => this.patchDocumentWrite('shopwindow-container')), switchMap(() => this.loadResource('src/app/scripts/shopwindow.js')) ); } } 

What are we doing? The loadResource method returns a stream and generates an event after the external resource is fully loaded ( onload ). patchDocumentWrite simply patches document.write , and takes the argument of the element element to which innerHTML should be placed. In the loadExternalResources method loadExternalResources we create an empty stream (needed for subsequent piping). Before loading the script homeSearch patch document.write and just as well further.

In this implementation, we load the scripts one by one, but this gives us a guarantee that some directive does not patch the method faster than another. We also do not use Renderer2 (it is not needed for this). Now we need to subscribe to the stream that the loadExternalResources method loadExternalResources to run the entire pipeline; we can do this in the ngAfterViewInit component ngAfterViewInit :

 export class AppComponent implements AfterViewInit { constructor(private scriptService: ScriptService) {} public ngAfterViewInit(): void { this.scriptService.loadExternalResources().subscribe(() => { console.log('все скрипты загрузились'); }); } } 
  • Thank you very much! Everything is working. I tried to implement something similar, but without observable, in the end nothing happened. Tell me, is it better to implement your services for other components (the same functionality, only other modules)? And where can I read about all these methods next, complete, tap, etc.? - lzhec
  • it is better to keep all "such" logic in services, this is the usual decomposition of + one responsibility, you can read it here . Tick ​​the answer? - overthesanity
  • Required. Is this in the sense of loading external resources? - lzhec