Home  >  Article  >  Web Front-end  >  Learning TDD by doing: Tagging members in Umbraco&#s Rich Text Editor

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

Barbara Streisand
Barbara StreisandOriginal
2024-10-08 06:21:01904browse

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:


<mention user-id="1324" class="mceNonEditable">@D_Inventor</mention>


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


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


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

The above is the detailed content of Learning TDD by doing: Tagging members in Umbraco&#s Rich Text Editor. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn