Skip to content

Commit 8445ebc

Browse files
rubennortefacebook-github-bot
authored andcommitted
Create module to handle event handler attributes (#48920)
Summary: Pull Request resolved: #48920 Changelog: [internal] Implements a module with helpers to define event handler IDL attributes in classes extending `EventTarget`. E.g.: ``` import {getEventHandlerAttribute, setEventHandlerAttribute} from '../path/to/EventHandlerAttributes'; class EventTargetSubclass extends EventTarget { get oncustomevent(): EventListener | null { return getEventHandlerAttribute(this, 'customEvent'); } set oncustomevent(listener: EventListener | null) { setEventHandlerAttribute(this, 'customEvent', listener); } } const eventTargetInstance = new EventTargetSubclass(); eventTargetInstance.oncustomevent = (event: Event) => { console.log('custom event received'); }; eventTargetInstance.dispatchEvent(new Event('customEvent')); // Logs 'custom event received' to the console. eventTargetInstance.oncustomevent = null; eventTargetInstance.dispatchEvent(new Event('customEvent')); // Does not log anything to the console. ``` Reviewed By: javache Differential Revision: D67839560 fbshipit-source-id: f301d174fa9a4c010940b875cadfc31298947c26
1 parent 9fbc02a commit 8445ebc

File tree

3 files changed

+340
-5
lines changed

3 files changed

+340
-5
lines changed
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict
8+
* @format
9+
*/
10+
11+
/**
12+
* This module provides helpers for classes to implement event handler IDL
13+
* attributes, as defined in https://html.spec.whatwg.org/multipage/webappapis.html#event-handler-idl-attributes.
14+
*
15+
* Expected usage:
16+
* ```
17+
* import {getEventHandlerAttribute, setEventHandlerAttribute} from '../path/to/EventHandlerAttributes';
18+
*
19+
* class EventTargetSubclass extends EventTarget {
20+
* get oncustomevent(): EventListener | null {
21+
* return getEventHandlerAttribute(this, 'customEvent');
22+
* }
23+
*
24+
* set oncustomevent(listener: EventListener | null) {
25+
* setEventHandlerAttribute(this, 'customEvent', listener);
26+
* }
27+
* }
28+
*
29+
* const eventTargetInstance = new EventTargetSubclass();
30+
*
31+
* eventTargetInstance.oncustomevent = (event: Event) => {
32+
* console.log('custom event received');
33+
* };
34+
* eventTargetInstance.dispatchEvent(new Event('customEvent'));
35+
* // Logs 'custom event received' to the console.
36+
*
37+
* eventTargetInstance.oncustomevent = null;
38+
* eventTargetInstance.dispatchEvent(new Event('customEvent'));
39+
* // Does not log anything to the console.
40+
* ```
41+
*/
42+
43+
import type EventTarget from './EventTarget';
44+
import type {EventCallback} from './EventTarget';
45+
46+
type EventHandler = $ReadOnly<{
47+
handleEvent: EventCallback,
48+
}>;
49+
type EventHandlerAttributeMap = Map<string, EventHandler | null>;
50+
51+
const EVENT_HANDLER_CONTENT_ATTRIBUTE_MAP_KEY = Symbol(
52+
'eventHandlerAttributeMap',
53+
);
54+
55+
function getEventHandlerAttributeMap(
56+
target: EventTarget,
57+
): ?EventHandlerAttributeMap {
58+
// $FlowExpectedError[prop-missing]
59+
return target[EVENT_HANDLER_CONTENT_ATTRIBUTE_MAP_KEY];
60+
}
61+
62+
function setEventHandlerAttributeMap(
63+
target: EventTarget,
64+
map: ?EventHandlerAttributeMap,
65+
) {
66+
// $FlowExpectedError[prop-missing]
67+
target[EVENT_HANDLER_CONTENT_ATTRIBUTE_MAP_KEY] = map;
68+
}
69+
70+
/**
71+
* Returns the event listener registered as an event handler IDL attribute for
72+
* the given target and type.
73+
*
74+
* Should be used to get the current value for `target.on{type}`.
75+
*/
76+
export function getEventHandlerAttribute(
77+
target: EventTarget,
78+
type: string,
79+
): EventCallback | null {
80+
const listener = getEventHandlerAttributeMap(target)?.get(type);
81+
return listener != null ? listener.handleEvent : null;
82+
}
83+
84+
/**
85+
* Sets the event listener registered as an event handler IDL attribute for
86+
* the given target and type.
87+
*
88+
* Should be used to set a value for `target.on{type}`.
89+
*/
90+
export function setEventHandlerAttribute(
91+
target: EventTarget,
92+
type: string,
93+
callback: ?EventCallback,
94+
): void {
95+
let map = getEventHandlerAttributeMap(target);
96+
if (map != null) {
97+
const currentListener = map.get(type);
98+
if (currentListener) {
99+
target.removeEventListener(type, currentListener);
100+
map.delete(type);
101+
}
102+
}
103+
104+
if (
105+
callback != null &&
106+
(typeof callback === 'function' || typeof callback === 'object')
107+
) {
108+
// Register the listener as a different object in the target so it
109+
// occupies its own slot and cannot be removed via `removeEventListener`.
110+
const listener = {
111+
handleEvent: callback,
112+
};
113+
114+
try {
115+
target.addEventListener(type, listener);
116+
// If adding the listener fails, we don't store the value
117+
if (map == null) {
118+
map = new Map();
119+
setEventHandlerAttributeMap(target, map);
120+
}
121+
map.set(type, listener);
122+
} catch (e) {
123+
// Assigning incorrect listener does not throw in setters.
124+
}
125+
}
126+
127+
if (map != null && map.size === 0) {
128+
setEventHandlerAttributeMap(target, null);
129+
}
130+
}

packages/react-native/src/private/webapis/dom/events/EventTarget.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@ import {
3333
INTERNAL_DISPATCH_METHOD_KEY,
3434
} from './internals/EventTargetInternals';
3535

36-
export type EventListener =
37-
| ((event: Event) => void)
38-
| interface {
39-
handleEvent(event: Event): void,
40-
};
36+
export type EventCallback = (event: Event) => void;
37+
export type EventHandler = interface {
38+
handleEvent(event: Event): void,
39+
};
40+
export type EventListener = EventCallback | EventHandler;
4141

4242
export type EventListenerOptions = {
4343
capture?: boolean,
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
* @oncall react_native
10+
* @fantom_flags enableAccessToHostTreeInFabric:true
11+
*/
12+
13+
// flowlint unsafe-getters-setters:off
14+
15+
import '../../../../../../Libraries/Core/InitializeCore.js';
16+
17+
import type {EventCallback} from '../EventTarget';
18+
19+
import Event from '../Event';
20+
import {
21+
getEventHandlerAttribute,
22+
setEventHandlerAttribute,
23+
} from '../EventHandlerAttributes';
24+
import EventTarget from '../EventTarget';
25+
26+
class EventTargetSubclass extends EventTarget {
27+
get oncustomevent(): EventCallback | null {
28+
return getEventHandlerAttribute(this, 'customEvent');
29+
}
30+
31+
set oncustomevent(listener: ?EventCallback) {
32+
setEventHandlerAttribute(this, 'customEvent', listener);
33+
}
34+
}
35+
36+
describe('EventHandlerAttributes', () => {
37+
it('should register event listeners assigned to the attributes', () => {
38+
const target = new EventTargetSubclass();
39+
40+
const listener = jest.fn();
41+
target.oncustomevent = listener;
42+
43+
expect(target.oncustomevent).toBe(listener);
44+
45+
const event = new Event('customEvent');
46+
47+
target.dispatchEvent(event);
48+
49+
expect(listener).toHaveBeenCalledTimes(1);
50+
expect(listener.mock.lastCall[0]).toBe(event);
51+
});
52+
53+
it('should NOT register values assigned to the attributes if they are not an event listener', () => {
54+
const target = new EventTargetSubclass();
55+
56+
const listener = Symbol();
57+
// $FlowExpectedError[incompatible-type]
58+
target.oncustomevent = listener;
59+
60+
expect(target.oncustomevent).toBe(null);
61+
62+
const event = new Event('customEvent');
63+
64+
// This doesn't fail.
65+
target.dispatchEvent(event);
66+
});
67+
68+
it('should remove event listeners assigned to the attributes when reassigning them to null', () => {
69+
const target = new EventTargetSubclass();
70+
71+
const listener = jest.fn();
72+
target.oncustomevent = listener;
73+
74+
const event = new Event('customEvent');
75+
76+
target.dispatchEvent(event);
77+
78+
expect(listener).toHaveBeenCalledTimes(1);
79+
expect(listener.mock.lastCall[0]).toBe(event);
80+
81+
target.oncustomevent = null;
82+
83+
expect(target.oncustomevent).toBe(null);
84+
85+
target.dispatchEvent(event);
86+
87+
expect(listener).toHaveBeenCalledTimes(1);
88+
});
89+
90+
it('should remove event listeners assigned to the attributes when reassigning them to a different listener', () => {
91+
const target = new EventTargetSubclass();
92+
93+
const listener = jest.fn();
94+
target.oncustomevent = listener;
95+
96+
const event = new Event('customEvent');
97+
98+
target.dispatchEvent(event);
99+
100+
expect(listener).toHaveBeenCalledTimes(1);
101+
expect(listener.mock.lastCall[0]).toBe(event);
102+
103+
const newListener = jest.fn();
104+
target.oncustomevent = newListener;
105+
106+
expect(target.oncustomevent).toBe(newListener);
107+
108+
target.dispatchEvent(event);
109+
110+
expect(listener).toHaveBeenCalledTimes(1);
111+
expect(newListener).toHaveBeenCalledTimes(1);
112+
expect(newListener.mock.lastCall[0]).toBe(event);
113+
});
114+
115+
it('should remove event listeners assigned to the attributes when reassigning them to an incorrect listener value', () => {
116+
const target = new EventTargetSubclass();
117+
118+
const listener = jest.fn();
119+
target.oncustomevent = listener;
120+
121+
const event = new Event('customEvent');
122+
123+
target.dispatchEvent(event);
124+
125+
expect(listener).toHaveBeenCalledTimes(1);
126+
expect(listener.mock.lastCall[0]).toBe(event);
127+
128+
const newListener = Symbol();
129+
// $FlowExpectedError[incompatible-type]
130+
target.oncustomevent = newListener;
131+
132+
expect(target.oncustomevent).toBe(null);
133+
134+
target.dispatchEvent(event);
135+
136+
expect(listener).toHaveBeenCalledTimes(1);
137+
});
138+
139+
it('should interoperate with listeners registered via `addEventListener`', () => {
140+
const target = new EventTargetSubclass();
141+
142+
let order = 0;
143+
144+
const regularListener1: JestMockFn<[Event], void> = jest.fn(() => {
145+
// $FlowExpectedError[prop-missing]
146+
regularListener1.order = order++;
147+
});
148+
target.addEventListener('customEvent', regularListener1);
149+
150+
const attributeListener: JestMockFn<[Event], void> = jest.fn(() => {
151+
// $FlowExpectedError[prop-missing]
152+
attributeListener.order = order++;
153+
});
154+
target.oncustomevent = attributeListener;
155+
156+
const regularListener2: JestMockFn<[Event], void> = jest.fn(() => {
157+
// $FlowExpectedError[prop-missing]
158+
regularListener2.order = order++;
159+
});
160+
target.addEventListener('customEvent', regularListener2);
161+
162+
const event = new Event('customEvent');
163+
164+
target.dispatchEvent(event);
165+
166+
expect(regularListener1).toHaveBeenCalledTimes(1);
167+
expect(regularListener1.mock.lastCall[0]).toBe(event);
168+
// $FlowExpectedError[prop-missing]
169+
expect(regularListener1.order).toBe(0);
170+
171+
expect(attributeListener).toHaveBeenCalledTimes(1);
172+
expect(attributeListener.mock.lastCall[0]).toBe(event);
173+
// $FlowExpectedError[prop-missing]
174+
expect(attributeListener.order).toBe(1);
175+
176+
expect(regularListener2).toHaveBeenCalledTimes(1);
177+
expect(regularListener2.mock.lastCall[0]).toBe(event);
178+
// $FlowExpectedError[prop-missing]
179+
expect(regularListener2.order).toBe(2);
180+
});
181+
182+
it('should not be considered the same callback when adding it again via `addEventListener`', () => {
183+
const target = new EventTargetSubclass();
184+
185+
const listener = jest.fn();
186+
187+
target.addEventListener('customEvent', listener);
188+
target.oncustomevent = listener;
189+
190+
const event = new Event('customEvent');
191+
192+
target.dispatchEvent(event);
193+
194+
expect(listener).toHaveBeenCalledTimes(2);
195+
expect(listener.mock.calls[0][0]).toBe(event);
196+
expect(listener.mock.calls[1][0]).toBe(event);
197+
198+
target.removeEventListener('customEvent', listener);
199+
200+
target.dispatchEvent(event);
201+
202+
expect(listener).toHaveBeenCalledTimes(3);
203+
expect(listener.mock.lastCall[0]).toBe(event);
204+
});
205+
});

0 commit comments

Comments
 (0)