| name | TinyMCE Rich Text Editor Integration |
| description | Integrate TinyMCE rich text editor using ngx-tinymce. Use this skill when implementing WYSIWYG editors, content management, article editing, or any rich text input. Covers configuration, plugins, toolbar customization, file uploads, content sanitization, and Angular integration with reactive forms and signals. Ensures accessibility and proper validation. |
| license | MIT |
TinyMCE Rich Text Editor Integration Skill
This skill helps integrate TinyMCE rich text editor using ngx-tinymce in Angular.
Core Principles
Rich Text Editing
- WYSIWYG: What You See Is What You Get editing
- Plugins: Extensible with TinyMCE plugins
- Validation: Content validation and sanitization
- Forms Integration: Works with Angular Reactive Forms
Key Features
- Customizable toolbar
- Image upload and management
- Code view and formatting
- Auto-save functionality
- Responsive design
- Accessibility support
Installation
npm install ngx-tinymce@^20.0.0
Basic Configuration
Standalone Component
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { TinymceModule } from 'ngx-tinymce';
import { SHARED_IMPORTS } from '@shared';
@Component({
selector: 'app-article-editor',
standalone: true,
imports: [SHARED_IMPORTS, TinymceModule, FormsModule],
template: `
<tinymce
[(ngModel)]="content"
[config]="config"
(ngModelChange)="handleContentChange($event)"
/>
<button nz-button nzType="primary" (click)="save()">
Save Article
</button>
`
})
export class ArticleEditorComponent {
content = signal('');
config = {
height: 500,
menubar: false,
plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap',
'preview', 'anchor', 'searchreplace', 'visualblocks', 'code',
'fullscreen', 'insertdatetime', 'media', 'table', 'help', 'wordcount'
],
toolbar:
'undo redo | formatselect | bold italic backcolor | \
alignleft aligncenter alignright alignjustify | \
bullist numlist outdent indent | removeformat | help'
};
handleContentChange(content: string): void {
this.content.set(content);
}
save(): void {
console.log('Saving content:', this.content());
}
}
Reactive Forms Integration
import { Component, inject } from '@angular/core';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { TinymceModule } from 'ngx-tinymce';
@Component({
selector: 'app-article-form',
standalone: true,
imports: [SHARED_IMPORTS, TinymceModule, ReactiveFormsModule],
template: `
<form [formGroup]="articleForm" (ngSubmit)="submit()">
<nz-form-item>
<nz-form-label [nzRequired]="true">Title</nz-form-label>
<nz-form-control nzErrorTip="Please enter title">
<input nz-input formControlName="title" placeholder="Article title" />
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzRequired]="true">Content</nz-form-label>
<nz-form-control nzErrorTip="Please enter content">
<tinymce
formControlName="content"
[config]="config"
/>
</nz-form-control>
</nz-form-item>
<button
nz-button
nzType="primary"
[disabled]="articleForm.invalid"
>
Publish
</button>
</form>
`
})
export class ArticleFormComponent {
private fb = inject(FormBuilder);
articleForm: FormGroup = this.fb.group({
title: [, [., .()]],
: [, [., .()]]
});
config = {
: ,
: [, , , , ],
:
};
(): {
(..) {
.(, ..);
}
}
}
Advanced Configuration
Full-Featured Editor
config = {
height: 600,
menubar: 'file edit view insert format tools table help',
plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount',
'codesample', 'emoticons', 'template', 'paste', 'pagebreak'
],
toolbar: [
'undo redo | formatselect | bold italic underline strikethrough | forecolor backcolor',
'alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist',
'link image media codesample | table | code fullscreen preview | emoticons charmap'
],
toolbar_mode: 'sliding',
contextmenu: 'link image table',
skin: 'oxide',
content_css: 'default',
branding: false,
elementpath: true,
statusbar: true,
resize: true,
: ,
: ,
: ,
:
};
Image Upload
Firebase Storage Integration
import { Storage, ref, uploadBytes, getDownloadURL } from '@angular/fire/storage';
@Component({
selector: 'app-editor-with-upload',
template: `
<tinymce [(ngModel)]="content" [config]="config" />
`
})
export class EditorWithUploadComponent {
private storage = inject(Storage);
content = signal('');
config = {
height: 500,
plugins: ['image', 'link', 'media'],
toolbar: 'image link',
images_upload_handler: async (blobInfo: any, progress: any) => {
return this.uploadImage(blobInfo.blob(), blobInfo.filename());
},
automatic_uploads: true,
image_advtab: true,
image_caption: true,
image_title: true
};
private async (: , : ): <> {
{
storageRef = (., );
(storageRef, blob);
url = (storageRef);
url;
} (error) {
.(, error);
error;
}
}
}
Base64 Images (Simple)
config = {
plugins: ['image'],
toolbar: 'image',
images_upload_handler: (blobInfo: any) => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.readAsDataURL(blobInfo.blob());
});
}
};
Content Sanitization
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@Component({
selector: 'app-article-view',
template: `
<div [innerHTML]="sanitizedContent()"></div>
`
})
export class ArticleViewComponent {
private sanitizer = inject(DomSanitizer);
private content = signal('<p>Article content</p>');
sanitizedContent = computed(() =>
this.sanitizer.sanitize(SecurityContext.HTML, this.content()) || ''
);
}
Custom Plugins
Character Counter Plugin
config = {
plugins: ['wordcount'],
toolbar: 'wordcount',
setup: (editor: any) => {
editor.on('init', () => {
console.log('Editor initialized');
});
editor.on('change', () => {
const content = editor.getContent();
console.log('Content changed:', content.length);
});
editor.ui.registry.addButton('customButton', {
text: 'Custom Action',
onAction: () => {
editor.insertContent(' <strong>Custom content</strong> ');
}
});
}
};
Code Highlighting
config = {
plugins: ['codesample'],
toolbar: 'codesample',
codesample_languages: [
{ text: 'TypeScript', value: 'typescript' },
{ text: 'JavaScript', value: 'javascript' },
{ text: 'Python', value: 'python' },
{ text: 'Java', value: 'java' },
{ text: 'HTML/XML', value: 'markup' },
{ text: 'CSS', value: 'css' },
{ text: 'SQL', value: 'sql' }
],
codesample_global_prismjs: true
};
Templates
config = {
plugins: ['template'],
toolbar: 'template',
templates: [
{
title: 'Meeting Notes',
description: 'Template for meeting notes',
content: `
<h2>Meeting Notes</h2>
<p><strong>Date:</strong> </p>
<p><strong>Attendees:</strong> </p>
<h3>Agenda</h3>
<ol>
<li>Item 1</li>
<li>Item 2</li>
</ol>
<h3>Action Items</h3>
<ul>
<li>Action 1</li>
</ul>
`
},
{
title: 'Blog Post',
description: 'Template for blog posts',
content: `
<h1>Blog Title</h1>
<p><em>Published on: </em></p>
<h2>Introduction</h2>
<p>Introduction text...</p>
<h2>Main Content</h2>
<p>Main content...</p>
<h2>Conclusion</h2>
<p>Conclusion...</p>
`
}
]
};
Localization
config = {
language: 'zh_CN',
language_url: '/assets/tinymce/langs/zh_CN.js',
directionality: 'ltr'
};
Mobile Responsive
config = {
mobile: {
menubar: false,
toolbar: ['undo', 'redo', 'bold', 'italic']
},
toolbar: 'undo redo | bold italic underline | alignleft aligncenter alignright',
resize: 'both',
min_height: 300,
max_height: 800
};
Auto-Save
@Component({
selector: 'app-auto-save-editor',
template: `
<tinymce [(ngModel)]="content" [config]="config" />
<div class="save-status">
@if (saveStatus()) {
<nz-tag [nzColor]="saveStatus() === 'saved' ? 'success' : 'warning'">
{{ saveStatus() }}
</nz-tag>
}
</div>
`
})
export class AutoSaveEditorComponent {
content = signal('');
saveStatus = signal<'saving' | 'saved' | null>(null);
config = {
plugins: ['autosave'],
autosave_interval: '30s',
autosave_restore_when_empty: true,
setup: (editor: any) => {
editor.on('BeforeAutoSave', () => {
this.saveStatus.set('saving');
});
editor.on('AutoSave', () => {
this.saveContent();
});
}
};
private async saveContent(): Promise<void> {
try {
await ..(.());
..();
( ..(), );
} (error) {
.(, error);
}
}
}
Validation
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export function minTextLengthValidator(minLength: number): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const html = control.value || '';
const text = html.replace(/<[^>]*>/g, '').trim();
if (text.length < minLength) {
return { minTextLength: { required: minLength, actual: text.length } };
}
return null;
};
}
articleForm = this.fb.group({
content: ['', [
Validators.required,
minTextLengthValidator(100)
]]
});
Accessibility
config = {
a11y_advanced_options: true,
aria_label: 'Rich text editor',
plugins: ['help'],
toolbar: 'help',
auto_focus: false,
statusbar: true,
elementpath: true
};
Checklist
When integrating TinyMCE:
Best Practices
✅ DO
sanitizedContent = computed(() =>
this.sanitizer.sanitize(SecurityContext.HTML, this.content())
);
config = {
images_upload_handler: (blobInfo) => this.uploadImage(blobInfo)
};
content: ['', [Validators.required, minTextLengthValidator(50)]]
autosave_interval: '30s'
❌ DON'T
<div [innerHTML]="content"></div> // XSS vulnerability!
images_upload_url: '/upload'
References