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
Step eight (bonus)
Here is working stackblitz example https://stackblitz.com/edit/mathjax-example so you can check your progress against implementation