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

Be able to setup a field level validation schema #25

Closed
outerlook opened this issue May 22, 2021 · 14 comments
Closed

Be able to setup a field level validation schema #25

outerlook opened this issue May 22, 2021 · 14 comments
Labels
enhancement New feature or request felte Related to main felte package

Comments

@outerlook
Copy link

outerlook commented May 22, 2021

Description

Hi! It would be nice to set some field level validation.

Example

My example is a multi-step form that shares the same

element. This form asks for phone, name and email.

HTML
 STEP 1
<form>
	<input name='name'>
	<button type='submit'/>
</form>

 STEP 2
<form>
	<input name='phone'>
	<button type='submit'/>
</form>

STEP 3
<form>
	<input name='email'>
	<button type='submit'/>
</form>

I use zod for validating things.

And I'm using the following schema:

### Schema
const validationSchema = z.object({
	name: z.string().nonempty(),
	phone: phoneRegexValidation,
	email: emailValidation
})

Example's issue

Every step is required. But I can't use like that. If all of them are required for submitting, I can't use the button as a submit button for controlling my form. And I do have some clear options:

  • Handle only the last button as submittable, control my "steps" out of onSubmit (a bit boilerplatey)
  • Separate into 3 forms, one with each validation (less svelte composable components power)

There is an option I used on react, using the library react-hook-form

  • dynamically setup my validationSchema when "step" changed
### Schema
const validationSchema = step => z.object({
	name: z.string().nonempty(),
	phone: step > 0 ? phoneRegexValidation : optional,
	email: ztep > 1 ? emailValidation : optional
})
### Creating form context
const { form } = createFormContext({
		validateSchema: validationSchema(step),
		onSubmit: values => {
			if (!isLastStep) step++;
			else submitToServer(values)
		}
	})

But it doesn't seems to work (it could be another issue if I did it right, but that's not the main request here)

Svelte-action

I've being trying to create an svelte action to solve that:

### Implementation
export const validate = (
	node: HTMLInputElement,
	{ schema }: { schema: z.Schema<any> }
) => {
	const listener = (ev: Event) => {
		try {
			schema.parse(node.value)
		} catch (e) {
			ev.preventDefault();
			ev.stopPropagation();
			ev.stopImmediatePropagation();
			const error = e as z.ZodError<any>
			node.setCustomValidity(error.message)
		}
	}
	node.form?.addEventListener('submit', listener)

	return {
		destroy: () => {
			node.form?.removeEventListener('submit', listener)
		}
	}
}
### Usage
	<input
		use:validate={nameValidation}
		name="name"
	/>

But I'm having no success on that. It isn't preventing form onSubmit from being executed. I don't know how to achieve that outside felte.

Further comments

The library I mentioned (react-hook-form) uses a different and less html native for subscribing new fields that can validate at this level.

RHF validation docs

I mention them only for bringing examples of libraries that have this kind of field-level API (and are choosing to continue supporting it).

Another benefit of solving it would be composability problem. A form would not need to know every field that are inside them for validating purposes.

@pablo-abc pablo-abc added felte Related to main felte package enhancement New feature or request labels May 23, 2021
@pablo-abc
Copy link
Owner

Hi! Sorry for the long wait. I broke my arm and am just recovering.

So let's start with my thoughts on this:

  1. I am sort of building this on a direction that would allow you to build your forms in a way that they would work both with or without JS. Taking this into account, the best approach is to have a "submit" button on each step, and each step being a different form. (With JS enabled each one being a different call to felts createForm). What does sound interesting in this sense is maybe creating another export from felte to create multiple forms each one with their own configuration. I'd have to think of a possible API for this (although I do think this might be a bit of overkill).

  2. For using a separate "Next" button that Felte handles without submitting the form (this would work with JS only), I do see the issue you're presenting since you'd want to prevent a user from going to the next step without first validating the current one. For this the validator-zod packages (as well as the others) do export the function that validates the schema directly:

export function validateSchema<Data extends Obj>(
schema: ZodTypeAny
): ValidationFunction<Data> {
function shapeErrors(errors: ZodError): Errors<Data> {
return errors.errors.reduce((err, value) => {
if (!value.path) return err;
return _set(err, value.path.join('.'), value.message);
}, {});
}
return async function validate(
values: Data
): Promise<Errors<Data> | undefined> {
try {
await schema.parseAsync(values);
} catch (error) {
return shapeErrors(error);
}
};
}

So instead of "extending" felte with the validator you may use that function directly inside of your validate function. That way you may use a different schema whenever the step changes.

Do you feel one of these suggestions work for you? Or is there a specific API you might have in mind? I am completely open to suggestions

@outerlook
Copy link
Author

Move fast and break things shouldn't be taken so seriously. Sorry for your arm.

It's interesting to know this "with or without JS" intention. With that in mind I do prefer to create an solution based on my app over that second suggestion's function and not grow the library API on a wrong direction.

I will try that by tomorrow and I'll post it here how it ended being if this helps someone. Thanks!

@ryanrussell
Copy link

@outerlook @pablo-abc

Currently trying to solve the same issue.

Would be very helpful if you could share a multi-step form demo on a codesandbox if you find a solution you are happy with and feel like adheres to best practices.

Thanks!

@outerlook
Copy link
Author

Here is at REPL

https://svelte.dev/repl/d83a688976fd466ab0a1d55400ab94f6?version=3.38.2

A bit simple but makes it, right?
There is this thing of already errored password input, but I kept it simple...

@outerlook
Copy link
Author

So, the problem of a multi-step validation could be taken easily. But the other part of this issue is about a method of transfering validations to a input directly, something like HTML does with

