Angular — detailed integration
Standalone components, NgModule, reactive forms, validation patterns, and the wrapper component pattern.
Angular — detailed integration
Standalone component
AtelierEditorComponent is a standalone Angular component implementing ControlValueAccessor.
Place it in the imports array — not declarations:
@Component({
standalone: true,
imports: [FormsModule, AtelierEditorComponent], // ← imports, not declarations
// ...
})NgModule (pre-standalone apps)
For apps that haven't migrated to standalone components, the standalone component still works —
it goes in the module's imports array (not declarations):
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AtelierEditorComponent } from '@innosoft/atelier-editor-angular';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
FormsModule,
AtelierEditorComponent // standalone component goes in imports[]
],
bootstrap: [AppComponent]
})
export class AppModule {}Reactive forms
AtelierEditorComponent is a ControlValueAccessor, so it works with any form abstraction:
// Reactive form
import { FormControl } from '@angular/forms';
const contentCtrl = new FormControl<string>('', { nonNullable: true });
// In template: [formControl]="contentCtrl"<!-- Template-driven with ngModel -->
<atelier-editor [(ngModel)]="htmlContent" name="content"></atelier-editor>
<!-- Reactive with FormControl -->
<atelier-editor [formControl]="contentCtrl"></atelier-editor>
<!-- Read-only through form -->
<atelier-editor [formControl]="contentCtrl" [disabled]="true"></atelier-editor>Calling contentCtrl.disable() / contentCtrl.enable() propagates through setDisabledState()
and dims the editor surface automatically.
Validation pattern (content + button state)
A common requirement is to keep a "Next" or "Submit" button disabled until the user has typed a minimum number of characters. There are two traps to avoid:
Trap 1 — circular dependency with Validators.required. If you put Validators.required
on the content form field and the editor is not live-synced to the form, the form is always
invalid, so the button is always disabled, so the action that would set content is never
reachable.
Trap 2 — using a live-computed validation value but starting it non-zero. Starting a
character-count check at, say, 30 means a < 15 check is already satisfied, masking the fact
that the form's content field is still empty.
Correct pattern:
@Component({
/* ... */
})
export class DocFormComponent {
// 1. No Validators.required on content — validation is done via validationVal
openForm = this._fb.group({
nameAr: [null, [Validators.pattern(Patterns.OnlyArCharacters)]],
nameEn: [null, [Validators.pattern(Patterns.OnlyEnCharacters)]],
content: [null] // no required validator
});
// 2. Start at 0 so the button is disabled until the user types
validationVal = 0;
@ViewChild('editor') private editorRef!: TextAtelierComponent;
// 3. Update validationVal whenever the editor content changes
onEditorContentChange(html: string): void {
this.validationVal = html
.replace(/<[^>]+>|[\s]+/gm, '')
.replace(/ /g, ' ')
.trim().length;
}
// 4. In EDIT mode — seed validationVal immediately from API content so the
// button is enabled without requiring the user to type first
ngOnInit(): void {
this._route.params
.pipe(switchMap(p => (p['reqId'] ? this._service.getById(p['reqId']) : [])))
.subscribe({
next: res => {
this.openForm.patchValue(res.data);
// Seed validationVal from loaded content
const apiContent = res.data?.content || '';
this.validationVal = apiContent
.replace(/<[^>]+>|[\s]+/gm, '')
.replace(/ /g, ' ')
.trim().length;
}
});
}
// 5. The button checks validationVal only (form.invalid only catches pattern violations)
// Template: [disabled]="openForm.invalid || validationVal < 15"
// 6. On submit: read HTML from the editor, set the form field, then navigate
previewDoc(): void {
const html = this.editorRef.exportHTML();
this.openForm.controls['content'].setValue(html, { emitEvent: false });
if (this.openForm.valid && this.validationVal >= 15) {
this.openDocContent = html;
this.stepper.next();
}
}
}Wrapper component pattern
When embedding the editor in a host component that receives content via @Input and reports
changes via @Output, follow these rules to avoid an infinite loop (see Preventing the infinite
update loop):
// text-atelier.component.ts
@Component({
standalone: true,
selector: 'app-text-atelier',
imports: [AtelierEditorComponent, FormsModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<atelier-editor
#editor
[starterKit]="true"
[bold]="true"
[italic]="true"
[underline]="true"
[rtl]="isRtl"
[lang]="'ar'"
[licenseKey]="licenseKey"
[labels]="activeLabels"
[ngModel]="editorContent"
(ngModelChange)="onContentChange($event)"
></atelier-editor>
`
})
export class TextAtelierComponent implements OnChanges {
@ViewChild('editor') private editorRef!: AtelierEditorComponent;
@Input() initialContent = '';
@Output() contentChange = new EventEmitter<string>();
readonly licenseKey = '...';
isRtl = true;
// IMPORTANT: always a stable readonly reference — never a getter returning {}
private readonly emptyLabels: Partial<Record<string, string>> = {};
get activeLabels(): Partial<Record<string, string>> {
return this.emptyLabels;
}
editorContent = '';
ngOnChanges(changes: SimpleChanges): void {
if (changes['initialContent']) {
const incoming = changes['initialContent'].currentValue as string;
// Only update if different — prevents overwriting user edits
if (incoming && incoming !== this.editorContent) {
this.editorContent = incoming;
}
}
}
onContentChange(value: string): void {
this.editorContent = value;
this.contentChange.emit(value); // ← propagate to parent for validationVal
}
// Call this from the parent when you need the final HTML (e.g. on submit)
exportHTML(): string {
return this.editorRef.getHTML();
}
}Parent usage:
<app-text-atelier
#editor
[initialContent]="openForm.get('content')?.value || ''"
(contentChange)="onEditorContentChange($event)"
></app-text-atelier>