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

Clear global errors on new submission #162

Merged
merged 1 commit into from
Feb 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions apps/web/tests/examples/actions/environment.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,7 @@ testWithoutJS('With JS disabled', async ({ example }) => {
// Submit form
await button.click()
await page.reload()
await expect(page.locator('form > div[role="alert"]:visible')).toHaveText(
'Missing custom header',
)
await example.expectGlobalError('Missing custom header')

// Submit with valid headers
await page.setExtraHTTPHeaders({ customHeader: 'foo' })
Expand Down
10 changes: 5 additions & 5 deletions apps/web/tests/examples/actions/global-error.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,17 @@ test('With JS enabled', async ({ example }) => {
await example.expectValid(password)

// Submit form
button.click()
await example.expectNoGlobalError()
await button.click()
await expect(button).toBeDisabled()

// Show global error
await example.expectGlobalError('Wrong email or password')

// Submit valid form
await password.input.fill('supersafe')
button.click()
await button.click()
await example.expectNoGlobalError()
await example.expectData({ email: 'john@doe.com', password: 'supersafe' })
})

Expand Down Expand Up @@ -101,9 +103,7 @@ testWithoutJS('With JS disabled', async ({ example }) => {
await page.reload()

// Show global error
await expect(page.locator('form > div[role="alert"]:visible')).toHaveText(
'Wrong email or password',
)
await example.expectGlobalError('Wrong email or password')

// Submit valid form
await password.input.fill('supersafe')
Expand Down
6 changes: 6 additions & 0 deletions apps/web/tests/setup/example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ class Example {
).toHaveText(message)
}

async expectNoGlobalError() {
expect(
await this.page.locator('form > div[role="alert"]:visible').count(),
).toEqual(0)
}

async expectErrorMessage(fieldName: string, message: string) {
await expect(
this.page.locator(`#errors-for-${fieldName}:visible`),
Expand Down
91 changes: 61 additions & 30 deletions packages/remix-forms/src/createForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,16 @@ function createForm({

const actionErrors = actionData?.errors as FormErrors<SchemaType>
const actionValues = actionData?.values as FormValues<SchemaType>
const errors = { ...errorsProp, ...actionErrors }
const values = { ...valuesProp, ...actionValues }

const errors = React.useMemo(
() => ({ ...errorsProp, ...actionErrors }),
[errorsProp, actionErrors],
)

const values = React.useMemo(
() => ({ ...valuesProp, ...actionValues }),
[valuesProp, actionValues],
)
danielweinmann marked this conversation as resolved.
Show resolved Hide resolved

const schemaShape = objectFromSchema(schema).shape
const defaultValues = mapObject(schemaShape, (key, fieldShape) => {
Expand Down Expand Up @@ -292,10 +300,14 @@ function createForm({
],
)

const fieldErrors = (key: keyof SchemaType) => {
const message = (formErrors[key] as unknown as FieldError)?.message
return browser() ? message && [message] : errors && errors[key]
}
const fieldErrors = React.useCallback(
danielweinmann marked this conversation as resolved.
Show resolved Hide resolved
(key: keyof SchemaType) => {
const message = (formErrors[key] as unknown as FieldError)?.message
return browser() ? message && [message] : errors && errors[key]
},
[errors, formErrors],
)

const firstErroredField = () =>
Object.keys(schemaShape).find((key) => fieldErrors(key)?.length)
const makeField = (key: string) => {
Expand Down Expand Up @@ -333,34 +345,45 @@ function createForm({
} as Field<SchemaType>
}

const hiddenFieldsErrorsToGlobal = (globalErrors: string[] = []) => {
const deepHiddenFieldsErrors = hiddenFields?.map((hiddenField) => {
const hiddenFieldErrors = fieldErrors(hiddenField)

if (hiddenFieldErrors instanceof Array) {
const hiddenFieldLabel =
(labels && labels[hiddenField]) || inferLabel(String(hiddenField))
return hiddenFieldErrors.map(
(error) => `${hiddenFieldLabel}: ${error}`,
)
} else return []
})
const hiddenFieldsErrors: string[] = deepHiddenFieldsErrors?.flat() || []
const hiddenFieldsErrorsToGlobal = React.useCallback(
danielweinmann marked this conversation as resolved.
Show resolved Hide resolved
(globalErrors: string[] = []) => {
const deepHiddenFieldsErrors = hiddenFields?.map((hiddenField) => {
const hiddenFieldErrors = fieldErrors(hiddenField)

if (hiddenFieldErrors instanceof Array) {
const hiddenFieldLabel =
(labels && labels[hiddenField]) || inferLabel(String(hiddenField))
return hiddenFieldErrors.map(
(error) => `${hiddenFieldLabel}: ${error}`,
)
} else return []
})
const hiddenFieldsErrors: string[] =
deepHiddenFieldsErrors?.flat() || []

const allGlobalErrors = ([] as string[])
.concat(globalErrors, hiddenFieldsErrors)
.filter((error) => typeof error === 'string')
const allGlobalErrors = ([] as string[])
.concat(globalErrors, hiddenFieldsErrors)
.filter((error) => typeof error === 'string')

return allGlobalErrors.length > 0 ? allGlobalErrors : undefined
}
return allGlobalErrors.length > 0 ? allGlobalErrors : undefined
},
[fieldErrors, hiddenFields, labels],
)

let globalErrors = hiddenFieldsErrorsToGlobal(errors?._global)
const globalErrors = React.useMemo(
danielweinmann marked this conversation as resolved.
Show resolved Hide resolved
() => hiddenFieldsErrorsToGlobal(errors?._global),
[errors?._global, hiddenFieldsErrorsToGlobal],
)

const buttonLabel =
transition.state === 'submitting' ? pendingButtonLabel : rawButtonLabel

const [disabled, setDisabled] = React.useState(false)

const [globalErrorsState, setGlobalErrorsState] = React.useState<
string[] | undefined
>(globalErrors)

const customChildren = mapChildren(
childrenFn?.({
Field,
Expand Down Expand Up @@ -403,9 +426,9 @@ function createForm({
autoFocus,
})
} else if (child.type === Errors) {
if (!child.props.children && !globalErrors?.length) return null
if (!child.props.children && !globalErrorsState?.length) return null

if (child.props.children || !globalErrors?.length) {
if (child.props.children || !globalErrorsState?.length) {
return React.cloneElement(child, {
role: 'alert',
...child.props,
Expand All @@ -414,7 +437,7 @@ function createForm({

return React.cloneElement(child, {
role: 'alert',
children: globalErrors.map((error) => (
children: globalErrorsState.map((error) => (
<Error key={error}>{error}</Error>
)),
...child.props,
Expand All @@ -437,9 +460,9 @@ function createForm({
{Object.keys(schemaShape)
.map(makeField)
.map((field) => renderField({ Field, ...field }))}
{globalErrors?.length && (
{globalErrorsState?.length && (
<Errors role="alert">
{globalErrors.map((error) => (
{globalErrorsState.map((error) => (
<Error key={error}>{error}</Error>
))}
</Errors>
Expand Down Expand Up @@ -486,8 +509,16 @@ function createForm({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [errorsProp, unparsedActionData])

React.useEffect(() => {
setGlobalErrorsState(globalErrors)
}, [globalErrors])

React.useEffect(() => {
onTransition && onTransition(form)

if (transition.state === 'submitting') {
setGlobalErrorsState(undefined)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [transition.state])

Expand Down