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

Better handling of checkbox edge cases #51

Open
brophdawg11 opened this issue Feb 9, 2023 · 2 comments
Open

Better handling of checkbox edge cases #51

brophdawg11 opened this issue Feb 9, 2023 · 2 comments

Comments

@brophdawg11
Copy link
Owner

Related to some of the comments in #46

Checkboxes pose some interesting challenges compared to other inputs:

  • They're sort of inherently multiple
  • I tend to view them as a different UI for <select multiple>
  • While you can assign different attributes to each individual checkbox, it makes no sense to me and they should be validated as a group. Either you have to check at least one box or you don't.

So right now they use a single useValidatedInput and you are expected to spread getInputAttrs onto each checkbox and provide a value.

However, right now they are not re-checked in the no-js submission case since we only fill defaultValue at the moment and we would need to set checked instead.

Also think about whether checkboxes should disallow or require multiple in the definition?

@andrecasal
Copy link

andrecasal commented Mar 1, 2023

Yeah, the fact that if a checkbox is unchecked when its form is submitted, there is no value submitted to the server to represent its unchecked state (e.g. value=unchecked) is quite challenging.

If you wanted to submit a default value for the checkbox when it is unchecked, you could include an <input type="hidden"> inside the form with the same name and a value of false for example.

This works if you only have a single checkbox for which you add the default value, but doesn't work if you have more than one checkbox because remix-validity-state will render the same name for all checkboxes.

My use case

This code:

type formSchema = {
	inputs: {
		schedule: InputDefinition
	}
}

const formDefinition: formSchema = {
	inputs: {
		schedule: {
			validationAttrs: {
				type: 'checkbox',
			},
		},
	},
}

export const ScheduleInfo = ({ schedule }: { schedule: UserSchedule[] }) => {
	const fetcher = useFetcher()
	const { getInputAttrs, getLabelAttrs } = useValidatedInput<
		typeof formDefinition
	>({ formDefinition, name: 'schedule' })

	// Since we'll share these attributes across all checkboxes we call these
	// once here to avoid calling per-input.  And since we put the input inside
	// the label we don't need the `for` attribute
	const labelAttrs = getLabelAttrs({ htmlFor: undefined })
	const inputAttrs = getInputAttrs()

	return (
		<div>
			<fetcher.Form method="post" action="/profile/scheduleinfo">
				<div className="flex gap-8">
					{schedule.map(({ day, times }) => (
						<fieldset key={day}>
							<legend>{day}</legend>
							{times.map(({ time, free }) => (
								<label key={`${day}-${time}`} {...labelAttrs} className="block">
									{`${time} - ${time + 1}`}
									<input
										{...{
											...inputAttrs,
											defaultChecked: free,
											value: `${day}-${time}`,
										}}
									/>
								</label>
							))}
						</fieldset>
					))}
				</div>
				<button
					className="btn-secondary"
					type="submit"
					disabled={fetcher.state !== 'idle'}
				>
					{fetcher.state !== 'idle' ? 'Saving...' : 'Save'}
				</button>
			</fetcher.Form>
		</div>
	)
}

This renders something like this:

<div>
	<fieldset>
		<legend>Monday</legend>
		<label>7 - 8
			<input type="checkbox" name="schedule" value="Monday-7" />
		</label>
		<label>8 - 9
			<input type="checkbox" name="schedule" value="Monday-8" />
		</label>
		...
	</fieldset>
	<fieldset>
		<legend>Tuesday</legend>
		<label>7 - 8
			<input type="checkbox" name="schedule" value="Tuesday-7" />
		</label>
		<label>8 - 9
			<input type="checkbox" name="schedule" value="Tuesday-8" />
		</label>
		...
	</fieldset>
	...
</div>

The challenge is that the backend receives an array of strings:

[
	'Monday-7',			'Monday-8',			'Monday-9',			'Monday-10',		'Monday-11',
	'Monday-12',		'Monday-13',		'Monday-14',		'Monday-15',		'Monday-16',
	'Monday-17',		'Monday-18',		'Monday-19',		'Monday-20',		'Monday-21',
	'Monday-22',		'Monday-23',		'Tuesday-7',		'Tuesday-8',		'Tuesday-9',
	'Tuesday-10',		'Tuesday-11',		'Tuesday-12',		'Tuesday-13',		'Tuesday-14',
	'Tuesday-15',		'Tuesday-16',		'Tuesday-17',		'Tuesday-18',		'Tuesday-19',
	'Tuesday-20',		'Tuesday-21',		'Tuesday-22',		'Tuesday-23',		'Wednesday-7',
	...
]

And many of those strings will not be there if the checkboxes are uncheckex, which forces the backend to know, beforehand, which checkboxes exist, which make the code brittle.

@brophdawg11
Copy link
Owner Author

I agree the default behavior of HTML checkboxes is challenging, but I also don't want to change the semantics too much in the remix-validity-state layer. For example, if FormData will not have a value for an unchecked checkbox, I don't think it's desirable for serverFormInfo.submittedValues to have a default value either. Part of the goal of this library is to force people to learn the underlying platform (HTML validation attrs, ValidityState, FormData, etc.) versus learning an abstraction.

Generally it feels like your backend has to know something about the potential incoming values, doesn't it? Otherwise how do you validate any of the incoming values? In your example, where is schedule coming from?

If you're looking to know all possible checkboxes as well as whether they were checked, you could dump them in a single hidden input:

<input type="hidden" name="available-fruits" value="apple,orange,lemon" />
<input type="checkbox" name="fruits" value="apple" />
<input type="checkbox" name="fruits" value="orange" />
<input type="checkbox" name="fruits" value="lemon" />

But remember that users can alter that hidden input so you can't necessarily blindly trust it depending on the use-case. It feels like most of the time your backend will be providing the data that generates the various checkboxes in the first place, so you should be able to hook into that in some fashion?

export async function loader() {
  // loader returns data that drives what checkboxes are displayed
  let fruits = await getAvailableFruits();
  return json({ fruits });
}

export async function action({ request }) {
  const formData = await request.formData();
  const serverFormInfo = await validateServerFormData(
    formData,
    formDefinitions
  );
   
  // Action can grab data from the same source to know what checkboxes existed
  const fruits = await getAvailableFruits();
  ...
}

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

No branches or pull requests

2 participants