ホームページ > ウェブフロントエンド > jsチュートリアル > TDD の学習: Umbraco のリッチ テキスト エディターでメンバーにタグ付けする

TDD の学習: Umbraco のリッチ テキスト エディターでメンバーにタグ付けする

Barbara Streisand
リリース: 2024-10-08 06:21:01
オリジナル
976 人が閲覧しました

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:

1

<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:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

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.

以上がTDD の学習: Umbraco のリッチ テキスト エディターでメンバーにタグ付けするの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
著者別の最新記事
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート