Skip to content

Angular Integration

The @notectl/angular package provides a native Angular wrapper around the notectl editor. It ships a standalone component, a ControlValueAccessor for reactive and template-driven forms, and an injectable service for programmatic access.

Requirements: Angular 21+, Node.js 20+

  1. Install both packages

    Terminal window
    npm install @notectl/core @notectl/angular

    @notectl/core contains the editor engine and plugins. @notectl/angular provides the Angular bindings.

  2. Register the provider

    Add provideNotectl() to your application providers. This is optional but recommended — it enables app-wide default configuration.

    app.config.ts
    import { type ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';
    import { provideNotectl } from '@notectl/angular';
    export const appConfig: ApplicationConfig = {
    providers: [
    provideZonelessChangeDetection(),
    provideNotectl(),
    ],
    };

Import NotectlEditorComponent and configure the plugins you need:

import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import {
NotectlEditorComponent,
TextFormattingPlugin,
HeadingPlugin,
ListPlugin,
LinkPlugin,
ThemePreset,
} from '@notectl/angular';
import type { Plugin, StateChangeEvent } from '@notectl/angular';
@Component({
selector: 'app-editor',
imports: [NotectlEditorComponent],
template: `
<ntl-editor
[toolbar]="toolbar"
[plugins]="plugins"
[theme]="theme()"
[autofocus]="true"
(stateChange)="onStateChange($event)"
/>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class EditorComponent {
protected readonly theme = signal<ThemePreset>(ThemePreset.Light);
protected readonly toolbar: ReadonlyArray<ReadonlyArray<Plugin>> = [
[new TextFormattingPlugin({ bold: true, italic: true, underline: true })],
[new HeadingPlugin()],
[new ListPlugin()],
[new LinkPlugin()],
];
protected readonly plugins: Plugin[] = [];
protected onStateChange(event: StateChangeEvent): void {
console.log('New state:', event.newState);
}
}

The <ntl-editor> component uses Angular signals for all inputs and outputs:

InputTypeDefaultDescription
toolbarPlugin[][]Toolbar layout (groups of plugins)
pluginsPlugin[][]Plugins without toolbar buttons
featuresPartial<TextFormattingConfig>Feature toggles for text formatting
placeholderstring'Start typing...'Placeholder text
readonlyModebooleanfalseReadonly mode
autofocusbooleanfalseAuto-focus on mount
maxHistoryDepthnumberUndo/redo history limit
themeThemePreset | ThemeThemePreset.LightEditor theme
OutputPayloadDescription
stateChangeStateChangeEventFires on every content change
selectionChangeSelectionChangeEventFires when cursor/selection moves
editorFocusvoidEditor gained focus
editorBlurvoidEditor lost focus
readyvoidEditor fully initialized

Use [(content)] to bind the document model in both directions:

<ntl-editor [(content)]="myDocument" [toolbar]="toolbar" />
protected readonly myDocument = signal<Document | undefined>(undefined);

The NotectlValueAccessorDirective integrates with Angular’s reactive and template-driven forms automatically.

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import {
NotectlEditorComponent,
NotectlValueAccessorDirective,
TextFormattingPlugin,
} from '@notectl/angular';
import type { Document, Plugin } from '@notectl/angular';
@Component({
selector: 'app-editor-form',
imports: [ReactiveFormsModule, NotectlEditorComponent, NotectlValueAccessorDirective],
template: `
<ntl-editor [formControl]="editorControl" [toolbar]="toolbar" />
<button (click)="save()">Save</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class EditorFormComponent {
protected readonly editorControl = new FormControl<Document | null>(null);
protected readonly toolbar: ReadonlyArray<ReadonlyArray<Plugin>> = [
[new TextFormattingPlugin({ bold: true, italic: true, underline: true })],
];
protected save(): void {
const value: Document | null = this.editorControl.value;
console.log('Form value:', value);
}
}
@Component({
selector: 'app-editor-ngmodel',
imports: [FormsModule, NotectlEditorComponent, NotectlValueAccessorDirective],
template: `<ntl-editor [(ngModel)]="content" [toolbar]="toolbar" />`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class EditorNgModelComponent {
protected content: Document | null = null;
protected readonly toolbar: ReadonlyArray<ReadonlyArray<Plugin>> = [
[new TextFormattingPlugin()],
];
}

By default, forms use JSON (Document objects). You can switch to HTML or plain text via the provider:

provideNotectl({
contentFormat: 'html', // 'json' | 'html' | 'text'
})
FormatForm Value TypeDescription
'json'DocumentStructured document model (default)
'html'stringSanitized HTML
'text'stringPlain text

Switch themes at runtime using a signal:

protected readonly theme = signal<ThemePreset>(ThemePreset.Light);
toggleTheme(): void {
const next = this.theme() === ThemePreset.Light
? ThemePreset.Dark
: ThemePreset.Light;
this.theme.set(next);
}
<ntl-editor [theme]="theme()" [toolbar]="toolbar" />
<button (click)="toggleTheme()">Toggle Theme</button>

Use NotectlEditorService when a sibling component (e.g. a custom toolbar) needs to interact with the editor without a template reference:

import {
ChangeDetectionStrategy,
Component,
afterNextRender,
inject,
viewChild,
} from '@angular/core';
import {
NotectlEditorComponent,
NotectlEditorService,
} from '@notectl/angular';
import type { Plugin } from '@notectl/angular';
@Component({
selector: 'app-editor-page',
providers: [NotectlEditorService],
imports: [NotectlEditorComponent, ToolbarComponent],
template: `
<app-toolbar />
<ntl-editor #editor [toolbar]="toolbar" />
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class EditorPageComponent {
private readonly editorRef = viewChild.required<NotectlEditorComponent>('editor');
private readonly service = inject(NotectlEditorService);
protected readonly toolbar: ReadonlyArray<ReadonlyArray<Plugin>> = [/* ... */];
constructor() {
afterNextRender(() => {
this.service.register(this.editorRef());
});
}
}

Then in the toolbar component:

@Component({
selector: 'app-toolbar',
template: `
<button (click)="bold()">Bold</button>
<button (click)="undo()">Undo</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ToolbarComponent {
private readonly editorService = inject(NotectlEditorService);
protected bold(): void {
this.editorService.executeCommand('toggleBold');
}
protected undo(): void {
this.editorService.executeCommand('undo');
}
}
// Read content in different formats
const json: Document = editor.getJSON();
const html: string = editor.getHTML();
const text: string = editor.getText();
// Write content
editor.setJSON(doc);
editor.setHTML('<h1>Hello</h1><p>World</p>');
// Check if empty
if (editor.isEmpty()) {
console.log('No content');
}
// Toggle formatting
editor.commands.toggleBold();
editor.commands.toggleItalic();
editor.commands.undo();
editor.commands.redo();
// Execute any registered command by name
editor.executeCommand('toggleStrikethrough');
editor.executeCommand('insertHorizontalRule');
// Check command availability
if (editor.can().toggleBold()) {
editor.commands.toggleBold();
}

A production-ready editor with all plugins:

import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import {
NotectlEditorComponent,
TextFormattingPlugin,
HeadingPlugin,
ListPlugin,
LinkPlugin,
BlockquotePlugin,
CodeBlockPlugin,
TablePlugin,
HorizontalRulePlugin,
StrikethroughPlugin,
HighlightPlugin,
TextColorPlugin,
FontPlugin,
FontSizePlugin,
AlignmentPlugin,
SuperSubPlugin,
STARTER_FONTS,
ThemePreset,
} from '@notectl/angular';
import type { Plugin } from '@notectl/angular';
@Component({
selector: 'app-full-editor',
imports: [NotectlEditorComponent],
template: `
<ntl-editor
[toolbar]="toolbar"
[theme]="theme()"
[autofocus]="true"
placeholder="Start writing..."
/>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FullEditorComponent {
protected readonly theme = signal<ThemePreset>(ThemePreset.Light);
protected readonly toolbar: ReadonlyArray<ReadonlyArray<Plugin>> = [
[new TextFormattingPlugin({ bold: true, italic: true, underline: true })],
[new HeadingPlugin()],
[new BlockquotePlugin(), new LinkPlugin()],
[new ListPlugin()],
[new TablePlugin()],
[new HorizontalRulePlugin(), new StrikethroughPlugin(), new TextColorPlugin()],
[new HighlightPlugin(), new SuperSubPlugin()],
[new CodeBlockPlugin()],
[
new AlignmentPlugin(),
new FontPlugin({ fonts: [...STARTER_FONTS] }),
new FontSizePlugin({ sizes: [12, 16, 24, 32, 48], defaultSize: 16 }),
],
];
}

Use provideNotectl() to set app-wide defaults. Component-level inputs override these:

provideNotectl({
config: {
theme: ThemePreset.Dark,
placeholder: 'Write something...',
readonly: false,
},
contentFormat: 'json',
})

The @notectl/angular/testing package provides a test harness:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NotectlEditorComponent } from '@notectl/angular';
import { NotectlTestHarness } from '@notectl/angular/testing';
describe('MyEditorComponent', () => {
let fixture: ComponentFixture<MyEditorComponent>;
let harness: NotectlTestHarness;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyEditorComponent],
}).compileComponents();
fixture = TestBed.createComponent(MyEditorComponent);
fixture.detectChanges();
// Wrap the editor component in the test harness
const editorComponent = fixture.debugElement
.query(By.directive(NotectlEditorComponent))
.componentInstance;
harness = new NotectlTestHarness(fixture);
await harness.whenReady();
});
it('should render content', () => {
harness.setHTML('<p>Hello</p>');
expect(harness.getText()).toBe('Hello');
});
it('should execute commands', () => {
harness.executeCommand('toggleBold');
// assert formatting applied
});
});