Giter Club home page Giter Club logo

remix-validity-state's People

Contributors

brophdawg11 avatar chaance avatar csflorin avatar kentcdodds avatar michaeldeboey avatar nickmccurdy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

remix-validity-state's Issues

Improve type of getErrorsAttrs

I think it's safe to have the opinion that getErrorsAttrs should be spread on a ul so it would make sense to type the overrides object as attributes for that.

Better handling of checkbox edge cases

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?

Better TypeScript types on validation attributes

TypeScript isn't happy with this:

type FormValidations = {
	inputs: {
		email: InputDefinition
	}
}

const formValidations: FormValidations = {
	inputs: {
		email: {
			validationAttrs: {
				type: 'email',
				required: true,
				minLength: min_email_length
			}
		}
	}
}

const Login = () => {
	return (
		<StyledForm method="post">
			<div>
				<label>Email<input {...formValidations.inputs.email.validationAttrs} /></label>
			</div>
			<Button type="submit" action="primary">Login</Button>
		</StyledForm>
	)
}

I'm getting this error on any validation attribute:

Types of property 'maxLength' are incompatible.
Type 'number | BuiltInValidationAttrsFunction<number>' is not assignable to type 'number | undefined'.
Type 'BuiltInValidationAttrsFunction<number>' is not assignable to type 'number'.

But this fixes it:

-declare type BuiltInValidationAttrsFunction<T> = (fd: FormData) => T | null | undefined;
interface BuiltInValidationAttrs {
-    type?: string | BuiltInValidationAttrsFunction<string>;
-    required?: boolean | BuiltInValidationAttrsFunction<boolean>;
-    minLength?: number | BuiltInValidationAttrsFunction<number>;
-    maxLength?: number | BuiltInValidationAttrsFunction<number>;
-    min?: number | BuiltInValidationAttrsFunction<number>;
-    max?: number | BuiltInValidationAttrsFunction<number>;
-    pattern?: string | BuiltInValidationAttrsFunction<string>;
+    type?: string;
+    required?: boolean;
+    minLength?: number;
+    maxLength?: number;
+    min?: number;
+    max?: number;
+    pattern?: string;
}

The optional property operator ? is already there making it possibly undefined.

What is the BuiltInValidationAttrsFunction<T> helping with?

Better Typescript Types on serverFormInfo.submittedValues

Right now, submittedValues fields are always string[] | string | null to account for various scenarios:

  • string[] - multiple inputs of the same name are submitted
  • string - single inputs of the same name are submitted
  • null - radio/checkbox with nothing selected will not submit any values for the name

It would be nice if these could be inferred somehow?

  • string[] | null - radio/checkbox inputs
    • Could include string for single-radio/single-checkbox scenarios, but might be fine to just use a single-element array there
  • string[] if a field defines multiple: true in formDefinitions.inputs[name]
  • string - Everything else

Server-only custom validation

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... }
    }
  }
);

Expose form-level `valid` prop on client side for submission prevention?

Still thinking about whether this is needed, but right now when you submit to the server you get back a form-level valid prop. But since all of our validation is field-level on the client there's a not exactly an easy way to prevent submission on failed client side navigation. Arguably, this is less-common with real-time inline error messaging but it's still nice to have.

<FormProvider ...>
  <form onSubmit={(e) => {
    // what to check here?
    if (!valid) {
      e.preventDefault();
    }
  }}>
    ...
  </form>
</FormProvider>

If you're not using custom validations you could check directly via the DOM:

<form onSubmit={(e) => {
  let inputs = Array.from(e.currentTarget.elements);
  let isInvalid = inputs.some((e) => e.validity?.valid === false);
  if (isInvalid) {
    e.preventDefault();
  }
}}>

But that doesn't account for customValidations nor does it give you a way to disable a submit button when the form is invalid. So I think this is likely going to require something at the FormProvider level.

Bug updating state of select element

When having two-way data binding, the select doesn't update on first try, only second.

Minimal reproduction on StackBlitz

import { Link } from '@remix-run/react'
import type { ChangeEvent } from 'react'
import { useState } from 'react'
import { useValidatedSelect } from 'remix-validity-state'
import type { SelectDefinition } from 'remix-validity-state'

type FormSchema = {
	inputs: {
		select: SelectDefinition
	}
}

const formDefinition = {
	inputs: {
		select: {
			element: 'select',
			validationAttrs: {
				required: true,
			},
		},
	},
} satisfies FormSchema

export default function Index() {
	const options = [
		{ value: '', text: '--Choose an option--' },
		{ value: 'apple', text: 'Apple 🍏' },
		{ value: 'banana', text: 'Banana 🍌' },
		{ value: 'kiwi', text: 'Kiwi πŸ₯' },
	]

	const [value, setValue] = useState<string>(options[0].value)
	const select = useValidatedSelect({ name: 'select', formDefinition })
	const selectAttributes = select.getSelectAttrs()

	const onChange = (event: ChangeEvent<HTMLSelectElement>) => {
		console.log(event.target.value)
		setValue(event.target.value)
	}

	return (
		<form>
			<select {...selectAttributes} value={value} onChange={onChange}>
				{options.map((option) => (
					<option key={option.value} value={option.value}>
						{option.text}
					</option>
				))}
			</select>
		</form>
	)
}

Date range validation

I'm building a date range picker with two date inputs and I'd like to validate that the end date does not come before the start date and the start is not after the end. It would be cool if I could somehow dynamically set the min and max based on other fields in the form or even have some way to set that via useValidatedInput/Field/etc. so I can control the state myself with whatever I need and just pass what those validation values should be dynamically.

Allow conditionally rendered optional inputs

You cannot currently have an input that is optional and only sometimes rendered. This is because calling useValidatedInput requires its ref to be assigned to an input. If it's not, you get the following error:

Error in validateInput useEffect Error: validateInput expected an inputEl.form to be available for input "inputName"
    at invariant2 (index.tsx:338:11)
    at validateInput (index.tsx:419:5)
    at go (index.tsx:975:26)
    at index.tsx:1010:5
    at commitHookEffectListMount (react-dom.development.js:23150:26)
    at invokePassiveEffectMountInDEV (react-dom.development.js:25154:13)
    at invokeEffectsInDev (react-dom.development.js:27351:11)
    at commitDoubleInvokeEffectsInDEV (react-dom.development.js:27330:7)
    at flushPassiveEffectsImpl (react-dom.development.js:27056:5)
    at flushPassiveEffects (react-dom.development.js:26984:14)

You cannot conditionally call the hook itself of course, so it makes it impossible to have a situation like: if you are an admin, have an extra field in your "create item" form for the item's value. When a normal user creates an item, we do not want to render or validate that field because only admins can assign value. But when an admin creates an item, you want to validate that the item's value is in a range using this lovely library.

Hope that example makes sense.

Incorrect input value during async validation

See #16 (comment)

Consider the scenario:

let formValidations = { 
  thing: { 
    async customMinLength(value) {
        await new Promise(r => setTimeout(r, 1000));
        return value?.length > 5;
    },
  }
}

let errorMessages = {
  customMinLength: (_, __, value) => `${value} must be at least 5 characters`
};
let { getInputAttrs } = useValidatedInput({ name: "thing });

// render
<input {...getInputAttrs()} />;

When the user types matt and waits, it'll start an async validation which will fail after a second and display matt must be at least 5 characters.

However, if they then quickly type hew - it will perform a new async validation for matthew which will pass - but during the validation the error message will be updated to incorrectly display matthew must be at least 5 characters.

Need to effectively freeze the error message during subsequent async validations.

Consider allowing controlled inputsβ€”example of problem with HeadlessUI Listbox

I realize you said you don't want to go this way, but I'm trying to use this with HeadlessUI Listbox but their component has some issues that prevent it from being used uncontrolled in a form:

  1. It doesn't trigger the form's onChange, meaning the value you expose is not updated. I filed an issue a couple weeks ago there for another component (but Listbox also seems to exhibit it) along with an idea of the solution but no word yet tailwindlabs/headlessui#2180
  2. When uncontrolled, they use a hidden input to track the value in the DOM, but something is causing the inputEl.form to not be availableβ€”maybe they are not forwarding the refβ€”and this library errors.
  3. The field's onChange is a callback with just the value, not the full event like this library might expect from a native element.

Therefore, Listbox is only usable as a controlled input when used in a HTML form.

Here is a minimal repro. Keep in mind we use an InputDefinition because the Listbox doesn't render a proper select; it just renders a hidden input whose value is kept in sync with the component.

It's common for UI library inputs to not work uncontrolled, so other form validation libraries like React Hook Form provide a way to work with controlled inputs. RHF says "React Hook Form embraces uncontrolled components and native inputs, however it's hard to avoid working with external controlled component such as React-Select, AntD and MUI. This wrapper component will make it easier for you to work with them."

useValidatedInput cannot be used outside a FormContextProvider

I think it's supposed to be able to, but it looks like in a recent version useFormContext added an invarient. I know that makes TypeScript easier which is nice, but in my case I'd like to avoid rendering the FormContextProvider and instead just use useValidatedInput. How about creating a useOptionalFormContext which can be used instead?

Type check `name` based on `formValidations`?

It would be awesome if this typechecked:

const confirmPasswordField = useValidatedInput({
  name: 'confirm-password',
  formValidations,
  errorMessages,
  serverFormInfo: actionData?.serverFormInfo,
})

So if my formValidations didn't have a confirm-password field I'd get a warning on the name (or vice versa, I'd just like to know I made a typo).

Make serverFormInfo typesafe

    if (!serverFormInfo.valid) {
		return json({ serverFormInfo }, { status: 400 })
	}
	serverFormInfo... // <-- I'd like to have something here that autocompletes and typechecks with all my fields.

Add support for min/max strings on date input

This errors out:

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

const formDefinition: formSchema = {
	inputs: {
		date: {
			validationAttrs: {
				type: 'date',
				min: new Date().toLocaleDateString('en-us') // πŸ‘ˆ TS error
			}
		}
	},
}

Error:

Type 'Date' is not assignable to type 'number | BuiltInValidationAttrsFunction<number> | undefined'.

Type mismatch on React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>

Simple newsletter subscription example:

import { Form } from '@remix-run/react'
import { validateServerFormData, InputDefinition } from 'remix-validity-state'
import { json, TypedResponse } from '@remix-run/node'
import { DataFunctionArgs } from '@remix-run/node'

type ActionData = {
	fields?: {
		email: string
	}
	formError?: string
}

const badRequest = (data: ActionData): TypedResponse<ActionData> => json(data, { status: 400 })

type FormSchema = {
	inputs: {
		email: InputDefinition
	}
}

const formDefinition: FormSchema = {
	inputs: {
		email: {
			validationAttrs: {
				type: 'email',
			}
		}
	}
}

export const action: ActionFunction = async ({ request }) => {
	const { valid, submittedValues, inputs } = await validateServerFormData(await request.formData(), formDefinition)
	if (!valid) {
		return badRequest({ formError: `Form not submitted correctly` })
	}

	// All ok, use submittedValues
}

const Newsletter = () => {
	return <Form method="post">
		<h2>Subscribe!</h2>
		<p>Don't miss any of the action!</p>
		<fieldset>
			<input {...formDefinition.inputs.email.validationAttrs} name="email" placeholder="[email protected]" />
			<button type="submit">Subscribe</button>
		</fieldset>
	</Form>
}

export default Newsletter

Notice there's a TS error on the <input ... />:
Type 'BuiltInValidationAttrsFunction<number>' is not assignable to type 'number'.

Full TS error:

Type '{
	name: string;
	placeholder: string;
} | {
	name: string;
	placeholder: string;
	type?: "url" | "text" | "search" | "tel" | "email" | "password" | undefined;
	required?: boolean | BuiltInValidationAttrsFunction<boolean> | undefined;
	minLength?: number | ... 1 more ... | undefined;
	maxLength?: number | ... 1 more ... | u...
}' is not assignable to type 'DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>'.

Type '{
	name: string;
	placeholder: string;
	type?: "url" | "text" | "search" | "tel" | "email" | "password" | undefined;
	required?: boolean | BuiltInValidationAttrsFunction<boolean> | undefined;
	minLength?: number | ... 1 more ... | undefined;
	maxLength?: number | ... 1 more ... | undefined;
	pattern?: string | ... 1 more ...
}' is not assignable to type 'DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>'.
   
Type '{
	name: string;
	placeholder: string;
	type?: "url" | "text" | "search" | "tel" | "email" | "password" | undefined;
	required?: boolean | BuiltInValidationAttrsFunction<boolean> | undefined;
	minLength?: number | ... 1 more ... | undefined;
	maxLength?: number | ... 1 more ... | undefined;
	pattern?: string | ... 1 more ...
}' is not assignable to type 'InputHTMLAttributes<HTMLInputElement>'.

