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

feat(remix-react): emulate put/patch/delete when JS is unavailable #4496

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from

Conversation

jenseng
Copy link
Contributor

@jenseng jenseng commented Nov 2, 2022

References #4420

  • Docs
  • Tests

Ensure <Form method=...> behaves the same with and without JavaScript. Although native forms don't support PUT/PATCH/DELETE, we can emulate them by doing a POST and injecting a _method parameter into the action URL. The Remix server runtime then provides action handlers a Request with the overridden method so they are none the wiser.

This provides a more reliable way to use these methods, which can be more ergonomic to write and handle than hidden inputs or buttons with values.

Note:
The emulation only works for action requests handled by Remix; if the form is submitted to another endpoint, it would need to handle the _method URL parameter accordingly.

@changeset-bot
Copy link

changeset-bot bot commented Nov 2, 2022

🦋 Changeset detected

Latest commit: 18d6351

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
Name Type
@remix-run/react Minor
@remix-run/server-runtime Minor
@remix-run/testing Minor
@remix-run/cloudflare Minor
@remix-run/deno Minor
@remix-run/dev Minor
@remix-run/node Minor
@remix-run/cloudflare-pages Minor
@remix-run/cloudflare-workers Minor
create-remix Minor
@remix-run/architect Minor
@remix-run/express Minor
@remix-run/netlify Minor
@remix-run/vercel Minor
@remix-run/serve Minor
remix Minor
@remix-run/eslint-config Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@jenseng
Copy link
Contributor Author

jenseng commented Nov 2, 2022

I titled this as a feat and bumped the minor versions accordingly, but I could see it being classified differently, lmk what you think...

  1. It could be argued this is just a bug fix, since we're making the non-JS behavior consistent with the JS behavior
  2. It could be argued this is a breaking change, since maaaaaaybe someone out there is intentionally handling the JS-disabled-<Form> case differently on the server side (e.g. if their action gets a POST instead of their desired method, they might render a message that says "you need to enable JavaScript", which would stop "working" with this change 🙃 )

Copy link
Member

@sergiodxa sergiodxa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love this, it makes other HTTP methods more reliable to use if you want to support a no-JS scenario

Copy link
Collaborator

@machour machour left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome!

(Although generally speaking, maybe we should start name-spacing remix special parameters like _remix_method, _remix_data ..)

@jenseng jenseng force-pushed the emulate-additional-form-methods branch from 939dc6a to d37807f Compare November 2, 2022 23:25
@jenseng jenseng force-pushed the emulate-additional-form-methods branch from d37807f to 23d258a Compare November 3, 2022 05:02
@brophdawg11 brophdawg11 self-assigned this Nov 4, 2022
@znycheporuk
Copy link

znycheporuk commented Nov 18, 2022

Is it possible to make it work with <button formmethod="put|patch|delete" />?

@jenseng jenseng force-pushed the emulate-additional-form-methods branch from 23d258a to f1aea2e Compare November 18, 2022 15:48
@jenseng
Copy link
Contributor Author

jenseng commented Nov 18, 2022

@znycheporuk there are a couple big hurdles:

  1. Browsers don't support setting the other form methods on a button. When you do <button ..., React is essentially running document.createElement("button") and then setting attributes on it. If you set a formmethod value other than get or post, the browser silently ignores it, which means we have no way of getting it later.
  2. Even if we could get the other form methods to stick on the button element somehow, when the form is submitted without/before JavaScript, it would not get used because browsers don't support that at submission time.

It may be possible to emulate it a different way, but that would require more plumbing and likely not be worth the trouble. One idea might be for remix to provide a Button component along these lines:

function Button({ formmethod, name, value, ...props }) {
  if (["put", "patch", "delete"].includes(formmethod)) {
    name = "_method"
    value = formmethod
  }
  return <button name={name} value={value} {...props} />
}

A downside is that you couldn't use an explicit name+value in conjunction with such a formmethod, since they would be overridden. Additionally, the server-side _method logic would need to be adjusted to also look in formData (multipart or url-encoded), which would make the code more complex and may have performance implications.

I suspect it's probably not worth the trouble, and there may be a much simpler alternative if you want to associate a custom form method with a specific button: Use multiple forms :) ... for example, instead of this:

<Form>
  <input name="todoItem" defaultValue={myTodo} />
  <button formmethod="put">Update</button>
  <button formmethod="delete">Delete</button>
</Form>

You could structure your elements like so:

<>
  <Form method="put">
    <input name="todoItem" defaultValue={myTodo} />
    <button>Update</button>
  </Form>
  <Form method="delete">
    <button>Delete</button>
  </Form>
</>

Or if for some reason you really want the buttons in the same form (e.g. to facilitate styling), you can associate it with another form:

<>
  <Form method="put">
    <input name="todoItem" defaultValue={myTodo} />
    <button>Update</button>
    <button form="delete-form">Delete</button>
  </Form>
  <Form method="delete" style={{ display: "none" }} id="delete-form"></Form>
</>

If neither of those approaches work or your use case (e.g. you need all buttons to submit all the form data), then I'd suggest instead using buttons like <button name="intent" value="update">, and keying off of the intent value on the server side.

@jenseng jenseng force-pushed the emulate-additional-form-methods branch 2 times, most recently from c97f578 to 75f4cd5 Compare December 12, 2022 17:00
References remix-run#4420

Ensure `<Form method=...>` behaves the same with and without JavaScript.
Although native forms don't support PUT/PATCH/DELETE, we can emulate them by
doing a POST and injecting a `_method` parameter into the action URL. The
Remix server runtime then provides action handlers a `Request` with the
overridden `method` so they are none the wiser.

This provides a more reliable way to use these methods, which can be more
ergonomic to write and handle than hidden inputs or buttons with values.

Note:
The emulation only works for action requests handled by Remix; if the form is
submitted to another endpoint, it would need to handle the `_method` URL
parameter accordingly.
@jenseng jenseng force-pushed the emulate-additional-form-methods branch from 75f4cd5 to 18d6351 Compare December 15, 2022 17:27
@MichaelDeBoey
Copy link
Member

As explained in #4420 (reply in thread), I don't think it's a good idea to make non-JS & JS work the same in this case

I'm personally in favor of dropping non-GET/POST tbh, as that makes us work closer to the spec.
Although it's maybe not that much discouraged in the current Form.method docs, we did make sure all other docs use GET/POST & set the submit button's name to 'intent' if necessary not that long ago.

Not discouraging using non-GET/POST would make people also want to use <button formmethod="delete|patch|put" />, which is something we'll never be able to support without a custom Button component on our end & some crazy logic on our end (as perfectly explained by @jenseng above & in #4496 (comment))
We have <button name="intent" value="update">...</button> to support these kind of things anyways, so I would rather make sure the docs are more pushing towards that instead tbh.

So what I think we should do is:

  • Keep the current support for non-GET/POST, but add a deprecation warning when people are using them
  • Deprecate non-GET/POST in v2

I agree that this would mean non-JS isn't working the same as when JS is enabled, which is something we should fix in most cases.
I do however think we shouldn't fix it in this case, as that would encourage using non-GET/POST more, which is (as I explained above) not a good thing imo.

@github-actions github-actions bot added the needs-response We need a response from the original author about this issue/PR label Apr 30, 2023
@machour machour removed the needs-response We need a response from the original author about this issue/PR label Apr 30, 2023
@remix-run remix-run deleted a comment from github-actions bot Apr 30, 2023
@jenseng
Copy link
Contributor Author

jenseng commented May 1, 2023

Happy to rebase/revisit this if we can get consensus from the Remix team... do we 1. want to move forward with emulation or 2. just want to deprecate those methods in v2 (or later?)

I think there are good arguments to be made for and against either approach. My personal preference is emulation, but either option would be preferable to the inconsistent status quo.

@machour machour assigned brophdawg11 and unassigned brophdawg11 May 1, 2023
@brophdawg11
Copy link
Contributor

I think this should be considered in conjunction with remix-run/react-router#10324 (reply in thread). I think the crux of the question is "Should any version of <Form> work when JS is not available"?

One one hand, it would be nice if it could - and in this specific case we could do it with the approach here and it would "just work" with the users action.

If we introduce <Form encType="application/json"> as stated in the above "better actions" proposal, then we've given the user a way to render a <Form> that won't work without JS since their action will read from request.json which will never be populated without JS. It's opt-in so I think we would just need to be clear in the docs that with that option you're opting out of PE.

But generally I think we should stay consistent. Either <Form> attrs align directly with underlying browser-supported <form> attrs (and you useSubmit if you need/want to opt-out of PE), or they don't and we clearly document which attr values will opt-out of PE.

Definitely will need to get @mjackson and @ryanflorence to weigh in on a choice here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants