Skip to content
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

Bug: Submit events from a button inside a portal don't bubble up to a form in its React DOM Ancestry. #22470

Open
jviall opened this issue Sep 30, 2021 · 13 comments
Labels
Component: DOM Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug Type: Discussion

Comments

@jviall
Copy link

jviall commented Sep 30, 2021

From the docs of React Portal

An event fired from inside a portal will propagate to ancestors in the containing React tree, even if those elements are not ancestors in the DOM tree.

However this doesn't seem to be the case with <form>s when using a <button type='submit'> that is inside a Portal.
Clicking a submit button that is inside a portal-ed modal which is contained by the form does not trigger the form's onSubmit event. The only way to get it to do so, is to wrap the submit button in an additional <form> element that is inside the portal as well. When this is done, then event bubbles up to both forms, and the original, outer form's onSubmit is triggered as desired.

React version: 16.14.0

Steps To Reproduce

  1. Write a simple form element that contains a React Portal, such as a modal.
  2. Inside thePortal, add a submit button, e.g. <button type='submit'>submit</button>.
  3. Click the submit button inside the portal.

Workaround

  1. Inside the Portal, wrap the submit button you've made in an additional form element.
  2. Click the submit button inside the portal.

Link to code example: https://codesandbox.io/s/new-dust-b6hoh?file=/src/App.tsx

The current behavior

Clicking the button inside the portal does not result in the form being submitted.
Clicking the button inside the portal with an additional form element results in both forms being submitted.

The expected behavior

The form outside of the portal is submitted by the portal's submit button (without including any additional form elements).

@jviall jviall added the Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug label Sep 30, 2021
@jviall jviall changed the title Bug: Submit events from a button inside a portal don't bubble up to a form in it's React DOM Ancestary. Bug: Submit events from a button inside a portal don't bubble up to a form in its React DOM Ancestry. Sep 30, 2021
@jviall
Copy link
Author

jviall commented Sep 30, 2021

I've found this bug from 2018 was opened, seemingly with the opposite problem around the same situation, but it was never investigated further as the behavior they experienced is documented as expected: #14456.

@akgupta0777
Copy link
Contributor

@jviall I think the Dialog Component in material-ui gets destroyed immediately after submitting and hence won't trigger the event-bubbling in form.

@eps1lon
Copy link
Collaborator

eps1lon commented Sep 30, 2021

The Dialog content is portaled to another part of the DOM and therefore not part of the <form /> element. This is intended platform behavior.

Either you want to disable the portal or trigger form submission by other means.

Support requests filed as GitHub issues often go unanswered. We want you to find the answer you're looking for, so we suggest the following alternatives:

Coding Questions
If you have a coding question related to React and React DOM, it might be better suited for Stack Overflow. It's a great place to browse through frequent questions about using React, as well as ask for help with specific questions.

https://stackoverflow.com/questions/tagged/react

Talk to other React developers
There are many online forums which are a great place for discussion about best practices and application architecture as well as the future of React.

https://reactjs.org/community/support.html

@jviall
Copy link
Author

jviall commented Sep 30, 2021

@eps1lon

The Dialog content is portaled to another part of the DOM and therefore not part of the <form /> element. This is intended platform behavior.

This directly contradicts what the documentation of Portals describes:

An event fired from inside a portal will propagate to ancestors in the containing React tree, even if those elements are not ancestors in the DOM tree.

Also, if it is intended that the event shouldn't bubble up to the form outside of the portal, then it shouldn't do so when the second form element is added within the portal. This is contradictory behavior.

@jviall
Copy link
Author

jviall commented Sep 30, 2021

@jviall I think the Dialog Component in material-ui gets destroyed immediately after submitting and hence won't trigger the event-bubbling in form.

I can validate that this isn't the issue because including the second form element like so

<form>
  <Portal>
    <form>
      <button type="submit"/>
    </form>
  </Portal>
</form>

makes bubbling occur as I expect, and the outer form is submitted.

@markerikson
Copy link
Contributor

@eps1lon fwiw, I specifically suggested asking over here when the question came up in Reactiflux, because A) it's a pretty tricky aspect of behavior to begin with, and B) there seems to be some difference between what the docs say should happen and how it's actually behaving.

@akgupta0777
Copy link
Contributor

@jviall I think the Dialog Component in material-ui gets destroyed immediately after submitting and hence won't trigger the event-bubbling in form.

I can validate that this isn't the issue because including the second form element like so

<form>
  <Portal>
    <form>
      <button type="submit"/>
    </form>
  </Portal>
</form>

makes bubbling occur as I expect, and the outer form is submitted.

Thanks @jviall for responding. I will look into and investigate this issue.

@eps1lon
Copy link
Collaborator

eps1lon commented Oct 1, 2021

This directly contradicts what the documentation of Portals describes:

We're talking about different things. I specificially said "not associated" i.e. the <button /> is not the submitter of the form. You are talking about event bubbling which does work through Portals. There's just no submit event to be fired. Clicking a submit button does not fire a submit event but rather submits it's associated form. This is an important distinction.

I can validate that this isn't the issue because including the second form element like so

I think nesting forms (even through a Portal) is another issue (see #20741) since it's technically invalid (if you want the submit to bubble) by HTML spec.

makes bubbling occur as I expect, and the outer form is submitted.

To be clear: This may be expected behavior by you but it's unintended behavior if we respect platform behavior. Nesting forms is not allowed in HTML ("Flow content, but with no form element descendants." -- https://html.spec.whatwg.org/multipage/forms.html#the-form-element). In your example only event bubbling should work since React bubbles event through portals. But the forms itself are completely separate and placement of different fields probably does not work as you think it does.

So this issue seems like an unfortunate mix of #20741 and expecting a portaled submitter to be associated with a form that's only the parent in the React tree.

I would suggest reducing the issue to just React usage, comparing behavior between 16 and 17 and writing down how exactly you think nesting forms across Portal boundaries should behave (e.g. which submitters are associated with which form, which fields are associated with which forms etc).

Note that you can also explicitly associate elements to a form with the form attribute to ignore DOM order.

@eps1lon
Copy link
Collaborator

eps1lon commented May 2, 2022

So the question is if portaling a form-related element outside of a <form /> is intended to not associate that element to the form or not.

I don't think it's clear that you do want it associated.

Let's say we start associating portaled form-related elements with their react-parent <form />. What should we tell people that specifically portaled a form-related element to avoid associating it with their react-parent <form />?

What if we start warning? How would people disable this warning if they do intend to de-associate a form-related element with their react-parent <form />?

Today you already have an API to "re-associate" a portaled form-related element to its react-parrent <form /> with the form attribute:

<div id="portal-container" />
<form id="my-form">
  {React.createPortal(<input type="submit" form="my-form" />, document.getElementByID("my-form"))}
</form>

What should the API look like to explicitly "de-associate" a form-related element with its react-parent <form />?

I'm re-opening to get some feedback how React should behave in the scenarios mentioned above.

@fluidsonic
Copy link

fluidsonic commented Jun 30, 2023

This also surprised us in our project.

For portaled components, events bubble up the logical hierarchy (React), not the rendered hierarchy (DOM). We expected type=submit to behave the same way (although it's not an event) because it's a logical connection between the button and the containing form.

We'd expect this behavior to be consistent.
Regarding opt-out - can developers opt out of React's event handling? If yes the same approach could be used here. If not, then it's consistent anyway to not offer an opt-out here. Although you could simply not use type=submit or specify another form by ID.

Our general understanding of portals is "logically this belongs here but in the DOM it should be placed elsewhere". Any related behavior should also be portaled.
But maybe our understanding is wrong here.

@jviall
Copy link
Author

jviall commented Jun 30, 2023

Today you already have an API to "re-associate" a portaled form-related element to its react-parrent

with the form attribute:

For what it's worth as the original reporter of this issue, I ended up leaning on the form attribute with success. @eps1lon though my original impression was that this was strange and potentially incorrect behavior, I understand the hesitation to change the behavior when native workarounds exist and the idea of what the workaround would be in the other paradigm is harder to grasp at.

Would it be a reasonable compromise to note this behavior somewhere in the documentation of Portals?

@fluidsonic
Copy link

Clear documentation would definitely be helpful.

There's more confusing and inconsistent behavior here which is along the same lines.
We have a <form> which also has a nested dialog that uses a portal. In that dialog is another <form> and a text <input> to search and use a value form a list of items. When I press the Enter key within that dialog input, not the dialog form is submitted but the other form instead. Probably also related to #20741.

@Jlomaka
Copy link

Jlomaka commented Nov 17, 2023

My solution

import { useState } from "react";
import { createPortal } from "react-dom";

export default function App() {
  const [isShowPopup, setIsShowPopup] = useState<boolean>(false);

  const onSubmit = (value?: string) => {
    console.log(value);
  };

  return (
    <div className="App">
      <button onClick={() => setIsShowPopup(true)}>Show Popup</button>
      <form
        onSubmit={(event) => {
          event.preventDefault();
          event.stopPropagation();
          onSubmit("value1");
        }}
      >
        {isShowPopup &&
          createPortal(
            <form
              onSubmit={(event) => {
                event.preventDefault();
                event.stopPropagation();
                onSubmit("value3");
              }}
            >
              <button type="submit">Submit</button>
            </form>,
            document.body
          )}
      </form>
    </div>
  );
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Component: DOM Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug Type: Discussion
Projects
None yet
Development

No branches or pull requests

6 participants