11

Unfortunately, very little information on this library. It is not completely clear to me after installation what I need to import into the app.module.ts and whether there is something to import there? I have prescribed the following code in the index.html:

<script type="text/x-mathjax-config">
MathJax.Hub.Config({
  tex2jax: {
    inlineMath: [['$','$'], ['\\(','\\)']]
  }
});
</script>
<script type="text/javascript" async
 src='https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/latest.js? 
 config=TeX-MML-AM_CHTML'>
</script>

And how can I apply the MathJax, if I have not simple text, but a table in which text with formulas appears in some columns? Perhaps you can somehow transfer the entire table to MathJax.Hub.Queue?

Mark
  • 171
  • 2
  • 10

1 Answers1

22

I was looking at the same problem like two weeks ago and today i finally manage to make it work. I'm no angular expert so it may need some optimization, but core funcionality is working.

Step one

Add @types/mathjax dependency to your package.json file.

Step two

Add newly added type to tsconfig.app.json like this:

{
  "compilerOptions": {
    "types": ["mathjax"]
  }
}

This will allow you to use constructs such as MathJax.Callback.Queue and your IDE won't complain about unkown type etc.

Step three create wrapper object for your math content (optional)

I had some issues with Mathml so i have created wrapper for math which looks like this :

export interface MathContent {
  latex?: string;
  mathml?: string;
}

Step four

Now we need to define module which whill inject MathJax script tag with configuration. Because it will be loaded dynamically async we need to make sure typesetting doesn't start before MathJax is fully loaded. Easiest way is to store Observer<any> into window object. Observer and render functionality can be wrapped into service.

// see https://stackoverflow.com/a/12709880/1203690
declare global {
  interface Window {
    hubReady: Observer<boolean>;
  }
}

@Injectable()
export class MathServiceImpl {
  private readonly notifier: ReplaySubject<boolean>;

  constructor() {
    this.notifier = new ReplaySubject<boolean>();
    window.hubReady = this.notifier; // as said, bind to window object
  }

  ready(): Observable<boolean> {
    return this.notifier;
  }

  render(element: HTMLElement, math?: MathContent): void {
    if (math) {
      if (math.latex) {
        element.innerText = math.latex;
      } else {
        element.innerHTML = math.mathml;
      }
    }

    MathJax.Hub.Queue(['Typeset', MathJax.Hub, element]);
  }
}

Step five

Now we will create directive which will trigger rendering once MathJax is loaded. The directive may look like this:

@Directive({
  selector: '[appMath]'
})
export class MathDirective implements OnInit, OnChanges, OnDestroy {
  private alive$ = new Subject<boolean>();

  @Input()
  private appMath: MathContent;
  private readonly _el: HTMLElement;

  constructor(private service: MathServiceImpl,
              private el: ElementRef) {
    this._el = el.nativeElement as HTMLElement;
  }

  ngOnInit(): void {
    this.service
      .ready()
      .pipe(
        take(1),
        takeUntil(this.alive$)
      ).subscribe(res => {
        this.service.render(this._el, this.appMath);
    });
  }

  ngOnChanges(changes: SimpleChanges): void {
    console.log(changes);
  }

  ngOnDestroy(): void {
    this.alive$.next(false);
  }
}

Step six (almost there)

In step four i mentioned async loading. According to docs onMathJax this is done using document.createElement. Angular Module is perfect place for this logic. To trigger .ready() method on our MathService we will use MathJax.Hub.Register.StartupHook and pass observable from MathService So our module will look like this:


@NgModule({
  declarations: [MathDirective],
  exports: [MathDirective]
})
export class MathModule {
  constructor(mathService: MathServiceImpl) {
    // see https://docs.mathjax.org/en/latest/advanced/dynamic.html
    const script = document.createElement('script') as HTMLScriptElement;
    script.type = 'text/javascript';
    script.src = 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML';
    script.async = true;

    document.getElementsByTagName('head')[0].appendChild(script);

    const config = document.createElement('script') as HTMLScriptElement;
    config.type = 'text/x-mathjax-config';
    // register notifier to StartupHook and trigger .next() for all subscribers
    config.text = `
    MathJax.Hub.Config({
        skipStartupTypeset: true,
        tex2jax: { inlineMath: [["$", "$"]],displayMath:[["$$", "$$"]] }
      });
      MathJax.Hub.Register.StartupHook('End', () => {
        window.hubReady.next();
        window.hubReady.complete();
      });
    `;

    document.getElementsByTagName('head')[0].appendChild(config);
  }

  // this is needed so service constructor which will bind
  // notifier to window object before module constructor is called
  public static forRoot(): ModuleWithProviders {
    return {
      ngModule: MathModule,
      providers: [{provide: MathServiceImpl, useClass: MathServiceImpl}]
    };
  }
}

Step seven: render math

Now everything is ready simply import MathModule.forRoot() in module where you want to render math. The component will look like this:

export class AppComponent {
  mathLatex: MathContent = {
    latex: 'When $a \\ne 0$, there are two solutions to $\\frac{5}{9}$'
  };

  mathMl: MathContent = {
    mathml: `<math xmlns="http://www.w3.org/1998/Math/MathML">
  <mrow>
    <mover>
      <munder>
        <mo>∫</mo>
        <mn>0</mn>
      </munder>
      <mi>∞</mi>
    </mover>
    <mtext> versus </mtext>
    <munderover>
      <mo>∫</mo>
      <mn>0</mn>
      <mi>∞</mi>
    </munderover>
  </mrow>
</math>`
  };
}

and template

<div [appMath]="mathLatex"></div>

<div [appMath]="mathMl"></div>

<!-- will render inline element math -->
<div [appMath]>
  $E = mc^2$
</div>

Which should render to this

enter image description here

Step eight (bonus)

Here is working stackblitz example https://stackblitz.com/edit/mathjax-example so you can check your progress against implementation

emptak
  • 429
  • 6
  • 11
  • I hope this decision will help others. for installation MathJax `npm install mathjax` and `npm install --save @types/mathjax` –  Mark Mar 15 '19 at 08:57
  • 1
    No need for the `npm install mathjax` because the Mathjax is loaded by the create element and `npm install -D @types/mathjax` can be installed as Dev dependencies because to prevent it going into your prod bundle. – Jeyenth Apr 30 '19 at 04:16
  • You are a life savior @emptak, Thank you very much – Himanshu Mittal Jun 19 '19 at 16:02
  • 1
    Thank you! I've tried a few angular wrapper, but this answer was the only that did the job – cfcm Dec 10 '19 at 14:40
  • For development mode it's working fine. I am getting this error for prod ssr build for it. any help ? ` Function calls are not supported in decorators but 'MathModule' was called.'. – Mr X Sep 13 '20 at 05:48
  • In MathJax v3 use `MathJax.typesetPromise()` rather than `MathJax.Hub.Queue(['Typeset', MathJax.Hub, element])`, and replace `config.text` with `MathJax = { tex: { inlineMath: [["$", "$"]] }, startup: { ready: () => { MathJax.startup.defaultReady(); window.hubReady.next(); window.hubReady.complete(); } } }`, and `script.src` with https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js – Andrew Allen May 29 '21 at 16:01