Types of property 'maxLength' are incompatible.

Type 'number | BuiltInValidationAttrsFunction<number> | undefined' is not assignable to type 'number | undefined'.

Type 'BuiltInValidationAttrsFunction<number>' is not assignable to type 'number'.

Better Typescript validation on errorMessages

errorMessages keys should ideally be restricted to ValidityStateKey unioned with keys in customValidations.

let formDefinitions: FormSchema = {
  inputs: {
    name: {
      validationAttrs: { 
        required: true,
      },
    },
    custom: {
      customValidations: {
        isThisThingValid() { ... }
      },
      errorMessages: {
        // βœ… OK since this matches a customValidation
        isThisThingValid: () => { ... }
        // ❌ Should fail because nope is not a ValidityStateKey or a customValidation name
        nope: () => { ... }
      },
    },
  },
  errorMessages: {
    // βœ… OK since this matches a ValidityStateKey
    valueMissing: () => { ... }
    // ❌ Should fail because nope is not a ValidityStateKey or a custom validation name
    nope: () => { ... }
  }
}

Feature Request: Form checkValidity method

It would be super helpful to have something just like the form's native checkValidity method https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement/checkValidity but that includes custom validity checks. This would make it so we don't have to do something like

Boolean(
  Object.entries({
    ...nameInput.info.errorMessages,
    ...addressInput.info.errorMessages,
    ...
  }).length
)

if we want to check if the entire form is valid to, for example, disable the submit button until everything is valid.

Question: should whole component re-render on checkbox onChange?

Using useValidatedInput() for a chechbox forces entire component to re-render.

From the React DevTools Profiler:
Screenshot 2023-03-10 at 13 09 00

This is breaking Cypress tests:

cy.findAllByLabelText('7 - 8').should('have.length', 7).click({ multiple: true })
cy.findAllByLabelText('17 - 18').should('have.length', 7).click({ multiple: true })
cy.findAllByLabelText('23 - 24').should('have.length', 7).click({ multiple: true }) // πŸ‘ˆ fires a React re-render on the whole <form>
cy.intercept('POST', '**/profile/scheduleinfo*').as('updateSchedule')
cy.findByText('Guardar').click() // πŸ‘ˆ doesn't fire a form submission because React will re-render
cy.wait('@updateSchedule') // πŸ‘ˆ test fails because this network request never happens

I'm wondering why is the entire component re-rendering and if it should πŸ€”

Fix select type

I'm getting this error:

Type 'ChangeEventHandler<HTMLInputElement> | undefined' is not assignable to type 'ChangeEventHandler<HTMLSelectElement> | undefined'.

On a select element:

type FormSchema = {
	inputs: {
		year: SelectDefinition
	}
}

const formDefinition = {
	inputs: {
		year: { element: 'select', validationAttrs: { required: true } },
	},
} satisfies FormSchema

export const SubjectsInfo = ({ subjects }: { subjects: UserSubjects[] }) => {
	const fetcher = useFetcher()
	const yearInput = useValidatedInput({ name: 'year', formDefinition })

	return (
		<fetcher.Form method="post" action="/profile/subjectsinfo">
			<select {...yearInput.getInputAttrs()}>
				<option value="FIRST">1ΒΊ Year</option>
				<option value="SECOND">2ΒΊ Year </option>
				<option value="THIRD">3ΒΊ Year </option>
			</select>
			<button type="submit">
				{fetcher.state !== 'idle' ? 'Adding...' : 'Add'}
			</button>
		</fetcher.Form>
	)
}

