Angular Application

  • 10 minutes to read

This topic describes how to add the client-side RichEdit control to an Angular application.

View Example: RichEdit Angular Application View Example: RichEdit for Angular - How to customize the built-in ribbon

TIP

You can import RichEdit types from modules or access them from a global namespace.

Code samples in this documentation use the global namespace approach. RichEdit types are accessed via the following prefix:

[window.]DevExpress.RichEdit. (for instance, DevExpress.RichEdit.DocumentFormat)

To use the modular approach, do the following.

  • Remove the DevExpress.RichEdit. prefix from type references.
  • Import the types from the RichEdit npm package, for instance:

    import { DocumentFormat } from 'devexpress-richedit';

    Note, the public RichEdit API is contained in the devexpress-richedit module. The sub-modules contain the private API.

Prerequisites

Requirements

  • To use the RichEdit control in an Angular application, you need to have a Universal, DXperience, or ASP.NET subscription.
  • Versions of the devexpress npm packages should be identical (their major and minor versions should be the same).

Create Application with RichEdit

Create Angular Application

In the command prompt, create an Angular application and navigate to the newly created folder:

ng new richedit-angular-app
cd richedit-angular-app

Install RichEdit Package

The devexpress-richedit npm package references devextreme as peerDependencies. The peerDependencies packages should be installed manually. This allows developers to control the peerDependencies package version and guarantees that the devextreme package is installed once.

Install a RichEdit package with required peerDependencies:

npm install devextreme devexpress-richedit --save

You can find all libraries in the node_modules folder after installation is complete.

Create New RichEdit Component

In the console window, use the ng generate component command to generate a new RichEdit component:

ng generate component richedit

Configure Component Class

Replace the content of the richedit.component.ts file with the code below to import the required decorators and modules, and configure the RichEdit component:

import { Component, ElementRef, OnDestroy, AfterViewInit } from '@angular/core';
import { create, createOptions, RichEdit } from 'devexpress-richedit';

@Component({
  selector: 'app-richedit',
  template: '<div></div>',
  styleUrls: ['./richedit.component.css']
})
export class RicheditComponent implements AfterViewInit, OnDestroy {
  private rich: RichEdit;

  constructor(private element: ElementRef) { }

  ngAfterViewInit(): void {
    const options = createOptions();
    // set options
    options.width = '1700px';
    options.height = '800px';
    this.rich = create(this.element.nativeElement.firstElementChild, options);
  }

  ngOnDestroy() {
    if (this.rich) {
      this.rich.dispose();
      this.rich = null;
    }
  }
}

Add Global Styles

Add the following global style references to the angular.json file:

"styles": [
  "node_modules/devextreme/dist/css/dx.light.css",
  "node_modules/devexpress-richedit/dist/dx.richedit.css",
  "src/styles.css"
],

Register Application Component

To add a component reference, insert the following code at the beginning of the src/app/app.component.html file:

<app-richedit></app-richedit>
<!-- ... -->

Run the Application

Run the application.

ng serve --open

Spell Check

The RichEdit control includes API to implement spell check based on third-party tools. The API includes the following members that are available via the spellCheck option.

Property Description
enabled Specifies whether spell check is enabled.
checkWordSpelling A function that checks the spelling of a word.
suggestionCount The maximum number of suggested words that can be displayed in the context menu.
addWordToDictionary A function that is called when a user clicks the Add to Dictionary context menu command.

Context menu with suggested words

Create an Nspell Bundle for the RichEdit

The RichEdit npm package includes a webpack configuration file that allows you to compile the nspell library and required dictionaries into a package bundle. The English dictionary is included by default.

Follow the steps below to compile the bundle.

  1. Run the following command within the node_modules/devexpress-richedit directory:

    npm i --save-dev
    

    If you require additional dictionaries, install them as follows. (Note that the command below installs a French dictionary.)

    npm i dictionary-fr
    
  2. Add the export and import directives for each additional dictionary to the node_modules/devexpress-richedit/bin/nspell-index.js file.

  3. Run the following command that builds an NSpell bundle according to the node_modules/devexpress-richedit/bin/nspell.webpack.config.js configuration file.

    npm run build-nspell
    

    This command creates the node_modules/devexpress-richedit/dist/custom/nspell.js file. A script within the file adds the NSpell object to the JavaScript window object.

