"If a worker wants to do his job well, he must first sharpen his tools." - Confucius, "The Analects of Confucius. Lu Linggong"
Front page > Programming > Learning TDD by doing: Tagging members in Umbraco&#s Rich Text Editor

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

Published on 2024-11-02
Browse:844

Learning TDD by doing: Tagging members in Umbraco

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:


@D_Inventor


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
      ): Promise
        // 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;
}


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);
  });
});


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);
  });
});


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 {
    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);
  }
}


❓ 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.

Release Statement This article is reproduced at: https://dev.to/d_inventor/learning-tdd-by-doing-tagging-members-in-umbracos-rich-text-editor-29o4?1 If there is any infringement, please contact [email protected] delete
Latest tutorial More>

Disclaimer: All resources provided are partly from the Internet. If there is any infringement of your copyright or other rights and interests, please explain the detailed reasons and provide proof of copyright or rights and interests and then send it to the email: [email protected] We will handle it for you as soon as possible.

Copyright© 2022 湘ICP备2022001581号-3