Logo

dev-resources.site

for different kinds of informations.

Learning TDD by doing: Tagging members in Umbraco's Rich Text Editor

Published at
10/7/2024
Categories
webdev
javascript
umbraco
testing
Author
d_inventor
Categories
4 categories in total
webdev
open
javascript
open
umbraco
open
testing
open
Author
10 person written this
d_inventor
open
Learning TDD by doing: Tagging members in Umbraco's Rich Text Editor

In the system that I'm building, I need the ability to mention Umbraco members in text in the website. In order to do that, I need to build an extension to Umbraco's Rich Text Editor: TinyMCE.

Context

As a content editor, I want to tag members in a message or article so that they get notified about new content about them.

I looked at similar implementations, like in Slack or on X. Slack uses a special html tag for mentions during writing, but then sends the data to the backend with a token with a specific format. I decided to take a similar approach, but for now forget about the translation step. In content, a mention will look like this:

<mention user-id="1324" class="mceNonEditable">@D_Inventor</mention>
Enter fullscreen mode Exit fullscreen mode

Initial exploration

Before I started building, I was looking for ways to hook into TinyMCE in Umbraco. This is one of my least favourite things to extend in the Umbraco backoffice. I have done this before though, and I found it easiest to extend the editor if I create a decorator on Umbraco's tinyMceService in AngularJS. In TinyMCE's documentation, I found a feature called 'autoCompleters', which did exactly what I needed, so there was my hook into the editor. My initial code (without any testing yet), looked like this:

rtedecorator.$inject = ["$delegate"];
export function rtedecorator($delegate: any) {
  const original = $delegate.initializeEditor;

  $delegate.initializeEditor = function (args: any) {
    original.apply($delegate, arguments);

    args.editor.contentStyles.push("mention { background-color: #f7f3c1; }");
    args.editor.ui.registry.addAutocompleter("mentions", {
      trigger: "@",
      fetch: (
        pattern: string,
        maxResults: number,
        _fetchOptions: Record<string, unknown>
      ): Promise<IMceAutocompleteItem[]>
        // TODO: fetch from backend
        => Promise.resolve([{ type: "autocompleteitem", value: "1234", text: "D_Inventor" }]),
      onAction: (api: any, rng: Range, value: string): void => {
        // TODO: business logic
        api.hide();
      },
    });
  };

  return $delegate;
}
Enter fullscreen mode Exit fullscreen mode

I'm using vite and typescript in this project, but I don't have any types for TinyMCE installed. For now I'll keep the any and just try to avoid TinyMCE as much as possible.

Building with TDD

I decided to use jest for testing. I found an easy getting started and I quickly managed to get something working.

βœ… Success
I learned a new tool for unit testing in frontend code. I succesfully applied the tool to write a frontend with unit tests

I wrote my first test:

mention-manager.test.ts

describe("MentionsManager.fetch", () => {
  let sut: MentionsManager;
  let items: IMention[];

  beforeEach(() => {
    items = [];
    sut = new MentionsManager();
  });

  test("should be able to fetch one result", async () => {
    items.push({ userId: "1234", userName: "D_Inventor" });
    const result = await sut.fetch(1);
    expect(result).toHaveLength(1);
  });
});
Enter fullscreen mode Exit fullscreen mode

I was somewhat surprised by the strictness of the typescript compiler. Working in steps here really meant not adding anything that you aren't actually using yet. For example, I wanted to add a reference to the "UI", because I knew I was going to use that later, but I couldn't actually compile the MentionsManager until I used everything that I put in the constructor.

After a few rounds of red, green and refactor, I ended up with these tests:

mention-manager.test.ts

