tailwindlabs / tailwindui-issues

A place to report bugs discovered in Tailwind UI.
234 stars 4 forks source link

Simple Toggle input when name provided #1521

Closed SaizDev closed 9 months ago

SaizDev commented 10 months ago

What component (if applicable)

Describe the bug When provided the switch component with a prop name, an input type hidden should appear. It only appears if the toggle is switched to on. The input dissappear when the toggle is off. https://headlessui.com/react/switch#using-with-html-forms

Expected behavior The input related to the toggle does not disappear when the toggle is off

Screenshots

image image

Browser/Device (if applicable)

adamwathan commented 10 months ago

Hey! This is by design at the moment — it works like an HTML checkbox which works the same way, the only time a value is included in the form data and sent to the server is when the checkbox is checked, otherwise it is omitted 👍

What's the actual problem you're running into as a result? Open to making changes here for sure if there's a good reason to do so.

SaizDev commented 10 months ago

Hey! Thanks for such quick answer!

The problem comes when I want to retrieve the value of the input. I retrieve the value of the input automatically based on a config array previously generated. If the input does not appear, it throws an error. I found a workaround, but makes the code ugly and less reusable.

image

What would you suggest?

thecrypticace commented 9 months ago

@SaizDev It looks like you're pulling data based on the name from the form element.

There's a couple of notes I have about what you've written:

1. Don't use form[field_name]

Using form[input.id] doesn't work with multiple inputs with the same name as it produces a collection instead of a single element. Another thing is that you can run into weird issues when there are form fields with the same name as a property on the form (for example action). When a field with tne name of action is present form.action no longer returns the value of the action attribute but the element (or list of elements if there are multiple). Additionally, for something like a checkbox that value can change conditionally based on whether or not it's checked. It might be a string if unchecked and an element if checked.

The best thing to do here is use new FormData(form) to get the form data which also helps clarify the intent. You can use Object.fromEntries(…) and FormData#entries to get the data into a JS object. Given that you've got a config array this code in your case might be a bit more complex but the idea is still similar:

const handleEdit = (e) => {
  e.preventDefault();
  let submitData = Object.fromEntries(new FormData(e.target).entries())
  submitData['active'] ??= 'false' // your workaround
  editAthlete({id, submitData})
  navigate("/athletes")
}

2. Use a hidden input for default values

In HTML you can have multiple inputs with the same name and in many cases servers will override the value for a given form field with whatever the "latest" value is (though this is not a given — some preserve all values but still in order). The Object.fromEntries trick does the same thing here as well.

So, if you place a hidden input before the Headless UI switch — and it has the same name:

<form>
  <input type="hidden" value="false" name="active" />
  <Switch
    name="active"
    value="true"
  >
    <span className="sr-only">Is Active</span>
  </Switch>
</form>

Coupled with the use of Object.fromEntries(…) and FormData you'll be able to remove your workaround.

const handleEdit = (e) => {
  e.preventDefault();
  let submitData = Object.fromEntries(new FormData(e.target).entries())
  // no workaround :)
  editAthlete({id, submitData})
  navigate("/athletes")
}

Aside: Don't re-create objects in reduce

This isn't directly related — I'd just like to share some knowledge here.

In a line like this: config.reduce((o, input) => ({...o, [input.id]: 'value here'}), {})

This creates objects and copies over properties a lot more than necessary. For example, with an array of 1000 entries this creates 1000 additional objects and copies over properties more than 500k times. (1 on the first, 2 on the second, 3 on the 3rd, etc… up to 1000 on the 1000th item).

You can tweak it like so to keep it roughly the same length and while it still creates more objects than necessary it only copies over properties 1000 times — 1 copy per property: config.reduce((o, input) => Object.assign(o, { [input.id]: 'value here' }), {})

Though, in a small or even semi-large list this isn't really a big deal. Just something to be aware of and figured I'd share :)