Use Nspell with a Web Worker

  1. To create a web worker, run the following code in the application folder.

    ng generate web-worker spellchecker
    
  2. Replace the content of the newly created src\app\spellchecker.worker.ts file with the code below.

    /// <reference lib="webworker" />
    import 'devexpress-richedit/dist/custom/nspell';
    declare const NSpell: any;
    
    let checkers: any[];
    
    function checkWord(word: string) {
      for (const checker of checkers) {
        if (checker.correct(word)) {
          return true;
        }
      }
      return false;
    }
    
    addEventListener('message', ({ data }) => {
      if (!checkers) {
        checkers = [];
        NSpell.dictionaries.forEach(dic => {
          checkers.push(new NSpell.nspell(dic));
        });
      }
      switch (data.command) {
        case 'checkWord': {
          const isCorrect = checkWord(data.word);
          postMessage({
            id: data.id,
            isCorrect,
            suggestions: isCorrect ? undefined : checkers[0].suggest(data.word),
          });
          break;
        }
        case 'addWord': {
          checkers[0].add(data.word);
          break;
        }
      }
    });
    
  3. Generate a spell checker service.

    ng generate service spellchecker
    
  4. Replace the content of the newly created src\app\spellchecker.service.ts file with the code below.

    import { Injectable } from '@angular/core';
    
    export type RichEditSpellcheckerCallback = (correct: boolean, suggestions: string[]) => void;
    
    @Injectable({
      providedIn: 'root'
    })
    export class SpellcheckerService {
      private spellCheckerWorker?: Worker;
      private spellCheckerCallbacks: Record<number, RichEditSpellcheckerCallback> = {};
      private spellCheckerWorkerCommandId = 0;
    
      constructor() { }
    
      checkWordSpelling(word: string, callback: RichEditSpellcheckerCallback) {
        if (!this.spellCheckerWorker) {
          const myDictionary = JSON.parse(localStorage.getItem('myDictionary')) || [];
          this.spellCheckerWorker = new Worker('./spellchecker.worker', { type: 'module' });
          myDictionary.forEach(word =>  {
            this.spellCheckerWorker.postMessage({
              command: 'addWord',
              word,
            });
          });
          this.spellCheckerWorker.onmessage = e => {
            const savedCallback = this.spellCheckerCallbacks[e.data.id];
            delete this.spellCheckerCallbacks[e.data.id];
            savedCallback(e.data.isCorrect, e.data.suggestions);
          };
        }
        const currId = this.spellCheckerWorkerCommandId++;
        this.spellCheckerCallbacks[currId] = callback;
        this.spellCheckerWorker.postMessage({
          command: 'checkWord',
          word,
          id: currId,
        });
      }
    
      addWordToDictionary(word: string) {
        const myDictionary = JSON.parse(localStorage.getItem('myDictionary')) || [];
        myDictionary.push(word);
        localStorage.setItem('myDictionary', JSON.stringify(myDictionary));
        this.spellCheckerWorker.postMessage({
          command: 'addWord',
          word,
        });
      }
    }
    
  5. Add the following code to the RichEdit component.

    // Import the SpellcheckerService class to the Rich Edit component.
    import { SpellcheckerService } from '../spellchecker.service';
    ...
    // Update the RichEdit constructor to the following:
    constructor(private element: ElementRef, private spellcheckerService: SpellcheckerService) { }
    ...
    // Add the following options to the component:
    options.spellCheck.enabled = true;
    options.spellCheck.suggestionCount = 5;
    options.spellCheck.checkWordSpelling = (word, callback) => this.spellcheckerService.checkWordSpelling(word, callback);
    options.spellCheck.addWordToDictionary = (word) => this.spellcheckerService.addWordToDictionary(word);
    

Localization

This section describes how to localize the ASP.NET Core RichEdit in an Angular application.

  1. Download translated resources from the DevExpress Localization Service. To learn more, see the following help topic: Localization Service.

  2. Unpack the self-extracting archive to get a folder with the localization resources.

  3. Copy the extracted resource file ~\DevExpressLocalizedResources_21.1_{lang}\json resources\dx-rich.{lang}.json to the node_modules\devexpress-richedit\localization folder.

  4. Add the following options in the compilerOptions section of the tsconfig.json file:

    {
      ...
      "compilerOptions": {
        ...
        "resolveJsonModule": true,
        "allowSyntheticDefaultImports": true,
      }
    }
    
  5. Add the following code in the src/app/app.component.ts file. Note that the code sample below imports the French localization.

    import { loadMessages, locale } from 'devextreme/localization';
    import RELocalization from 'devexpress-richedit/localization/dx-rich.fr.json';
    ...
    // load localized messages and specify the locale identifier in the constructor of the AppComponent class
    export class AppComponent {
        constructor() {
            loadMessages({ fr: RELocalization });
            locale("fr");
        }
        ...
    }
    

Custom Localization Strings

Rich Text Editor allows you to display custom localization strings for its UI elements. Use the localizationId property to specify an element’s identifier for localization. Note that if an element’s text is specified, the localization string is not applied.

// creates a custom button item with localization identifier
var sendEmailButton = new RibbonButtonItem("sendEmail", "", {
    icon: "email", showText: true, localizationId: "CustomStrings.SendEmailCommand"
});
// creates a custom tab with localization identifier
options.ribbon.insertTab(new RibbonTab("", "customTab", [sendEmailButton],"CustomStrings.CustomTab"), 1);

Declare custom localization strings in a separate JSON file:

{
  "CustomStrings.SendEmailCommand":"Envoyer un e-mail",
  "CustomStrings.CustomTab":"Onglet personnalisé"
}

Import the custom localization strings to your project.

import { loadMessages, locale } from 'devextreme/localization';
import RELocalization from 'devexpress-richedit/localization/dx-rich.fr.json';
import RELocalizationCustom from 'devexpress-richedit/localization/dx-rich-custom.fr.json';
...
export class AppComponent {
  constructor(){
    loadMessages({ fr: RELocalization });
    loadMessages({ fr: RELocalizationCustom });
    locale("fr");
    ...

Examples

How to Set RichEdit Options

The code sample below demonstrates how you can set RichEdit options.

// the createOptions() method creates an object that contains RichEdit options initialized with default values
const options = createOptions();

options.bookmarks.visibility = true;
options.bookmarks.color = '#ff0000';

options.confirmOnLosingChanges.enabled = true;
options.confirmOnLosingChanges.message = 'Are you sure you want to perform the action? All unsaved document data will be lost.';

options.fields.updateFieldsBeforePrint = true;
options.fields.updateFieldsOnPaste = true;

options.mailMerge.activeRecord = 2;
options.mailMerge.viewMergedData = true;
options.mailMerge.dataSource = [
    { Name: 'Indy', age: 32 },
    { Name: 'Andy', age: 28 },
];

// events
options.events.activeSubDocumentChanged = () => { };
options.events.autoCorrect = () => { };
options.events.calculateDocumentVariable = () => { };
options.events.characterPropertiesChanged = () => { };
options.events.contentInserted = () => { };
options.events.contentRemoved = () => { };
options.events.documentChanged = () => { };
options.events.documentFormatted = () => { };
options.events.documentLoaded = () => { };
options.events.gotFocus = () => { };
options.events.hyperlinkClick = () => { };
options.events.keyDown = () => { };
options.events.keyUp = () => { };
options.events.paragraphPropertiesChanged = () => { };
options.events.lostFocus = () => { };
options.events.pointerDown = () => { };
options.events.pointerUp = () => { };
options.events.saving = () => { };
options.events.saved = () => { };
options.events.selectionChanged = () => { };    
options.events.customCommandExecuted = (s, e) => {
    switch (e.commandName) {
    case 'insertEmailSignature':
        s.document.insertParagraph(s.document.length);
        s.document.insertText(s.document.length, '_________');
        s.document.insertParagraph(s.document.length);
        s.document.insertText(s.document.length, 'Best regards,');
        s.document.insertParagraph(s.document.length);
        s.document.insertText(s.document.length, 'John Smith');
        s.document.insertParagraph(s.document.length);
        s.document.insertText(s.document.length, 'john@example.com');
        s.document.insertParagraph(s.document.length);
        s.document.insertText(s.document.length, '+1 (818) 844-0000');
        break;
    }
};

options.unit = RichEditUnit.Inch;

options.view.viewType = ViewType.PrintLayout;
options.view.simpleViewSettings.paddings = {
  left: 15,
  top: 15,
  right: 15,
  bottom: 15,
};

options.autoCorrect = {
    correctTwoInitialCapitals: true,
    detectUrls: true,
    enableAutomaticNumbering: true,
    replaceTextAsYouType: true,
    caseSensitiveReplacement: false,
    replaceInfoCollection: [
        { replace: "wnwd", with: "well-nourished, well-developed" },
        { replace: "(c)", with: "©" }
    ],
};
// capitalize the first letter at the beginning of a new sentence/line
options.events.autoCorrect = function (s, e) {
    if (e.text.length == 1 && /\w/.test(e.text)) {
        var prevText = s.document.getText(new Interval(e.interval.start - 2, 2));
        if (prevText.length == 0 || /^(\. |\? |\! )$/.test(prevText) || prevText.charCodeAt(1) == 13) {
            var newText = e.text.toUpperCase();
            if (newText != e.text) {
                s.beginUpdate();
                s.history.beginTransaction();
                s.document.deleteText(e.interval);
                s.document.insertText(e.interval.start, newText);
                s.history.endTransaction();
                s.endUpdate();
                e.handled = true;
            }
        }
    }
};  

options.exportUrl = 'https://siteurl.com/api/';

options.readOnly = false;
options.width = '800px';
options.height = '1200px';

How to Customize the Ribbon

The code sample below modifies the built-in ribbon.

ribbonCustomization(options: Options) {
    // remove items
    const fileTab = options.ribbon.getTab(RibbonTabType.File);
    fileTab.removeItem(FileTabItemId.OpenDocument);

    const homeTab = options.ribbon.getTab(RibbonTabType.Home);
    homeTab.removeItem(HomeTabItemId.Copy);
    homeTab.removeItem(HomeTabItemId.Cut);
    homeTab.removeItem(HomeTabItemId.Paste);

    homeTab.removeItem(HomeTabItemId.DecreaseFontSize);
    homeTab.removeItem(HomeTabItemId.IncreaseFontSize);

    homeTab.removeItem(HomeTabItemId.ChangeStyle);

    // remove element from drop down list
    // fileTab.removeItem(FileTabItemId.DownloadRtf);
    // fileTab.removeItem(FileTabItemId.DownloadTxt);

    // allow download in docx format only. Create an item based on the default item options.
    const downloadDocxItem = fileTab.getItem(FileTabItemId.DownloadDocx) as RibbonSubMenuItem;
    const downloadItem = fileTab.getItem(FileTabItemId.Download) as RibbonMenuItem;
    fileTab.removeItem(FileTabItemId.Download);
    // icons: https://js.devexpress.com/Documentation/Guide/Themes_and_Styles/Icons/
    fileTab.insertItem(new RibbonButtonItem(downloadDocxItem.id, downloadItem.text, {icon: downloadItem.icon, showText: true}), 2);

    // move items to new tab
    const findElem: RibbonItem = homeTab.getItem(HomeTabItemId.Find);
    const replaceElem: RibbonItem = homeTab.getItem(HomeTabItemId.Replace);
    homeTab.removeItem(findElem);
    homeTab.removeItem(replaceElem);

    // remove tab
    options.ribbon.removeTab(RibbonTabType.References);

    // insert tab
    const findTabId = 'FindTabId';
    const newFindTab = options.ribbon.insertTab(new RibbonTab('Find', findTabId, [findElem, replaceElem]), 2);

    // add custom item
    const findInGoogleId = 'FindInGoogleId';
    newFindTab.insertItem(new RibbonButtonItem(findInGoogleId, 'Find in Google', { beginGroup: true }));
    options.events.customCommandExecuted = (s, e) => {
      if (e.commandName === findInGoogleId) {
        const selectedInterval = s.selection.intervals[0];
        if (selectedInterval.length > 0) {
          const selectedText = s.selection.activeSubDocument.getText(selectedInterval);
          window.open(`https://www.google.com/search?q=${selectedText}`, '_blank');
        }
      }
    };
}