I'm not sure if this was fixed by #58, but if not we might need to update the onChange type from ChangeEventHandler<HTMLInputElement> to ChangeEventHandler<HTMLSelectElement> for <select>s.

type="email" is not being validated on the server

Simple newsletter subscription example:

import { Form } from '@remix-run/react'
import { validateServerFormData, InputDefinition } from 'remix-validity-state'
import { json, TypedResponse } from '@remix-run/node'
import { DataFunctionArgs } from '@remix-run/node'

type ActionData = {
	fields?: {
		email: string
	}
	formError?: string
}

const badRequest = (data: ActionData): TypedResponse<ActionData> => json(data, { status: 400 })

type FormSchema = {
	inputs: {
		email: InputDefinition
	}
}

const formDefinition: FormSchema = {
	inputs: {
		email: {
			validationAttrs: {
				type: 'email',
				required: true,
			}
		}
	}
}

export const action: ActionFunction = async ({ request }) => {
	const { valid, submittedValues, inputs } = await validateServerFormData(await request.formData(), formDefinition)
	if (!valid) {
		return badRequest({ formError: `Form not submitted correctly` })
	}

	// All ok, use submittedValues
}

const Newsletter = () => {
	return <Form method="post">
		<h2>Subscribe!</h2>
		<p>Don't miss any of the action!</p>
		<fieldset>
			<input {...{
				...formDefinition.inputs.email.validationAttrs,
				type: 'text' // πŸ‘ˆ Replace type="email" with type="text" to simulate user tampering with UI
			}} name="email" placeholder="[email protected]" />
			<button type="submit">Subscribe</button>
		</fieldset>
	</Form>
}

export default Newsletter

Submit an invalid email and notice the type="email" validation fails on the server.

Validation handling

On a simple login page:

type FormSchema = {
	inputs: {
		email: InputDefinition
		password: InputDefinition
	}
}

const formDefinition = {
	inputs: {
		email: {
			validationAttrs: {
				type: 'email',
				required: true,
			},
		},
		password: {
			validationAttrs: {
				type: 'password',
				required: true,
				minLength: min_password_length,
			},
		},
	},
} satisfies FormSchema

type ActionData = {
	fields?: {
		email: string
		password: string
	}
	formError?: string
}

export const action = async ({ request }: ActionArgs): Promise<TypedResponse<ActionData | never>> => {
	// Parse form data
	const { valid, submittedValues: fields } = await validateServerFormData(await request.formData(), formDefinition)
	if (!valid) {
		throw badRequest<ActionData>({ formError: `Form not submitted correctly` })
	}

	// Check if password is correct
	const user = await login(fields)
	if (!user) {
		return badRequest({ fields, formError: `Email/password combination is incorrect` })
	}

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

const Login = () => {
	const { fields, formError } = useActionData<ActionData>() || {}
	const emailInput = useValidatedInput({ formDefinition, name: 'email' })
	const passwordInput = useValidatedInput({ formDefinition, name: 'password' })
	const transition = useTransition()

	return (
		<Form method="post">
			<label>
				Email:
				<input {...emailInput.getInputAttrs()} className="form-input" defaultValue={fields?.email} />
			</label>				<label>
				Password:
				<input {...passwordInput.getInputAttrs()} className="form-input" defaultValue={fields?.password} />
			</label>
			{formError ? <p role="alert">{formError}</p> : null}
			<button className="btn-primary" type="submit">
				{transition.state === 'submitting' || (transition.state === 'loading' && transition.type === 'actionRedirect') ? 'Logging in...' : 'Login'}
			</button>
		</Form>
	)
}

If the user manipulates the DOM and removes both required and changes type="email" to type="text" and inserts a string that's not an email, const { valid, submittedValues: fields } = await validateServerFormData(await request.formData(), formDefinition) still results in a true valid. Given required needs the field to have something, shouldn't validateServerFormData() not only check if the email and password fields are POSTed, but also that their strings are not empty?

Also I thought email validation was already working in v0.11.0 πŸ€”

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.