Skip to content

Commit

Permalink
minor tweaks and doc updates. (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
nathanb authored Feb 16, 2024
1 parent 6d2bdc4 commit 0496b8c
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 24 deletions.
62 changes: 52 additions & 10 deletions demo/src/content/invalid-feedback.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ This examples puts this to use and shows how you can customize the error messagi
<div class="not-prose">

```jsx
// NOTE: leaving customError excluded so they report directly as-is.
const mapping: ErrorMapping = {
badInput: 'Invalid',
customError: 'Invalid',
patternMismatch: 'Invalid',
rangeOverflow: 'Too high',
rangeUnderflow: 'Too low',
Expand All @@ -24,18 +24,58 @@ const mapping: ErrorMapping = {
valueMissing: 'Required'
}

export const Field = () => {
/**
* In this example, we render a text input field and apply custom onChange validation rules to treat it like a number.
*/
export const Field: FC<{name: string}> = ({ name }) => {
const { checkFieldError, setFieldError } = useFieldManager()
const fieldError = checkFieldError(name)

// this change event invokes AFTER the field manager change handler; so the state should be updated with a value along with any validity state from the base input
const handleChange = (e) => {
// NOTE: dont' reference state here, it could be out of sync w/ the field manager
if (!e.target.validity.valid) return // already failed; likely required or pattern mismatch
// parsed
// text value treated like a number; another way of handling step/min/max
const value = +e.target.value
if (isNaN(value)) return setFieldError(name, 'Invalid') // numeric: handled as custom error
if (value < 2) return setFieldError(name, 'Too low') // range low: handled as custom error
if (value > 99) return setFieldError(name, 'Too high') // range high: handled as custom error
if (value % 1 !== 0) return setFieldError(name, 'Invalid') // step rules: handled as custom error
}

return (
<>
<div className="form-control w-1/2">
<div className="indicator w-full">
<InputField name={name} required className={`input input-bordered w-full ${fieldError ? 'input-error' : ''}`} type="text" pattern="^[\d\.]+$" onChange={handleChange} />
<InvalidFeedbackForField name={name} className="indicator-item badge badge-error" />
</div>
<label className="label">
<span className="label-text-alt">Text input with onChange custom valdiation; Required value:<code>number &gt; 1 and &lt; 100, step: 1</code></span>
</label>
</div>
<p className="mt-0">This nuanced approach above shows a more responsive interaction. Try: <code>submit</code>, then enter: <code>.1</code> <em>(shows Too low)</em>, <code>backspace</code> <em>(will show invalid)</em>, <code>backspace</code> <em>(shows required)</em></p>
<p className="m-0"><code>&lt;input type="number"&gt;</code> (below) does not do this. It only triggers onChange with number value differences and remains "Invalid" after clearing the text when it should show <em>Required</em>.</p>
</>
)
}

/**
* This example shows a simple input type="number" with min, max, and step validation
*/
export const Field2: FC<{name: string}> = ({ name }) => {
const { checkFieldError } = useFieldManager()
const fieldError = checkFieldError('field')
const fieldError = checkFieldError(name)

return (
<div className="form-control">
<div className="indicator">
<InputField name="field" required className={`input input-bordered ${fieldError ? 'input-error' : ''}`} pattern="^[a-zA-Z]+$" minLength={2} />
<InvalidFeedbackForField name="field" className="indicator-item badge badge-error" />
<div className="form-control w-1/2">
<div className="indicator w-full">
<InputField name={name} required className={`input input-bordered w-full ${fieldError ? 'input-error' : ''}`} type="number" step="1" min="2" max="99" />
<InvalidFeedbackForField name={name} className="indicator-item badge badge-error" />
</div>
<label className="label">
<span className="label-text-alt">Required pattern:<code>^[a-zA-Z]+$</code></span>
<span className="label-text-alt">Number input using min, max, and step validation. Required value:<code>number &gt; 1 and &lt; 100, step: 1</code></span>
</label>
</div>
)
Expand All @@ -54,11 +94,12 @@ export const InvalidFeedbackDemo = () => {
setSuccess(true)
}
return (
<FieldManager fields={{ field: '' }} onValidSubmit={handleValidSubmit} onSubmit={handleSubmit} onReset={() => setSuccess(false)} errorMapping={mapping}>
<FieldManager fields={{ field: '', field2: '' }} onValidSubmit={handleValidSubmit} onSubmit={handleSubmit} onReset={() => setSuccess(false)} errorMapping={mapping}>
<fieldset className="border p-5">
<legend>Invalid Feedback</legend>
<div className="flex flex-col gap-5">
<Field />
<Field name="field" />
<Field2 name="field2" />
<div className="flex flex-row gap-4">
<ResetButton />
<button type="submit" className={`btn ${success ? 'btn-success' : ''}`}>Submit</button>
Expand All @@ -69,6 +110,7 @@ export const InvalidFeedbackDemo = () => {
</FieldManager>
)
}

```

</div>
Expand Down
65 changes: 53 additions & 12 deletions demo/src/samples/InvalidFeedback.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { FieldManager, InputField, InvalidFeedbackForField, useFieldManager, ErrorMapping } from '@iwsio/forms'
import { useState } from 'react'
import { FieldManager, InputField, InvalidFeedbackForField, useFieldManager, ErrorMapping, emptyValidity } from '@iwsio/forms'
import { FC, useState } from 'react'

// NOTE: leaving customError excluded so they report directly as-is.
const mapping: ErrorMapping = {
badInput: 'Invalid',
customError: 'Invalid',
patternMismatch: 'Invalid',
rangeOverflow: 'Too high',
rangeUnderflow: 'Too low',
Expand All @@ -14,18 +14,58 @@ const mapping: ErrorMapping = {
valueMissing: 'Required'
}

export const Field = () => {
/**
* In this example, we render a text input field and apply custom onChange validation rules to treat it like a number.
*/
export const Field: FC<{name: string}> = ({ name }) => {
const { checkFieldError, setFieldError } = useFieldManager()
const fieldError = checkFieldError(name)

// this change event invokes AFTER the field manager change handler; so the state should be updated with a value along with any validity state from the base input
const handleChange = (e) => {
// NOTE: dont' reference state here, it could be out of sync w/ the field manager
if (!e.target.validity.valid) return // already failed; likely required or pattern mismatch
// parsed
// text value treated like a number; another way of handling step/min/max
const value = +e.target.value
if (isNaN(value)) return setFieldError(name, 'Invalid') // numeric: handled as custom error
if (value < 2) return setFieldError(name, 'Too low') // range low: handled as custom error
if (value > 99) return setFieldError(name, 'Too high') // range high: handled as custom error
if (value % 1 !== 0) return setFieldError(name, 'Invalid') // step rules: handled as custom error
}

return (
<>
<div className="form-control w-1/2">
<div className="indicator w-full">
<InputField name={name} required className={`input input-bordered w-full ${fieldError ? 'input-error' : ''}`} type="text" pattern="^[\d\.]+$" onChange={handleChange} />
<InvalidFeedbackForField name={name} className="indicator-item badge badge-error" />
</div>
<label className="label">
<span className="label-text-alt">Text input with onChange custom valdiation; Required value:<code>number &gt; 1 and &lt; 100, step: 1</code></span>
</label>
</div>
<p className="mt-0">This nuanced approach above shows a more responsive interaction. Try: <code>submit</code>, then enter: <code>.1</code> <em>(shows Too low)</em>, <code>backspace</code> <em>(will show invalid)</em>, <code>backspace</code> <em>(shows required)</em></p>
<p className="m-0"><code>&lt;input type="number"&gt;</code> (below) does not do this. It only triggers onChange with number value differences and remains "Invalid" after clearing the text.</p>
</>
)
}

/**
* This example shows a simple input type="number" with min, max, and step validation
*/
export const Field2: FC<{name: string}> = ({ name }) => {
const { checkFieldError } = useFieldManager()
const fieldError = checkFieldError('field')
const fieldError = checkFieldError(name)

return (
<div className="form-control">
<div className="indicator">
<InputField name="field" required className={`input input-bordered ${fieldError ? 'input-error' : ''}`} pattern="^[a-zA-Z]+$" minLength={2} />
<InvalidFeedbackForField name="field" className="indicator-item badge badge-error" />
<div className="form-control w-1/2">
<div className="indicator w-full">
<InputField name={name} required className={`input input-bordered w-full ${fieldError ? 'input-error' : ''}`} type="number" step="1" min="2" max="99" />
<InvalidFeedbackForField name={name} className="indicator-item badge badge-error" />
</div>
<label className="label">
<span className="label-text-alt">Required pattern:<code>^[a-zA-Z]+$</code></span>
<span className="label-text-alt">Number input using min, max, and step validation. Required value:<code>number &gt; 1 and &lt; 100, step: 1</code></span>
</label>
</div>
)
Expand All @@ -44,11 +84,12 @@ export const InvalidFeedbackDemo = () => {
setSuccess(true)
}
return (
<FieldManager fields={{ field: '' }} onValidSubmit={handleValidSubmit} onSubmit={handleSubmit} onReset={() => setSuccess(false)} errorMapping={mapping}>
<FieldManager fields={{ field: '', field2: '' }} onValidSubmit={handleValidSubmit} onSubmit={handleSubmit} onReset={() => setSuccess(false)} errorMapping={mapping}>
<fieldset className="border p-5">
<legend>Invalid Feedback</legend>
<div className="flex flex-col gap-5">
<Field />
<Field name="field" />
<Field2 name="field2" />
<div className="flex flex-row gap-4">
<ResetButton />
<button type="submit" className={`btn ${success ? 'btn-success' : ''}`}>Submit</button>
Expand Down
1 change: 1 addition & 0 deletions forms/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './useFieldManager'
export * from './useForwardRef'
export * from './ValidatedForm'
export * from './useErrorMapping'
export * from './validityState'
5 changes: 3 additions & 2 deletions forms/src/useFieldState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { omitBy } from './omitBy'
import { defaults } from './defaults'
import { FieldError, FieldValues, UseFieldStateResult } from './types'
import { ErrorMapping, useErrorMapping } from './useErrorMapping'
import { emptyValidity } from './validityState'

/**
* Manages field state via change handler, values and error state.
Expand Down Expand Up @@ -49,7 +50,7 @@ export function useFieldState(fields: FieldValues, defaultValues?: FieldValues,
const hasMessage = message != null && message.trim().length > 0

if (hasMessage) {
_validity = validity ?? { valid: false, badInput: false, customError: true, patternMismatch: false, rangeOverflow: false, rangeUnderflow: false, stepMismatch: false, tooLong: false, tooShort: false, typeMismatch: false, valueMissing: false }
_validity = validity ?? { ...emptyValidity, customError: true }
}
setFieldErrors((old) => ({ ...old, [key]: !hasMessage ? undefined : { message, validity: _validity } }))
}, [errorMapping])
Expand All @@ -60,7 +61,7 @@ export function useFieldState(fields: FieldValues, defaultValues?: FieldValues,

if (fieldError == null) return undefined
return mapError(fieldError.validity, fieldError.message)
}, [fieldErrors, reportValidation, mapError])
}, [fieldErrors, reportValidation, mapError, fieldValues])

const reset = useCallback(() => {
setFieldErrors((_old) => ({}))
Expand Down
4 changes: 4 additions & 0 deletions forms/src/validityState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/**
* Baseline validity state with all props false.
*/
export const emptyValidity: ValidityState = { valid: false, badInput: false, customError: false, patternMismatch: false, rangeOverflow: false, rangeUnderflow: false, stepMismatch: false, tooLong: false, tooShort: false, typeMismatch: false, valueMissing: false }

0 comments on commit 0496b8c

Please sign in to comment.