Blueprint by Tiny
Return to Tiny.cloud
Return to Tiny.cloudTry TinyMCE for Free
Search by

Create an Angular reactive form with a rich text editor

Simon Fjeldså

February 12th, 2020

Written by

Simon Fjeldså
Simon Fjeldså

Category

Tips & How-Tos

Tagged

With Angular being a framework that aims to cater to the developer’s every need, with the explicit goal of being the go-to framework for large single-page applications, it incorporates many features to simplify the development process, including reactive forms.

In this article, we're going to take a look at how to integrate rich text editing in a reactive form using TinyMCE.

In addition to getting the rich text editor going inside a form, we’re going to build in some custom validation, as well as use skinning to match the style of the editor with the popular Angular Material component library.

NOTE: If you just want to get started with the basic setup in Angular, check out our previous article on how to add the TinyMCE rich text editor to a simple Angular project.

Complete example on CodeSandbox:

Prerequisites

Since this post focuses on using the tinymce-angular component, some prior knowledge of Angular is assumed. Although knowledge of Angular Material isn't necessary, it could be helpful. For anyone that's new to Angular Material and wants to learn more about it, there's a great getting started guide on their website.

To view the full source and the associated boilerplate code for this project, open up the embedded CodeSandbox example above.

Getting started

To get started, we'll need to install the tinymce-angular component with our package manager of choice.

# If you use npm
$ npm install @tinymce/tinymce-angular
# If you use Yarn
$ yarn add @tinymce/tinymce-angular

The tinymce-angular component is a wrapper around the TinyMCE rich text editor and thus requires TinyMCE in order to work. By default, the component will load TinyMCE from Tiny Cloud, which is the simplest and quickest way to get going. 

The only thing we need for this is an API key which anyone can get for free at tiny.cloud (including a 30 day trial of the premium plugins). The alternative is to self-host TinyMCE and make it available, together with the assets it requires. Refer to the documentation for more information.

Importing tinymce-angular

The first thing we have to do is make the tinymce-angular component available in our project’s module. To do this, we'll import the EditorModule from the tinymce-angular package and add it to the imports array.

import { EditorModule } from '@tinymce/tinymce-angular';

@NgModule({
  /* ... */
  imports: [
    EditorModule
  ]
  /* ... */
})
class FormModule {}

form.module.ts

Once the tinymce-angular component is imported, we can add it to our template together with any configuration we wish to have.

Configuring tinymce-angular

Throughout this guide, we’ll focus on the core of the form and the related logic, with some parts left out for brevity. To see the full project, and how the dialog has been built, take a closer look at the CodeSandbox example.

The tinymce-angular component accepts a property where you can provide your Tiny Cloud API key. All we need to do is insert the API key that we got at tiny.cloud, and we'll instantly have the configuration required to have a fully-fledged rich text editor working!

<editor
  apiKey="Get your free API key at tiny.cloud and paste it here"
  [init]="{
    icons: 'material',
    skin: 'borderless',
    plugins: 'wordcount',
    menubar: false,
    min_height: 150
  }"
></editor>

form.component.html

The other property we're using, init, accepts an object specifying the editor configuration. 

The Tiny Skins and Icon Packs premium plugin makes it possible to customize the appearance of TinyMCE. In this project, we’re using Material icons and the Borderless skin but there are many more ready-made ones to choose from. If you’ve just signed up for your Tiny Cloud API key, you have access to all the icons and skins (and all the premium plugins) for 30 days. So you are free to try them all out!

In this example, we've also added the WordCount plugin. In addition to displaying stats for word and character usage to the user in the status bar, this plugin also exposes some very useful APIs that we're going to use at a later stage.

TinyMCE rich text editor customized with Material icons, Borderless skin and WordCount plugin.

Constructing the form

For someone who hasn't worked much with Angular, much of what follows may seem like magic, but don’t be put off by what may seem to be a strange syntax. Gaining an intuitive feeling for how the large parts stick together is the first step to a deeper understanding.

We're going to construct our form as a reactive form, where all fields are bound to a model. The first thing we’ll do is create the connectors from the view to our model (which we’re going to build in the next section).

<form [formGroup]="myForm">
  <input [formControl]="myForm.controls.title"/>
  <editor
    [formControl]="myForm.controls.body"
     <!-- *** -->
  ></editor>
</form>

The FormGroupDirective will allow us to associate the form with a FormGroup instance, which can be thought of as the root of the model. Here we're simply going to assume that our model will be called “myForm”, but it could be named anything. Next, each field of our form will need a way to be associated with our model. To do this, we’ll use the FormControlDirective in a similar fashion. This will allow us to associate each field with  FormControl instances, which can be thought of as subcomponents of the model.

To recap:

  • The form-element will be associated with a FormGroup instance via the FormGroupDirective
  • Each field (input & editor) will be associated with a FormControl instance via the FormControlDirective

Constructing the model

The next step is to create the model our form is going to be associated with. With the FormGroup and FormControl constructors, we create the root of our model as well as its two subcomponents. Take a look at the code below and see how we now use the names as associated with the respective directive in the template earlier, namely “myForm”, “title” and “body”. The directives will take care of the actual mapping between model and view, and all we have to do is make sure we provide the correct names.

import { FormControl, FormGroup, Validators } from '@angular/forms';
import { Component } from "@angular/core";
import { AsyncSubject, Subject } from 'rxjs';
import { maxLength } from './maxlength.validator';

@Component({
  selector: "form-root",
  styleUrls: ["./form.component.css"],
  templateUrl: "./form.component.html"
})
class FormComponent {
  private editorSubject: Subject<any> = new AsyncSubject();

  public myForm = new FormGroup({
    title: new FormControl("", Validators.required),
    body: new FormControl("", Validators.required, maxLength(this.editorSubject, 10))
  });

  handleEditorInit(e) {
    this.editorSubject.next(e.editor);
    this.editorSubject.complete();
  }
}

form.component.ts

Besides tracking values between our view and model, reactive forms also provide a simple way to track the validity of the form and its fields. As an example, notice how we use the “Validators.required” validator provided by Angular. This is simply a function which will mark the form as invalid until a user has entered some text into each field, at which point the form will enter a valid state. With the help of this validity flag, we’ll be able to provide useful information to the user, such as error messages. In the last section of this guide, we’ll take a look at how this is done!

Read more about different validators provided by Angular.

The FormControl constructors may take an optional argument called an async validator. In our project, we’re going to create a custom async maxLength validator, which we'll use to limit the number of characters a user may enter into the editor. The reason for using an async validator is because TinyMCE is initialized asynchronously, and during this initialization process, we can't perform any validation that requires access to the editor. Usage of an asynchronous validator allows us to set the specific FormControl (and implicitly the whole form) into a pending state until our validator has been resolved.

For our maxLength validator, we require access to the editor instance, but since the editor is created asynchronously, we don't have access to it until a later point in time. This is why we'll use an AsyncSubject from RxJS to represent this value. This will allow us to handle the editor as a value even though it's actually not resolved until a later point.

Finally, we're using our binding to the onInit event from tinymce-angular via the handleEditorInit method to get the editor instance. We're emitting this value to the editorSubject, which will then, upon completion, emit this value to our validation logic inside the maxLength validator.

Our function, which produces the validator function, has a bit of boilerplate to accommodate our asynchronous editor value. Except for this, it works very much like any async validator function in Angular. It takes a controller and emits either errors or null (representing no error).

import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
import { Observable, Subject } from 'rxjs';
import { map } from 'rxjs/operators';

const maxLength = (editorSubject: Subject<any>, characterLimit: number): AsyncValidatorFn => {
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    return editorSubject.pipe(map((editor) => {
      /* Actual validation logic here */
    }));
  };
}

maxlength.validator.ts

What’s missing in the validation function is the actual validation logic. Here we're using the API of the WordCount plugin to retrieve the character count of the editor. By taking this approach, we're utilizing the powerful character ruleset provided by the WordCount plugin to get accurate results.

In case of an error, we're returning an object with the error name (maxlength) and additional information about it. This is useful information for crafting good error messages.

import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
import { Observable, Subject } from 'rxjs';
import { map } from 'rxjs/operators';

const maxLength = (editorSubject: Subject<any>, characterLimit: number): AsyncValidatorFn => {
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    return editorSubject.pipe(map((editor) => {
      const characterCount = editor.plugins.wordcount.body.getCharacterCount();

      return characterCount <= characterLimit ? null : {
        maxlength: {
          requiredLength: characterLimit,
          actualLength: characterCount
        }
      };
    }));
  };
}

maxlength.validator.ts

Read more about Async Validation.

Piecing together the template

The remaining pieces required for our template include some structural elements and classes for styling. Much of this is from Angular Material, which illustrates how easy it is to get the TinyMCE rich text editor to look and feel as if it were from the Angular Material component library all along!

mat-error from Angular Material helps us display formatted error messages for our form. We'll control the error messages by using the NgIf directive, which in turn will evaluate the status of our form via the FormGroup and FormControl instances that we associated with our form.

<form (ngSubmit)="onSubmit()" [formGroup]="myForm">
  <mat-form-field class="title">
    <input
      matInput
      [formControl]="myForm.controls.title"
      placeholder="Title"
      type="text"/>
    <mat-error *ngIf="myForm.controls.title.hasError('required')">
      Title is required
    </mat-error>
  </mat-form-field>
  <div class="mat-form-field-wrapper">
    <editor
      apiKey="Get your free API key at tiny.cloud and paste it here"
      [formControl]="myForm.controls.body"
      class="mat-elevation-z1"
      (onInit)="handleEditorInit($event)"
      [init]="{
        icons: 'material',
        skin: 'borderless',
        plugins: 'wordcount',
        menubar: false,
        min_height: 150
      }"
    ></editor>
    <div class="mat-form-field-subscript-wrapper">
      <mat-error *ngIf="form.controls.body.hasError('maxlength')">
        Your post exceeds exceeds the character limit
        {{ myForm.controls.body.getError('maxlength').actualLength }} /
        {{ myForm.controls.body.getError('maxlength').requiredLength }}
      </mat-error>
      <mat-error *ngIf="myForm.controls.body.touched && myForm.controls.body.hasError('required')">
        This form is required
      </mat-error>
    </div>
  </div>
</form>

form.component.html

Wrapping up

That’s it!

We now have a form with powerful editing capabilities provided by the TinyMCE rich text editor, together with a sleek Material look and custom input validation.

Angular reactive form with rich text editor and character limit error message displayed.

From here, the possibilities are endless. What are you doing with TinyMCE in Angular? Connect with us on Twitter and let us know what you’re up to.

Also remember to check out the full range of TinyMCE rich text editor plugins and how they can help you provide the most productive editing experience for your users.

Happy coding!

Angular
Simon Fjeldså
bySimon Fjeldså

Simon is an Engineer at Tiny, working on an array of features such as plugins and framework integrations for TinyMCE. Powered by coffee.

Related Articles

  • Visual depiction of Tiny and Angular integration
    Tips & How-Tos

    How to add TinyMCE 5 to a simple Angular project

    by Simon Fjeldså in Tips & How-Tos

Build beautiful content for the web with Tiny.

The rich text editing platform that helped launch Atlassian, Medium, Evernote and more.

Begin my FREE 30 day trial
Tiny Editor