-
Notifications
You must be signed in to change notification settings - Fork 46.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix change events for custom elements #22938
Conversation
Comparing: c09596c...93c9db1 Critical size changesIncludes critical production bundles, as well as any change greater than 2%:
Significant size changesIncludes any change greater than 0.2%: (No significant changes) |
This would break the event order relative to other click events etc. Break stop propagation etc. Since the react ones are delegated. That's partly why we wanted to keep special casing React events. I suspect that the fix might be specific to onChange and that the rest would work well because you really shouldn't repurpose the built in names with custom events. Since they can bubble through. We want to remove the synthetic event system all together and maybe it would make sense to put that in the same release. It's a much bigger change tho. |
Ok, I agree with your points, thanks! I'll try changing react's event system for custom elements then. Here's a more detailed example: https://codesandbox.io/s/sleepy-field-z7g83?file=/src/App.js
If you have any advice for where to look to fix these things, it would be greatly appreciated! I couldn't even figure out why |
Do you want to look at ChangeEventPlugin? Only events “registered” by SimpleEventPlugin work “as is”. The rest (like onChange) have special behavior described in the plugins. |
Ok, instead of bypassing the react event system, I made a small change to SimpleEventPlugin to make
I tried messing with ChangeEventPlugin but I couldn't get it to work quite right, and it was more complex, and there is a long comment here suggesting that all the logic should be in SimpleEventPlugin anyway... |
Is change event the only special event? I would expect to have special events for each plugin. Why does this change (pardon the pun) only affect change? |
After coming back to this after a while, I found that I could just modify ChangeEventPlugin to fix this despite my previous comment saying that I couldn't get it to work 😅 I pushed a commit to do this.
There is code in ChangeEventPlugin to only call onChange handlers for select and input elements: react/packages/react-dom/src/events/plugins/ChangeEventPlugin.js Lines 75 to 81 in c09596c
As far as I know, the other plugins don't have this constraint. |
if ( | ||
enableCustomElementPropertySupport && | ||
targetInst && | ||
isCustomComponent(targetInst.elementType, targetInst.pendingProps) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should use memoizedProps instead which represent the ones that were committed. pendingProps represent last work in progress which could've been interrupted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I'm able to comment out this whole line and the tests still pass. I think it's because there is no test for the is="...."
case which isCustomComponent
is trying to catch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. I understand we'd want <div is="my-custom-element">
to work with change event.
But if you do <input is="my-custom-element">
, which branch of logic should we use? Should it use the "raw" behavior or the polyfill which we normally use for inputs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sebmarkbage do you have an opinion on this? seems like it's a bit simpler to turn off polyfilling when you do <input is="my-thing">
but that would mean that adding is
changes semantics of your onChange
handler. maybe that's ok because you decided to "go manual"? especially in the light of onChange->onInput future change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I'm able to comment out this whole line and the tests still pass. I think it's because there is no test for the is="...." case which isCustomComponent is trying to catch.
On my machine, if I comment out just the && isCustomComponent(...)
, 7 tests fail from various test files. If I undo the entire change here, 2 of the tests I added fail.
Hmm. I understand we'd want
<div is="my-custom-element">
to work with change event.
Yes, <div is='my-custom-element'>
should get this behavior as well.
I could copy all of the tests I added and use is
instead of <my-custom-element />
to show that I used isCustomComponent
, but you could say the same thing about all of the many custom element tests I added...
But if you do , which branch of logic should we use? Should it use the "raw" behavior or the polyfill which we normally use for inputs?
Based on what I read in DOMPluginEventSystem, ChangeEventPlugin is a "polyfill", right? The code I added is in ChangeEventPlugin, which means that <input>
and custom elements should now be treated the same with regards to this behavior, right...?
I think this should use memoizedProps instead which represent the ones that were committed. pendingProps represent last work in progress which could've been interrupted.
Thanks, I was wondering which I should use! I have changed it to memoizedProps.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on what I read in DOMPluginEventSystem, ChangeEventPlugin is a "polyfill", right? The code I added is in ChangeEventPlugin, which means that and custom elements should now be treated the same with regards to this behavior, right...?
I meant the code of the polyfill below (look how this function is used). In the polyfill itself, we decide between using one of the events under the hood: change
, input
, or click
. We decide that based on the type of the element. The code you added makes us always use change
event for custom elements. That's good for <custom-element>
or <div is="custom-element">
. What I'm not sure about is <input is="custom-element">
.
Should <input is="custom-element">
map onChange
to the "raw" change
event (which means it would work differently from <input>
which in React maps onChange
to input
)? Or should it map onChange
to the input
/click
events to mirror how <input>
works?
In the latter case, it would mean that some custom elements still get special polyfill behavior that isn't 1:1 to the event names. That seems like it would be confusing. But if we go with former case (raw events), adding is
to an existing input and having its onChange
completely break (no longer called on every keystroke) also seems very confusing? Which is the behavior we'd get if it kept it raw like in this PR.
Using raw events for <input is="custom-element">
feels like simpler behavior to me but I'm not sure. There's also a potential intent to change it for <input>
in distant future altogether: #9657.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@josepharhar Taking a step back, we should look at how controlled inputs generally work with <input is="my-custom-element">
. Like how are you supposed to write a controlled input? One of the features of controlled inputs is that <input value="">
with no onChange
handler won't allow typing. The same should be true for <input value="" is="my-custom-element">
. Is that the case? If it already works, is it necessary to add onChange
(like normal React) or onInput
(closer to browser behavior) to make it respond to typing? That's what we need to decide on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just talked a bit with some LitElement folks about this, and they pointed out that Safari doesn't support the is
attribute at all and that they advise against using it: https://bugs.webkit.org/show_bug.cgi?id=182671
I think that it would make the most sense to continue doing all of react's current behavior for form controls when they have the is
attribute. Besides, the element and its shadowdom is still the same in this case when using is
.
I pushed a commit to do this with tests.
We should have some tests for these cases too: <div onChange={…} onInput={…}>
<my-custom-element />
<input />
<input is=“my-custom-element” />
</div> I.e. what is the bubbling expectations. Also what about Where the target is the child div. |
customInput.dispatchEvent(new Event('click', {bubbles: true})); | ||
|
||
expect(customOnInputHandler).toHaveBeenCalledTimes( | ||
regularOnInputHandler.mock.calls.length, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is kind of fragile. (What if we change some heuristic and all of these are 0.) Can you make the test a bit more specific? Also ideally you'd verify that the handler runs as early as possible, and not at the end of the function. So that if there's a timing difference, it is visible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added expectations after each dispatchEvent and put specific numbers in
Thanks for pointing this out!
I added a test for this, and unfortunately it looks like |
|
||
regularInput.dispatchEvent(new Event('input', {bubbles: true})); | ||
expect(regularOnInputHandler).toHaveBeenCalledTimes(1); | ||
expect(regularOnChangeHandler).toHaveBeenCalledTimes(0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like onChange never fires in any of these? Then it seems like this isn't testing the thing we care about. Since we care that input causes onChange.
Most likely what's missing is the actual change in the value. (And I think we also have to force React to "see" it with a hack). See how existing input tests do it:
react/packages/react-dom/src/__tests__/ReactDOMInput-test.js
Lines 226 to 229 in 69eb020
setUntrackedValue.call(instance.a, 'giraffe'); | |
// This must use the native event dispatching. If we simulate, we will | |
// bypass the lazy event attachment system so we won't actually test this. | |
dispatchEventOnNode(instance.a, 'input'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've pushed a few commits that address this and make the tests have a more realistic sequence of events.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
need to fix tests
Thanks for finishing this up! |
* Bypass react event system for custom elements * Going to try fixing react event system instead * finally got it to call onChange, but perhaps too many times * update test * Removed ReactDOMComponent changes, now works but still doubles for bubbles * Maybe i should only support bubbling events * removed some old stuff * cleaned up changeeventplugin stuff * prettier, lint * removed changeeventplugin stuff * remove unneeded gate for onInput test * Go back to using ChangeEventPlugin * Add input+change test * lint * Move logic to shouldUseChangeEvent * use memoizedProps instead of pendingProps * Run form control behavior before custom element behavior * add bubbling test * forgot to append container to body * add child event target test * expand <input is=...> test expectations * Make tests more realistic * Add extra test * Add missing gating * Actually fix gating Co-authored-by: Dan Abramov <dan.abramov@me.com>
* Bypass react event system for custom elements * Going to try fixing react event system instead * finally got it to call onChange, but perhaps too many times * update test * Removed ReactDOMComponent changes, now works but still doubles for bubbles * Maybe i should only support bubbling events * removed some old stuff * cleaned up changeeventplugin stuff * prettier, lint * removed changeeventplugin stuff * remove unneeded gate for onInput test * Go back to using ChangeEventPlugin * Add input+change test * lint * Move logic to shouldUseChangeEvent * use memoizedProps instead of pendingProps * Run form control behavior before custom element behavior * add bubbling test * forgot to append container to body * add child event target test * expand <input is=...> test expectations * Make tests more realistic * Add extra test * Add missing gating * Actually fix gating Co-authored-by: Dan Abramov <dan.abramov@me.com>
Summary
This patch makes
onChange
handlers listen tochange
events for custom elements. There was some odd behavior with theonChange
handler andchange
events, but this patch will make it work for custom elements.This fixes #22888 and I manually tested that it fixes Dan's example.
How did you test this change?
I tested it with the broken example and added tests in
DOMPropertyOperations-test.js
.