Skip to content

Commit f215bd0

Browse files
josepharharzhengjitf
authored andcommitted
Add custom element property support behind a flag (facebook#22184)
* custom element props * custom element events * use function type for on* * tests, htmlFor * className * fix ReactDOMComponent-test * started on adding feature flag * added feature flag to all feature flag files * everything passes * tried to fix getPropertyInfo * used @GATE and __experimental__ * remove flag gating for test which already passes * fix onClick test * add __EXPERIMENTAL__ to www flags, rename eventProxy * Add innerText and textContent to reservedProps * Emit warning when assigning to read only properties in client * Revert "Emit warning when assigning to read only properties in client" This reverts commit 1a093e5. * Emit warning when assigning to read only properties during hydration * yarn prettier-all * Gate hydration warning test on flag * Fix gating in hydration warning test * Fix assignment to boolean properties * Replace _listeners with random suffix matching * Improve gating for hydration warning test * Add outerText and outerHTML to server warning properties * remove nameLower logic * fix capture event listener test * Add coverage for changing custom event listeners * yarn prettier-all * yarn lint --fix * replace getCustomElementEventHandlersFromNode with getFiberCurrentPropsFromNode * Remove previous value when adding event listener * flow, lint, prettier * Add dispatchEvent to make sure nothing crashes * Add state change to reserved attribute tests * Add missing feature flag test gate * Reimplement SSR changes in ReactDOMServerFormatConfig * Test hydration for objects and functions * add missing test gate * remove extraneous comment * Add attribute->property test
1 parent 9e93a23 commit f215bd0

17 files changed

+594
-11
lines changed

packages/react-dom/src/__tests__/DOMPropertyOperations-test.js

+349
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,355 @@ describe('DOMPropertyOperations', () => {
155155
// Regression test for https://github.com/facebook/react/issues/6119
156156
expect(container.firstChild.hasAttribute('value')).toBe(false);
157157
});
158+
159+
// @gate enableCustomElementPropertySupport
160+
it('custom element custom events lowercase', () => {
161+
const oncustomevent = jest.fn();
162+
function Test() {
163+
return <my-custom-element oncustomevent={oncustomevent} />;
164+
}
165+
const container = document.createElement('div');
166+
ReactDOM.render(<Test />, container);
167+
container
168+
.querySelector('my-custom-element')
169+
.dispatchEvent(new Event('customevent'));
170+
expect(oncustomevent).toHaveBeenCalledTimes(1);
171+
});
172+
173+
// @gate enableCustomElementPropertySupport
174+
it('custom element custom events uppercase', () => {
175+
const oncustomevent = jest.fn();
176+
function Test() {
177+
return <my-custom-element onCustomevent={oncustomevent} />;
178+
}
179+
const container = document.createElement('div');
180+
ReactDOM.render(<Test />, container);
181+
container
182+
.querySelector('my-custom-element')
183+
.dispatchEvent(new Event('Customevent'));
184+
expect(oncustomevent).toHaveBeenCalledTimes(1);
185+
});
186+
187+
// @gate enableCustomElementPropertySupport
188+
it('custom element custom event with dash in name', () => {
189+
const oncustomevent = jest.fn();
190+
function Test() {
191+
return <my-custom-element oncustom-event={oncustomevent} />;
192+
}
193+
const container = document.createElement('div');
194+
ReactDOM.render(<Test />, container);
195+
container
196+
.querySelector('my-custom-element')
197+
.dispatchEvent(new Event('custom-event'));
198+
expect(oncustomevent).toHaveBeenCalledTimes(1);
199+
});
200+
201+
// @gate enableCustomElementPropertySupport
202+
it('custom element remove event handler', () => {
203+
const oncustomevent = jest.fn();
204+
function Test(props) {
205+
return <my-custom-element oncustomevent={props.handler} />;
206+
}
207+
208+
const container = document.createElement('div');
209+
ReactDOM.render(<Test handler={oncustomevent} />, container);
210+
const customElement = container.querySelector('my-custom-element');
211+
customElement.dispatchEvent(new Event('customevent'));
212+
expect(oncustomevent).toHaveBeenCalledTimes(1);
213+
214+
ReactDOM.render(<Test handler={false} />, container);
215+
// Make sure that the second render didn't create a new element. We want
216+
// to make sure removeEventListener actually gets called on the same element.
217+
expect(customElement).toBe(customElement);
218+
customElement.dispatchEvent(new Event('customevent'));
219+
expect(oncustomevent).toHaveBeenCalledTimes(1);
220+
221+
ReactDOM.render(<Test handler={oncustomevent} />, container);
222+
customElement.dispatchEvent(new Event('customevent'));
223+
expect(oncustomevent).toHaveBeenCalledTimes(2);
224+
225+
const oncustomevent2 = jest.fn();
226+
ReactDOM.render(<Test handler={oncustomevent2} />, container);
227+
customElement.dispatchEvent(new Event('customevent'));
228+
expect(oncustomevent).toHaveBeenCalledTimes(2);
229+
expect(oncustomevent2).toHaveBeenCalledTimes(1);
230+
});
231+
232+
it('custom elements shouldnt have non-functions for on* attributes treated as event listeners', () => {
233+
const container = document.createElement('div');
234+
ReactDOM.render(
235+
<my-custom-element
236+
onstring={'hello'}
237+
onobj={{hello: 'world'}}
238+
onarray={['one', 'two']}
239+
ontrue={true}
240+
onfalse={false}
241+
/>,
242+
container,
243+
);
244+
const customElement = container.querySelector('my-custom-element');
245+
expect(customElement.getAttribute('onstring')).toBe('hello');
246+
expect(customElement.getAttribute('onobj')).toBe('[object Object]');
247+
expect(customElement.getAttribute('onarray')).toBe('one,two');
248+
expect(customElement.getAttribute('ontrue')).toBe('true');
249+
expect(customElement.getAttribute('onfalse')).toBe('false');
250+
251+
// Dispatch the corresponding event names to make sure that nothing crashes.
252+
customElement.dispatchEvent(new Event('string'));
253+
customElement.dispatchEvent(new Event('obj'));
254+
customElement.dispatchEvent(new Event('array'));
255+
customElement.dispatchEvent(new Event('true'));
256+
customElement.dispatchEvent(new Event('false'));
257+
});
258+
259+
it('custom elements should still have onClick treated like regular elements', () => {
260+
let syntheticClickEvent = null;
261+
const syntheticEventHandler = jest.fn(
262+
event => (syntheticClickEvent = event),
263+
);
264+
let nativeClickEvent = null;
265+
const nativeEventHandler = jest.fn(event => (nativeClickEvent = event));
266+
function Test() {
267+
return <my-custom-element onClick={syntheticEventHandler} />;
268+
}
269+
270+
const container = document.createElement('div');
271+
document.body.appendChild(container);
272+
ReactDOM.render(<Test />, container);
273+
274+
const customElement = container.querySelector('my-custom-element');
275+
customElement.onclick = nativeEventHandler;
276+
container.querySelector('my-custom-element').click();
277+
278+
expect(nativeEventHandler).toHaveBeenCalledTimes(1);
279+
expect(syntheticEventHandler).toHaveBeenCalledTimes(1);
280+
expect(syntheticClickEvent.nativeEvent).toBe(nativeClickEvent);
281+
});
282+
283+
// @gate enableCustomElementPropertySupport
284+
it('custom elements should allow custom events with capture event listeners', () => {
285+
const oncustomeventCapture = jest.fn();
286+
const oncustomevent = jest.fn();
287+
function Test() {
288+
return (
289+
<my-custom-element
290+
oncustomeventCapture={oncustomeventCapture}
291+
oncustomevent={oncustomevent}>
292+
<div />
293+
</my-custom-element>
294+
);
295+
}
296+
const container = document.createElement('div');
297+
ReactDOM.render(<Test />, container);
298+
container
299+
.querySelector('my-custom-element > div')
300+
.dispatchEvent(new Event('customevent', {bubbles: false}));
301+
expect(oncustomeventCapture).toHaveBeenCalledTimes(1);
302+
expect(oncustomevent).toHaveBeenCalledTimes(0);
303+
});
304+
305+
it('innerHTML should not work on custom elements', () => {
306+
const container = document.createElement('div');
307+
ReactDOM.render(<my-custom-element innerHTML="foo" />, container);
308+
const customElement = container.querySelector('my-custom-element');
309+
expect(customElement.getAttribute('innerHTML')).toBe(null);
310+
expect(customElement.hasChildNodes()).toBe(false);
311+
312+
// Render again to verify the update codepath doesn't accidentally let
313+
// something through.
314+
ReactDOM.render(<my-custom-element innerHTML="bar" />, container);
315+
expect(customElement.getAttribute('innerHTML')).toBe(null);
316+
expect(customElement.hasChildNodes()).toBe(false);
317+
});
318+
319+
// @gate enableCustomElementPropertySupport
320+
it('innerText should not work on custom elements', () => {
321+
const container = document.createElement('div');
322+
ReactDOM.render(<my-custom-element innerText="foo" />, container);
323+
const customElement = container.querySelector('my-custom-element');
324+
expect(customElement.getAttribute('innerText')).toBe(null);
325+
expect(customElement.hasChildNodes()).toBe(false);
326+
327+
// Render again to verify the update codepath doesn't accidentally let
328+
// something through.
329+
ReactDOM.render(<my-custom-element innerText="bar" />, container);
330+
expect(customElement.getAttribute('innerText')).toBe(null);
331+
expect(customElement.hasChildNodes()).toBe(false);
332+
});
333+
334+
// @gate enableCustomElementPropertySupport
335+
it('textContent should not work on custom elements', () => {
336+
const container = document.createElement('div');
337+
ReactDOM.render(<my-custom-element textContent="foo" />, container);
338+
const customElement = container.querySelector('my-custom-element');
339+
expect(customElement.getAttribute('textContent')).toBe(null);
340+
expect(customElement.hasChildNodes()).toBe(false);
341+
342+
// Render again to verify the update codepath doesn't accidentally let
343+
// something through.
344+
ReactDOM.render(<my-custom-element textContent="bar" />, container);
345+
expect(customElement.getAttribute('textContent')).toBe(null);
346+
expect(customElement.hasChildNodes()).toBe(false);
347+
});
348+
349+
// @gate enableCustomElementPropertySupport
350+
it('values should not be converted to booleans when assigning into custom elements', () => {
351+
const container = document.createElement('div');
352+
document.body.appendChild(container);
353+
ReactDOM.render(<my-custom-element />, container);
354+
const customElement = container.querySelector('my-custom-element');
355+
customElement.foo = null;
356+
357+
// true => string
358+
ReactDOM.render(<my-custom-element foo={true} />, container);
359+
expect(customElement.foo).toBe(true);
360+
ReactDOM.render(<my-custom-element foo="bar" />, container);
361+
expect(customElement.foo).toBe('bar');
362+
363+
// false => string
364+
ReactDOM.render(<my-custom-element foo={false} />, container);
365+
expect(customElement.foo).toBe(false);
366+
ReactDOM.render(<my-custom-element foo="bar" />, container);
367+
expect(customElement.foo).toBe('bar');
368+
369+
// true => null
370+
ReactDOM.render(<my-custom-element foo={true} />, container);
371+
expect(customElement.foo).toBe(true);
372+
ReactDOM.render(<my-custom-element foo={null} />, container);
373+
expect(customElement.foo).toBe(null);
374+
375+
// false => null
376+
ReactDOM.render(<my-custom-element foo={false} />, container);
377+
expect(customElement.foo).toBe(false);
378+
ReactDOM.render(<my-custom-element foo={null} />, container);
379+
expect(customElement.foo).toBe(null);
380+
});
381+
382+
// @gate enableCustomElementPropertySupport
383+
it('custom element custom event handlers assign multiple types', () => {
384+
const container = document.createElement('div');
385+
document.body.appendChild(container);
386+
const oncustomevent = jest.fn();
387+
388+
// First render with string
389+
ReactDOM.render(<my-custom-element oncustomevent={'foo'} />, container);
390+
const customelement = container.querySelector('my-custom-element');
391+
customelement.dispatchEvent(new Event('customevent'));
392+
expect(oncustomevent).toHaveBeenCalledTimes(0);
393+
expect(customelement.oncustomevent).toBe(undefined);
394+
expect(customelement.getAttribute('oncustomevent')).toBe('foo');
395+
396+
// string => event listener
397+
ReactDOM.render(
398+
<my-custom-element oncustomevent={oncustomevent} />,
399+
container,
400+
);
401+
customelement.dispatchEvent(new Event('customevent'));
402+
expect(oncustomevent).toHaveBeenCalledTimes(1);
403+
expect(customelement.oncustomevent).toBe(undefined);
404+
expect(customelement.getAttribute('oncustomevent')).toBe(null);
405+
406+
// event listener => string
407+
ReactDOM.render(<my-custom-element oncustomevent={'foo'} />, container);
408+
customelement.dispatchEvent(new Event('customevent'));
409+
expect(oncustomevent).toHaveBeenCalledTimes(1);
410+
expect(customelement.oncustomevent).toBe(undefined);
411+
expect(customelement.getAttribute('oncustomevent')).toBe('foo');
412+
413+
// string => nothing
414+
ReactDOM.render(<my-custom-element />, container);
415+
customelement.dispatchEvent(new Event('customevent'));
416+
expect(oncustomevent).toHaveBeenCalledTimes(1);
417+
expect(customelement.oncustomevent).toBe(undefined);
418+
expect(customelement.getAttribute('oncustomevent')).toBe(null);
419+
420+
// nothing => event listener
421+
ReactDOM.render(
422+
<my-custom-element oncustomevent={oncustomevent} />,
423+
container,
424+
);
425+
customelement.dispatchEvent(new Event('customevent'));
426+
expect(oncustomevent).toHaveBeenCalledTimes(2);
427+
expect(customelement.oncustomevent).toBe(undefined);
428+
expect(customelement.getAttribute('oncustomevent')).toBe(null);
429+
});
430+
431+
// @gate enableCustomElementPropertySupport
432+
it('custom element custom event handlers assign multiple types with setter', () => {
433+
const container = document.createElement('div');
434+
document.body.appendChild(container);
435+
const oncustomevent = jest.fn();
436+
437+
// First render with nothing
438+
ReactDOM.render(<my-custom-element />, container);
439+
const customelement = container.querySelector('my-custom-element');
440+
// Install a setter to activate the `in` heuristic
441+
Object.defineProperty(customelement, 'oncustomevent', {
442+
set: function(x) {
443+
this._oncustomevent = x;
444+
},
445+
get: function() {
446+
return this._oncustomevent;
447+
},
448+
});
449+
expect(customelement.oncustomevent).toBe(undefined);
450+
451+
// nothing => event listener
452+
ReactDOM.render(
453+
<my-custom-element oncustomevent={oncustomevent} />,
454+
container,
455+
);
456+
customelement.dispatchEvent(new Event('customevent'));
457+
expect(oncustomevent).toHaveBeenCalledTimes(1);
458+
expect(customelement.oncustomevent).toBe(null);
459+
expect(customelement.getAttribute('oncustomevent')).toBe(null);
460+
461+
// event listener => string
462+
ReactDOM.render(<my-custom-element oncustomevent={'foo'} />, container);
463+
customelement.dispatchEvent(new Event('customevent'));
464+
expect(oncustomevent).toHaveBeenCalledTimes(1);
465+
expect(customelement.oncustomevent).toBe('foo');
466+
expect(customelement.getAttribute('oncustomevent')).toBe(null);
467+
468+
// string => event listener
469+
ReactDOM.render(
470+
<my-custom-element oncustomevent={oncustomevent} />,
471+
container,
472+
);
473+
customelement.dispatchEvent(new Event('customevent'));
474+
expect(oncustomevent).toHaveBeenCalledTimes(2);
475+
expect(customelement.oncustomevent).toBe(null);
476+
expect(customelement.getAttribute('oncustomevent')).toBe(null);
477+
478+
// event listener => nothing
479+
ReactDOM.render(<my-custom-element />, container);
480+
customelement.dispatchEvent(new Event('customevent'));
481+
expect(oncustomevent).toHaveBeenCalledTimes(2);
482+
expect(customelement.oncustomevent).toBe(null);
483+
expect(customelement.getAttribute('oncustomevent')).toBe(null);
484+
});
485+
486+
// @gate enableCustomElementPropertySupport
487+
it('assigning to a custom element property should not remove attributes', () => {
488+
const container = document.createElement('div');
489+
document.body.appendChild(container);
490+
ReactDOM.render(<my-custom-element foo="one" />, container);
491+
const customElement = container.querySelector('my-custom-element');
492+
expect(customElement.getAttribute('foo')).toBe('one');
493+
494+
// Install a setter to activate the `in` heuristic
495+
Object.defineProperty(customElement, 'foo', {
496+
set: function(x) {
497+
this._foo = x;
498+
},
499+
get: function() {
500+
return this._foo;
501+
},
502+
});
503+
ReactDOM.render(<my-custom-element foo="two" />, container);
504+
expect(customElement.foo).toBe('two');
505+
expect(customElement.getAttribute('foo')).toBe('one');
506+
});
158507
});
159508

160509
describe('deleteValueForProperty', () => {

0 commit comments

Comments
 (0)