Dans le système que je construis, j'ai besoin de pouvoir mentionner les membres d'Umbraco dans le texte du site Web. Pour ce faire, je dois créer une extension pour l'éditeur de texte enrichi d'Umbraco : TinyMCE.
En tant qu'éditeur de contenu, je souhaite identifier les membres dans un message ou un article afin qu'ils soient informés du nouveau contenu les concernant.
J'ai regardé des implémentations similaires, comme dans Slack ou sur X. Slack utilise une balise html spéciale pour les mentions lors de l'écriture, mais envoie ensuite les données au backend avec un jeton avec un format spécifique. J'ai décidé d'adopter une approche similaire, mais pour l'instant, oubliez l'étape de traduction. En contenu, une mention ressemblera à ceci :
<mention user-id="1324" class="mceNonEditable">@D_Inventor</mention>
Avant de commencer à construire, je cherchais des moyens de me connecter à TinyMCE à Umbraco. C'est l'une des choses que j'aime le moins étendre dans le backoffice d'Umbraco. Cependant, je l'ai déjà fait et j'ai trouvé plus facile d'étendre l'éditeur si je crée un décorateur sur tinyMceService d'Umbraco dans AngularJS. Dans la documentation de TinyMCE, j'ai trouvé une fonctionnalité appelée « autoCompleters », qui faisait exactement ce dont j'avais besoin, ce qui me permettait de me connecter à l'éditeur. Mon code initial (sans aucun test encore), ressemblait à ceci :
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; }
J'utilise vite et typescript dans ce projet, mais je n'ai installé aucun type pour TinyMCE. Pour l'instant, je vais garder le any et essayer d'éviter TinyMCE autant que possible.
J'ai décidé d'utiliser Jest pour les tests. J'ai trouvé un démarrage facile et j'ai rapidement réussi à faire fonctionner quelque chose.
✅ Success |
---|
I learned a new tool for unit testing in frontend code. I succesfully applied the tool to write a frontend with unit tests |
J'ai écrit mon premier 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); }); });
J'ai été quelque peu surpris par la rigueur du compilateur TypeScript. Travailler par étapes ici signifiait vraiment ne rien ajouter que vous n’utilisiez pas encore. Par exemple, je voulais ajouter une référence à "UI", parce que je savais que j'allais l'utiliser plus tard, mais je ne pouvais pas réellement compiler MentionsManager tant que j'avais utilisé tout ce que j'avais mis dans le constructeur.
Après quelques tours de rouge, vert et refactor, je me suis retrouvé avec ces 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); }); });
Avec cette logique en place, je pourrais récupérer les mentions de n'importe quelle source et les afficher dans le RTE via le hook 'fetch'.
J'ai utilisé la même approche pour créer une méthode « pick » pour prendre le membre sélectionné et insérer la mention dans l'éditeur. Voici le code avec lequel je me suis retrouvé :
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. |
Dans l’ensemble, je pense que je me suis retrouvé avec un code simple et facile à modifier. Il y a encore des parties de ce code que je n'aime pas vraiment. Je voulais que la logique métier pilote l'interface utilisateur, mais le code ressemblait davantage à un simple magasin qui effectue également un seul appel à l'interface utilisateur. Je me demande si je pourrais envelopper plus fortement l'interface utilisateur pour mieux utiliser le gestionnaire.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!