resolve undefined this in onClick handler (#32576) (#32583)

* resolve undefined this in onClick handler

* add regression test for bound DisambiguatedProfileViewModel onClick

* Update docs

* add regression for sender profile click mention insertion

* Eslint Fix

(cherry picked from commit 134c7cde46447bf865fe1b3b136d2c1c4c6eb64f)

Co-authored-by: Zack <zazi21@student.bth.se>
This commit is contained in:
ElementRobot 2026-02-19 17:10:05 +01:00 committed by GitHub
parent faf3278a8e
commit d19208bee3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 59 additions and 3 deletions

View File

@ -127,6 +127,20 @@ export class FooViewModel extends BaseViewModel<FooViewSnapshot, Props> implemen
}
```
#### Binding of View Model Actions:
All view model actions must be defined as arrow functions to ensure they are bound to the class instance.
Using standard class methods can result in `this` being undefined when the function is passed as a callback (e.g. to a React event handler), which may cause runtime errors.
Correct pattern:
```ts
public doSomething = (): void => {
...
};
```
### `useViewModel` hook
Your view must call this hook with the view-model as argument. Think of this as your view subscribing to the view model.<br>

View File

@ -1008,6 +1008,34 @@ test.describe("Timeline", () => {
await expect(page.getByRole("button", { name: "Show video" })).toBeVisible();
await expect(page.locator("video")).not.toBeVisible();
});
test("should insert a mention when clicking sender profile in timeline", async ({
page,
app,
homeserver,
room,
}) => {
const senderDisplayName = "SenderBot";
const messageFromSender = "message from sender";
const bot = new Bot(page, homeserver, {
displayName: senderDisplayName,
autoAcceptInvites: false,
});
await bot.prepareClient();
await app.client.inviteUser(room.roomId, bot.credentials.userId);
await bot.joinRoom(room.roomId);
await bot.sendMessage(room.roomId, messageFromSender);
await app.viewRoomById(room.roomId);
const senderMessageTile = getEventTilesWithBodies(page).filter({ hasText: messageFromSender }).first();
await expect(senderMessageTile).toBeVisible();
await senderMessageTile.locator(".mx_DisambiguatedProfile").click();
await expect(app.getComposerField().getByText(senderDisplayName)).toBeVisible();
});
});
test.describe("message sending", { tag: ["@no-firefox", "@no-webkit"] }, () => {

View File

@ -141,7 +141,7 @@ export class DisambiguatedProfileViewModel
this.snapshot.set(DisambiguatedProfileViewModel.computeSnapshot(this.props));
}
public onClick(evt: MouseEvent<HTMLDivElement>): void {
public onClick = (evt: MouseEvent<HTMLDivElement>): void => {
this.props.onClick?.(evt);
}
};
}

View File

@ -75,13 +75,27 @@ describe("DisambiguatedProfileViewModel", () => {
const subscriber = jest.fn();
vm.subscribe(subscriber);
onClick({} as never);
vm.onClick?.({} as never);
expect(onClick).toHaveBeenCalledTimes(1);
expect(subscriber).not.toHaveBeenCalled();
expect(vm.getSnapshot()).toBe(prevSnapshot);
});
it("should keep onClick bound when extracted as a callback", () => {
const onClick = jest.fn();
const vm = new DisambiguatedProfileViewModel({
member,
fallbackName: "Fallback",
onClick,
});
const clickHandler = vm.onClick;
expect(() => clickHandler?.({} as never)).not.toThrow();
expect(onClick).toHaveBeenCalledTimes(1);
});
it("should emit snapshot update when fallbackName changes", () => {
const vm = new DisambiguatedProfileViewModel({
member: null,