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

Server-only custom validation #25

Closed
merelinguist opened this issue Nov 10, 2022 · 3 comments · Fixed by #33
Closed

Server-only custom validation #25

merelinguist opened this issue Nov 10, 2022 · 3 comments · Fixed by #33

Comments

@merelinguist
Copy link

Hey, I can see that it's possible to add custom validation in formValidations, but I’m wondering if it's possible to check this just on the server? I.e. I don't want to have to write an API route just to check if an email if unique, if that makes sense. Something like:

const serverFormInfo = await validateServerFormData(
  formData,
  formValidations,
  {
    email: {
      isUnique: async () => { // database code... }
    }
  }
);
@brophdawg11
Copy link
Owner

Hm, I've usually considered just doing this by altering the custom validation so it's always "valid" when on the client, something like:

const isBrowser =
  typeof window !== "undefined" &&
  typeof window.document !== "undefined";

const formValidations = {
  email: {
    isUnique() {
      if (isBrowser) return true;
      // database code
    },
  },
}

But thinking a bit more about that now, I think that'll end up with some server code in your client bundle. So it may make sense to do something along these lines. I'll think a bit about the best way to tackle this 👍

@andrecasal
Copy link

andrecasal commented Jan 3, 2023

My 2 cents: there's an important distinction between making sure the data coming from the client is processable, and the results of that data processing.

Consider checking for login credentials with an email and password, for example:

// Up to us (or another library) 👇
type ActionData = {
	fields?: {
		email: string
		password: string
	}
	formError?: string
	fieldErrors?: {
		email: string | undefined
		password: string | undefined
	}
}

const badRequest = (data: ActionData): TypedResponse<ActionData> => json(data, { status: 400 })
// Up to us (or another library) 👆

// The scope of remix-validity-state 👇
type FormValidations = {
	email: Validations
	password: Validations
}

const formValidations: FormValidations = {
	email: {
		type: 'email',
		required: true
	},
	password: {
		required: true
	}
}
// The scope of remix-validity-state 👆

export const action = async ({ request }: ActionArgs): Promise<TypedResponse<ActionData | never>> => {
	// The scope of remix-validity-state 👇
	// Parse form data
	const { valid, submittedFormData: { email, password } } = await validateServerFormData(await request.formData(), formValidations)
	if (!valid) {
		return badRequest({ formError: `Form not submitted correctly` })
	}
	// The scope of remix-validity-state 👆

	// Up to us (or another library) 👇
	// Check if password is correct
	const user = await login({ email, password })
	if (!user) {
		return badRequest({
			fields: { email, password },
			formError: `Email/Password combination is incorrect`
		})
	}
	// Up to us (or another library) 👆

	// Correct login credentials: set user in session and redirect
	const redirectTo = new URL(request.url).searchParams.get('redirectTo') || '/'
	return setUserInSession(user, redirectTo)
}

const Login = () => {
	const { fields, formError, fieldErrors } = useActionData<ActionData>() || {}

	const emailInputAttributes = {
		name: "email",
		placeholder: "Type in your email address",
		defaultValue: fields?.email,
		...formValidations.email
	}

	const passwordInputAttributes = {
		type: "password",
		name: "password",
		placeholder: "Type in your password",
		defaultValue: fields?.password,
		...formValidations.password
	}

	return (
		<>
			<h1>Login</h1>
			<Form method="post">
				<label>Email<input {...emailInputAttributes} /></label>
				<p role="alert" id="email-error">{fieldErrors?.email}</p>
				<label>Password<input {...passwordInputAttributes} /></label>
				<p role="alert" id="password-error">{fieldErrors?.password}</p>
				<p role="alert">{formError}</p>
				<Button type="submit" action="primary">Login</Button>
			</Form>
		</>
	)
}

Note that the ActionData type has a fieldErrors field that I'm not using because I don't have server-only validation here, but this is where you'd add your server-only validation errors like the server-only email uniqueness error.

This being said, I think the purpose of a library like remix-validity-state should be to ensure the email and password reached the server in the correct format so that we can then use that data to check if the email and password match - but not to decide what to do if the credentials are wrong, that would be up to us or another library.

However, in the case of email uniqueness, I'd recommend indeed creating an API endpoint so that you can tell the user, as soon as possible, whether or not the email is unique, even before submitting the form.

@brophdawg11
Copy link
Owner

I generally agree that after validateServerFormData is where you would do post-validation stuff. This would include business-specific logic like validating the login attempt, adding the user to the DB, etc. However, for true validation checks I think there's still a case for server-only here since we'd want to be able to hook into the same field-level error UI that we're using for other validations, which would be a bit messy to do after the fact since you'd need to mutate the returned serverFormInfo.inputs[name].uniqueEmail and serverFormInfo.inputs[name].valid.

I like the idea of being able to provide a specific server customValidation that is used instead of the one provided in formDefinition where applicable.

Added in #33 and will release shortly in 0.9.0 👍

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

Successfully merging a pull request may close this issue.

3 participants