describe("MentionsManager.fetch", () => {
  let sut: MentionsManager;
  let items: IMention[];

  beforeEach(() => {
    items = [];
    sut = new MentionsManager(() => Promise.resolve(items));
  });

  test("should be able to fetch one result", async () => {
    items.push({ userId: "1234", userName: "D_Inventor" });
    const result = await sut.fetch(1);
    expect(result).toHaveLength(1);
  });

  test("should be able to fetch empty result", async () => {
    const result = await sut.fetch(1);
    expect(result).toHaveLength(0);
  });

  test("should be able to fetch many results", async () => {
    items.push({ userId: "1324", userName: "D_Inventor" }, { userId: "3456", userName: "D_Inventor2" });
    const result = await sut.fetch(2);
    expect(result).toHaveLength(2);
  });

  test("should return empty list upon error", () => {
    const sut = new MentionsManager(() => {
      throw new Error("Something went wrong while fetching");
    }, {} as IMentionsUI);
    return expect(sut.fetch(1)).resolves.toHaveLength(0);
  });
});
Enter fullscreen mode Exit fullscreen mode

With this logic in place, I could fetch mentions from any source and show them in the RTE through the 'fetch' hook.
I used the same approach to create a 'pick' method to take the selected member and insert the mention into the editor. This is the code that I ended up with:

mention-manager.ts

export class MentionsManager {
  private mentions: IMention[] = [];

  constructor(
    private source: MentionsAPI,
    private ui: IMentionsUI
  ) {}

  async fetch(take: number, query?: string): Promise<IMention[]> {
    try {
      const result = await this.source(take, query);
      if (result.length === 0) return [];
      this.mentions = result;

      return result;
    } catch {
      return [];
    }
  }

  pick(id: string, location: Range): void {
    const mention = this.mentions.find((m) => m.userId === id);
    if (!mention) return;

    this.ui.insertMention(mention, location);
  }
}
Enter fullscreen mode Exit fullscreen mode
❓ Uncertainty
The Range interface is a built-in type that is really difficult to mock and this interface leaks an implementation detail into my business logic. I feel like there might've been a better way to do this.

Retrospect

Overall, I think I ended up with simple code that is easy to change. There are still parts of this code that I don't really like. I wanted the business logic to drive the UI, but the code ended up more like a simple store that also does a single call to the UI. I wonder if I could more strongly wrap the UI to get more use out of the manager.

umbraco Article's
30 articles in total
Favicon
It doesn’t have to be complicated to build a custom Umbraco property editor
Favicon
please create a post for custom section's tree
Favicon
Umbraco package of the month: xStatic
Favicon
Umbraco and Bellissima: Swagger, Tokens, Entry Points
Favicon
Catching the Bus? How a Service Bus and Azure Functions Can Help Your Integration Reliability
Favicon
Driving Umbraco's dictionary to the edge (of your system)
Favicon
Managing Stale Carts in Umbraco Commerce
Favicon
Quick fix for IPublishedSnapshotAccessor issues when upgrading to Umbraco 15
Favicon
Umbraco package of the month: Auto dictionaries
Favicon
Umbraco package of the month: Uskinned
Favicon
Learning TDD by doing: Tagging members in Umbraco's Rich Text Editor
Favicon
Learning TDD by doing: Dealing with Umbraco's published content
Favicon
My Highlights from the Umbraco US Festival 2024
Favicon
Mastering Authorization in Umbraco 14/15: Real-World Management API Challenges and Solutions
Favicon
Adding Entity Actions to Trees in Umbraco 14
Favicon
Umbraco Forms 12+, Custom Field Type
Favicon
Make your own Umbraco Starter Kit without making a Starter Kit
Favicon
Adding Dynamic Routes to Umbraco 14 Sections
Favicon
Building Custom Trees in Umbraco 14 using menus!
Favicon
Doing custom sections in Umbraco 14
Favicon
Facetted search with Examine - Umbraco 13
Favicon
Server-side registering of Package Manifest in Umbraco 14
Favicon
How to skip login screens during development for Umbraco 13 Users and Members
Favicon
My journey through programmatically creating Umbraco `stuff`
Favicon
Deploying TypeScript Type Definitions for Umbraco v14 Packages
Favicon
Creating an Umbraco Backoffice Accessor for Conditional Page Rendering
Favicon
Create an API in Umbraco in 5 Minutes: A Quick Guide for Developers
Favicon
How to Create a New Umbraco Project: A Step-by-Step Guide
Favicon
Programmatically add a new 'Accepted file extension'
Favicon
Umbraco CodeGarden24

Featured ones: