1

I am trying to use MathJax to display equations in my Angular 2 app. Only a small bit of my app actually needs this functionality, so I don't want all users to have to download the 3-400kb needed.

My initial thought was to import "mathjax" within my component, so that the library will be loaded only when the component is created. However, I learned from my earlier question that MathJax doesn't play well with others because it uses its own custom module loader.

My current setup loads the script from my index.html but that means everyone will download it when only a small percentage of users need this component

<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=AM_SVG"></script>

Is there a clean way in Angular 2 to have my component download a script within the ngOnInit() hook and execute a callback once it's in the DOM? Something such as (pseudo-code):

ngOnInit(){
    //download script, add to DOM      
    fetch('https://...MathJax.js').then(addToDom).then(this.onMathJaxLoaded);
}
onMathJaxLoaded(){
  // Run math renderer
  MathJax.Hub.Queue("Typeset",...);
}
Community
  • 1
  • 1
BeetleJuice
  • 39,516
  • 19
  • 105
  • 165

2 Answers2

6

I have not tried it, but I think this might be possible:

ngOnInit(){
    //download script, add to DOM      
    var script = document.createElement('script');
    document.body.appendChild(script)
    script.onload = this.onMathJaxLoaded.bind(this);
    script.src = 'https://...MathJax.js';
}
onMathJaxLoaded(){
  // Run math renderer
  MathJax.Hub.Queue("Typeset",...);
}

Here is a fiddle, which contains a example of how it would work in plain JavaScript; https://jsfiddle.net/5qu5h7bc/

TryingToImprove
  • 7,047
  • 4
  • 30
  • 39
  • Thanks for the fast response. It works. I need to refine it a bit because there is a lot of redundancy: in the `Angular 2` app, there can be many instances of this component (each component holds a user comment) and I'm seeing 11 scripts loaded and 11 tags added when the page has 11 comments. Ideally, the script tag would be added just once, though `onMathJaxLoaded` must be run 11 times, once to render each comment. – BeetleJuice Dec 06 '16 at 09:54
  • Cool, please accept if works.. One way to ensure that the tag is only added once, is to set a global variable like `isMathJaxLoaded` and if it is `true`, then just call `this.onMathJaxLoaded` otherwise append the `script`-tag – TryingToImprove Dec 06 '16 at 10:06
  • Your code inspired me to my posted solution. Thanks again. – BeetleJuice Dec 07 '16 at 03:24
2

Building on @TryingToImprove's solution, I decided to add a ScriptLoaderService to my app with a load() function that fetches a remote script and notifies the caller (using an RxJS Observable) when it's done.

Here is how to use it:

this.scriptLoaderService.load('https://...js').subscribe(
    ()=>console.log('script has loaded');
);

Here is the service:

/**
 * A directory of scripts that have been or are being loaded
 */
private scripts = new Map<string,Observable<boolean>|boolean>();

/**
 * Downloads script; returns Observable that emits TRUE once "load" event fires
 */
public load(src:string):Observable<boolean>{        
    if(this.scripts.has(src)){
        // If script has already been fully loaded.
        if(this.scripts.get(src)===true) return Observable.of(true);

        // Else if download is already underway but load event hasn't fired yet
        else return this.scripts.get(src);
    }

    //Add a script tag to the DOM
    let script = document.createElement('script');
    document.body.appendChild(script);

    // upon subscription, listen for the load event. Once it arrives, emit TRUE
    let obs:Observable<boolean> = Observable.fromEvent(script,'load')
        // Map should hold TRUE once script is loaded
        .do(()=> this.scripts.set(src,true))
        .take(1).map(e=>true);

    // set the src attribute (will cause browser to download)
    script.src = src;

    //add observable to the scripts Map
    this.scripts.set(src,obs);

    return obs;
}

Tested on Firefox 51

BeetleJuice
  • 39,516
  • 19
  • 105
  • 165