<input required min=6 name='phone'/>

but not possible if we want more complex validations.

<input use:validate={...}/>

This api would be against the library direction as it requires some changes that could be only used with js?

Just asking, we can close the issue if is not the intention

@pablo-abc
Copy link
Owner

Hey all, after seeing #29 it seems that the most dev friendly way to solve for multi-step forms is to use a separate form per step. For a couple reasons:

  1. As @ryanrussell mentioned on multistep reporter displays errors on steps 2+ and select #29 if a single form is used and you use an <input type="submit"> to go to the next step, the next step will show all validation errors since the form has already "submitted" and all fields have been "touched". This could be mitigated by setting the fields of the other steps to untouched. But it requires manual work and I don't see how Felte could do this internally since we would need to differentiate between an actual multi-step form from a simply a hidden field.

  2. If using a <button type="button"> to go to the next step then validation would need to be triggered manually using a helper. And again this would validate all fields of the form. As mentioned above, switching the validation schema per step might be an option but it's a little bit cumbersome.

If we do go towards the a separate form for each step route, would a helper to create multiple forms be useful at all? That seems to me like something trivial anyone could do if they actually need it on the project. Or having each step being a component with its own createForm call. I don't see much value on adding something like this.

Is there a use-case that would be made too complex by using a separate form for each step?

@pablo-abc
Copy link
Owner

pablo-abc commented Jun 15, 2021

@outerlook The way I'd see this is that JS only "enhances" the experience. So most likely if you're planning on making your form work also without JS you would anyway need to add the attributes such as required, min=..., etc. This is why Felte adds the novalidate attribute on load, since you'll definitely not want browser validation if you already are using JS validation.

Using an action like you mentioned does raise a couple questions regarding implementation. Mainly if it would need to be implemented in the core package or if it's a niche enough use-case that can somehow be handled using an extender package (although this approach also raises some uncertainties).

Another thing is that most of the issues we are finding have more to do with when Felte sets a field to "touched" rather than when/how it validates it. Validation occurs always, but the error won't be present on the "errors" store until the field is "touched", which currently happens on blur and on change per field, or on all fields when the form is submitted.

@pablo-abc
Copy link
Owner

pablo-abc commented Jun 17, 2021

So regarding multi-page forms I've posted a package to help with it, do not see this as a best practice yet since I am open for this to change a lot based on usage. There might be a lot of edge cases not considered. Seeing as the implementation is not so big this could eventually be merged to the core package. Although the current assumptions make it very simplistic.

Feel free to test it. I've created a small example on CodeSandbox to showcase its usage. Do let me know of any edge cases you might find.

@ryanrussell
Copy link

@pablo-abc

Awesome! I had built this with my own custom store and counter for a specific use case, but your implementation looks more robust and I see you've already stubbed out decreaseStep() as well.

Official author support goes without saying as a better path...

Will port using your package, and post back if I find any edge cases or difficulties along the way.

Thanks so much for jumping on these bug and feature requests so quickly. You're a one man army!

@ryanrussell
Copy link

Just following up.

Your multi step package seems to work well for me so far, I'd suggest closing this out...

If I come across some edge cases, I can put them in a new issue.

Thanks again for the support!

@pablo-abc
Copy link
Owner

@ryanrussell I'm leaving this open as I am still exploring per-field validation to handle this as well. But do open a new issue if you find anything in that package!

@outerlook
Copy link
Author

I'm sorry I could not reach up earlier. I am reading it all but right now we are delivering a 650 files migration to svelte 😅

I imagine I will be able to test and contribute with some thoughts in 1,5 week...

@benbender
Copy link
Contributor

benbender commented Aug 1, 2021

@pablo-abc what you basically did here is to build a basic state-machine on top of felte.
Tbh, Imho this is not the problem-domain which felte tries to solve. It's a nice example and/or helper but shouldn't go to core. Because as it expands the problem-domain unnecessarily and won't hold up more complex cases anyways.

If your specific usecase isn't covered by the builtin store-concept by svelte (which is used in the helper), use something like @xstate/svelte (btw @xstate/fsm is very lightweight, powerful and a perfect match for this kind of problems).

So, imho there are two solutions to the multi-step/wizard-form-problem (and I have a clear preference):

  • Either you show/hide some fields of the form depending on a step-count or similar. This would basically be only ux-sugar and contradicts fundamental principles of html-forms and (most likely) usability/accessibility. This approach should be (at least) independent from submitting the form in a html/http-sense. If one opts for this approach, there is a ton of problems arising if you try to do it correct for every edge-/use-case as the complexity of every step multiplies.

  • Or, imho the better approach, you create individual forms per step. This could be perfectly handled by svelte as it is. In this case you have to carry over/combine the state of those individual forms with a simple store or a state-machine to submit it to an api or whatever in the last step. Felte's job in this solution is handling the form - and not the state-handling for the process at large.

Btw: here is the react-hook-foms-example for such a case: https://react-hook-form.com/advanced-usage/#WizardFormFunnel and it uses the second approach as laid out above.

That said, there might be use-cases where field-level-validation is desirable (f.e. checking if a username is taken, pw-strength etc) and it would be a fair question if those are the responsibility of felte's validation-layer or if those should live in a customized input-component. I for myself lean towards the latter as it helps keeping bloat out of the nifty little core of felte :)

@pablo-abc
Copy link
Owner

Closing this. Did not find a way that would provide a way to add validators per-field in a comfortable way. The closest might be the validator on #48 tracked in #43 which I must revisit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request felte Related to main felte package
Projects
None yet
Development

No branches or pull requests

4 participants