Frameworks
Angular
A directive, so the editor's element stays in the template where it belongs. Everything on
this page is ordinary @matrajs/core · there is no Angular-specific package to install.
npm i @matrajs/core Why there is no @matrajs/angular
Worth saying plainly rather than leaving you to wonder whether Angular is an afterthought.
An Angular library has to be published through ng-packagr in Angular's partial compilation
format, and that format is tied to the compiler version that produced it. Ship one and it fails
in some consumers' AOT builds · a package that breaks production builds is worse than the fifteen
lines below, which your compiler handles correctly because it is the one compiling them.
The directive
import { Directive, ElementRef, OnDestroy, OnInit, inject } from '@angular/core'
import { createEditor, starterKit } from '@matrajs/core'
@Directive({ selector: '[matra]', standalone: true, exportAs: 'matra' })
export class MatraDirective implements OnInit, OnDestroy {
private host = inject(ElementRef<HTMLElement>)
editor = createEditor({ extensions: starterKit, content: '<p>Hello</p>' })
ngOnInit() {
if (!this.editor.unsafe.view) this.editor.mount(this.host.nativeElement)
}
ngOnDestroy() {
this.editor.destroy()
}
} Using it
<div matra #ed="matra"></div>
<button (click)="ed.editor.commands.toggleBold()">Bold</button> exportAs is what makes #ed the directive rather than the element, so
a toolbar anywhere in the template can reach ed.editor without a service.
A toolbar that keeps up
The editor is not an Angular signal, so a button's pressed state has to be told when to
change.
editor.on() returns its own unsubscribe, and the cheapest correct wiring is a signal
updated from it.
import { Directive, ElementRef, OnDestroy, OnInit, inject, signal } from '@angular/core'
import { createEditor, starterKit } from '@matrajs/core'
@Directive({ selector: '[matra]', standalone: true, exportAs: 'matra' })
export class MatraDirective implements OnInit, OnDestroy {
private host = inject(ElementRef<HTMLElement>)
private off: (() => void)[] = []
editor = createEditor({ extensions: starterKit, content: '<p>Hello</p>' })
active = signal<Record<string, boolean>>({})
ngOnInit() {
if (!this.editor.unsafe.view) this.editor.mount(this.host.nativeElement)
const read = () => this.active.set({ bold: this.editor.isActive('bold') })
this.off.push(this.editor.on('selectionChange', read), this.editor.on('change', read))
read()
}
ngOnDestroy() {
for (const off of this.off) off()
this.editor.destroy()
}
}
Both events, not just one. selectionChange covers moving the caret into bold text;
change covers the command that made it bold without moving anything. A toolbar
wired to only the first goes stale the moment someone uses a keyboard shortcut.
Zone.js
Nothing here needs NgZone.run. The editor's listeners fire from DOM events
Angular already patches, so a signal set inside one is seen. If you are on
provideExperimentalZonelessChangeDetection, signals are the only thing that
schedules a render anyway, which is why the toolbar above uses one rather than a plain
field.
Server rendering
createEditor needs no DOM, so it is safe to construct during SSR · only
mount touches the document, and ngOnInit does not run on the server
for a directive on an element that was never created. To render stored content without an editor
at all, use renderToHTML.