brophdawg11 / remix-validity-state Goto Github PK
View Code? Open in Web Editor NEWEnhanced HTML Inputs for Remix
License: ISC License
Enhanced HTML Inputs for Remix
License: ISC License
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.
That way if I render multiple of the same input field I don't violate HTML :)
Related to some of the comments in #46
Checkboxes pose some interesting challenges compared to other inputs:
multiple
<select multiple>
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?
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?
Right now, submittedValues
fields are always string[] | string | null
to account for various scenarios:
string[]
- multiple inputs of the same name are submittedstring
- single inputs of the same name are submittednull
- 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
string
for single-radio/single-checkbox scenarios, but might be fine to just use a single-element array therestring[]
if a field defines multiple: true
in formDefinitions.inputs[name]
string
- Everything elseHey, 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... }
}
}
);
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.
Could we make the object passed to getErrorsAttrs
optional?
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>
)
}
I style with Tailwind and would prefer to not have other classNames on my elements.
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.
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.
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.
This should have aria-invalid
and aria-describedby
which points to the errors element.
For example: https://github.com/remix-run/indie-stack/blob/2d7b4768ad419896bbb899d75ac4cb71c1f6aaa0/app/routes/login.tsx#L102-L110
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:
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."
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?
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).
if (!serverFormInfo.valid) {
return json({ serverFormInfo }, { status: 400 })
}
serverFormInfo... // <-- I'd like to have something here that autocompletes and typechecks with all my fields.
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'.
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'.
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: () => { ... }
}
}
I have a "confirm password" field that needs to be validated against the password field. Can custom validators get all the form fields?
I've got use cases for each of these :)
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.
Using useValidatedInput() for a chechbox forces entire component to re-render.
From the React DevTools Profiler:
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
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.
So I can specify my own. Similar to what we do in downshift here: https://github.com/downshift-js/downshift/blob/10542494cbb42f27e61234de831de20a606940a4/src/downshift.js#L495 https://github.com/downshift-js/downshift/blob/10542494cbb42f27e61234de831de20a606940a4/src/utils.js#L107-L117
Trying to access the value of one input inside another input component using useValidatedInput
but when you call it without destructuring getInputAttrs
inside an input element, you get that error. I'd just like to read the info
. Should this work?
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.
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
For example, this UI:
Powered by this code: https://github.com/kentcdodds/fakebooks-remix/blob/15daa8e1c76a87df7f2af044d9b710d071c2a1f2/app/routes/__app/sales/invoices/new.tsx#L175-L203
And on the server that's validated like this: https://github.com/kentcdodds/fakebooks-remix/blob/15daa8e1c76a87df7f2af044d9b710d071c2a1f2/app/routes/__app/sales/invoices/new.tsx#L79-L93
I wonder how that would look with validity-state
A declarative, efficient, and flexible JavaScript library for building user interfaces.
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. πππ
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google β€οΈ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.