mirror of
https://github.com/Steffo99/bluelib.git
synced 2024-12-22 03:24:20 +00:00
✨ BREAKING: Add useValidatedState
hook
This commit is contained in:
parent
15a14bc174
commit
2f65134826
6 changed files with 204 additions and 53 deletions
|
@ -72,6 +72,7 @@
|
|||
<option name="processLiterals" value="true" />
|
||||
<option name="processComments" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="TrivialIfJS" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="UnnecessaryLocalVariableJS" enabled="false" level="WARNING" enabled_by_default="false">
|
||||
<option name="m_ignoreImmediatelyReturnedVariables" value="false" />
|
||||
<option name="m_ignoreAnnotatedVariables" value="false" />
|
||||
|
|
|
@ -12,6 +12,7 @@ import { FormCheckboxGroup } from "./FormCheckboxGroup"
|
|||
import { FormRow } from "./FormRow"
|
||||
import { Button } from "../inputs/Button"
|
||||
import { Parenthesis } from "../panels/Parenthesis"
|
||||
import { useFormState } from "../../hooks/useValidatedState"
|
||||
|
||||
|
||||
export default {
|
||||
|
@ -22,53 +23,99 @@ export default {
|
|||
}
|
||||
|
||||
|
||||
export const Form = props => (
|
||||
<FormComponent {...props}>
|
||||
<FormComponent.Field label={"Username"}/>
|
||||
<FormComponent.Field label={"Password"} type={"password"}/>
|
||||
<FormComponent.Row>
|
||||
<Parenthesis>Enter the details of your characters below.</Parenthesis>
|
||||
</FormComponent.Row>
|
||||
<FormComponent.Field label={"Name"}/>
|
||||
<FormComponent.Area label={"Backstory"}/>
|
||||
<FormComponent.Select label={"Gender"}>
|
||||
<FormComponent.Select.Option value={"Male"}/>
|
||||
<FormComponent.Select.Option value={"Female"}/>
|
||||
<FormComponent.Select.Option value={"Non-binary"}/>
|
||||
</FormComponent.Select>
|
||||
<FormComponent.Field label={"Level"} type={"number"} min={1} max={20}/>
|
||||
<FormComponent.Radios label={"Alignment"} row={true} options={[
|
||||
"Lawful good",
|
||||
"Lawful neutral",
|
||||
"Lawful evil",
|
||||
"Neutral good",
|
||||
"Neutral",
|
||||
"Neutral evil",
|
||||
"Chaotic good",
|
||||
"Chaotic neutral",
|
||||
"Chaotic evil",
|
||||
"Other",
|
||||
]}/>
|
||||
<FormComponent.Checkboxes label={"Classes"} row={false} options={[
|
||||
"Artificer",
|
||||
"Barbarian",
|
||||
"Bard",
|
||||
"Cleric",
|
||||
"Druid",
|
||||
"Fighter",
|
||||
"Monk",
|
||||
"Paladin",
|
||||
"Ranger",
|
||||
"Rogue",
|
||||
"Sorcerer",
|
||||
"Warlock",
|
||||
"Wizard",
|
||||
]}/>
|
||||
<FormComponent.Row>
|
||||
<FormComponent.Button>Throw fireball</FormComponent.Button>
|
||||
<FormComponent.Button>Shoot a magic missile</FormComponent.Button>
|
||||
<FormComponent.Button>Save character</FormComponent.Button>
|
||||
</FormComponent.Row>
|
||||
</FormComponent>
|
||||
)
|
||||
export const Form = props => {
|
||||
const username = useFormState("", val => {
|
||||
if(val === "") return undefined
|
||||
return true
|
||||
})
|
||||
|
||||
const password = useFormState("", val => {
|
||||
if(val === "") return undefined
|
||||
if(val.length < 8) return false
|
||||
return true
|
||||
})
|
||||
|
||||
const name = useFormState("", val => {
|
||||
if(val === "") return undefined
|
||||
return true
|
||||
})
|
||||
|
||||
const backstory = useFormState("", val => {
|
||||
if(val === "") return undefined
|
||||
if(val.split("\n").length < 3) return false
|
||||
return true
|
||||
})
|
||||
|
||||
const gender = useFormState(null, val => {
|
||||
if(val === null) return undefined
|
||||
return true
|
||||
})
|
||||
|
||||
const level = useFormState(1, val => {
|
||||
if(val < 1) return false
|
||||
if(val > 20) return false
|
||||
return true
|
||||
})
|
||||
|
||||
const alignment = useFormState([], val => {
|
||||
if(val.length === 0) return undefined
|
||||
return true
|
||||
})
|
||||
|
||||
const classes = useFormState([], val => {
|
||||
if(val.length === 0) return undefined
|
||||
if(val.length > level.value) return false
|
||||
return true
|
||||
})
|
||||
|
||||
return (
|
||||
<FormComponent {...props}>
|
||||
<FormComponent.Field label={"Username"} {...username}/>
|
||||
<FormComponent.Field label={"Password"} type={"password"} {...password}/>
|
||||
<FormComponent.Row>
|
||||
<Parenthesis>Enter the details of your characters below.</Parenthesis>
|
||||
</FormComponent.Row>
|
||||
<FormComponent.Field label={"Name"} {...name}/>
|
||||
<FormComponent.Area label={"Backstory"} {...backstory}/>
|
||||
<FormComponent.Select label={"Gender"} {...gender}>
|
||||
<FormComponent.Select.Option value={"Male"}/>
|
||||
<FormComponent.Select.Option value={"Female"}/>
|
||||
<FormComponent.Select.Option value={"Non-binary"}/>
|
||||
</FormComponent.Select>
|
||||
<FormComponent.Field label={"Level"} type={"number"} min={1} max={20} {...level}/>
|
||||
<FormComponent.Radios label={"Alignment"} row={true} options={[
|
||||
"Lawful good",
|
||||
"Lawful neutral",
|
||||
"Lawful evil",
|
||||
"Neutral good",
|
||||
"Neutral",
|
||||
"Neutral evil",
|
||||
"Chaotic good",
|
||||
"Chaotic neutral",
|
||||
"Chaotic evil",
|
||||
"Other",
|
||||
]} {...alignment}/>
|
||||
<FormComponent.Checkboxes label={"Classes"} row={false} options={[
|
||||
"Artificer",
|
||||
"Barbarian",
|
||||
"Bard",
|
||||
"Cleric",
|
||||
"Druid",
|
||||
"Fighter",
|
||||
"Monk",
|
||||
"Paladin",
|
||||
"Ranger",
|
||||
"Rogue",
|
||||
"Sorcerer",
|
||||
"Warlock",
|
||||
"Wizard",
|
||||
]} {...classes}/>
|
||||
<FormComponent.Row>
|
||||
<FormComponent.Button>Throw fireball</FormComponent.Button>
|
||||
<FormComponent.Button>Shoot a magic missile</FormComponent.Button>
|
||||
<FormComponent.Button>Save character</FormComponent.Button>
|
||||
</FormComponent.Row>
|
||||
</FormComponent>
|
||||
)
|
||||
}
|
||||
Form.args = {}
|
||||
|
|
|
@ -23,13 +23,13 @@ export function FormPair({id, label, input, validity, bluelibClassNames, customC
|
|||
}
|
||||
|
||||
let validityClass = ""
|
||||
if(validity === "running") {
|
||||
if(validity === null) {
|
||||
validityClass = "color-yellow"
|
||||
}
|
||||
else if(validity === "ok") {
|
||||
else if(validity === true) {
|
||||
validityClass = "color-lime"
|
||||
}
|
||||
else if(validity === "error") {
|
||||
else if(validity === false) {
|
||||
validityClass = "color-red"
|
||||
}
|
||||
|
||||
|
|
19
src/hooks/usePromise.js
Normal file
19
src/hooks/usePromise.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import * as React from "react"
|
||||
import {useCallback, useMemo} from "react"
|
||||
|
||||
|
||||
/**
|
||||
* Hook similar to {@link useCallback} that converts a syncronous function into an asyncronous one.
|
||||
*
|
||||
* @todo Improve this docstring.
|
||||
* @todo I have no idea of how to write this in TypeScript.
|
||||
* @param func The function to convert.
|
||||
*/
|
||||
export function usePromise(func) {
|
||||
return useMemo(
|
||||
() => {
|
||||
return async (...args) => await func(...args)
|
||||
},
|
||||
[func]
|
||||
)
|
||||
}
|
64
src/hooks/useValidatedState.ts
Normal file
64
src/hooks/useValidatedState.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import * as React from "react"
|
||||
import {useState} from "react"
|
||||
import {Validator, Validity} from "../types"
|
||||
import {usePromise} from "./usePromise";
|
||||
|
||||
|
||||
/**
|
||||
* The return type of the {@link useFormState} hook.
|
||||
*/
|
||||
export type FormState<T> = {
|
||||
value: T,
|
||||
onSimpleChange: React.Dispatch<React.SetStateAction<T>>,
|
||||
validity: Validity,
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Hook similar to {@link useState} for handling the validation of form fields.
|
||||
*
|
||||
* @param def - Default value for the state.
|
||||
* @param validator - The {@link Validator} to apply.
|
||||
*/
|
||||
export function useFormState<T>(def: T, validator: Validator<T>): FormState<T> {
|
||||
const [value, setValue]
|
||||
= React.useState(def)
|
||||
|
||||
const [validity, setValidity]
|
||||
= React.useState<Validity>(null)
|
||||
|
||||
const trueValidator
|
||||
= usePromise(validator)
|
||||
|
||||
const validate
|
||||
= React.useCallback(
|
||||
async (value: T, abort: AbortSignal) => {
|
||||
setValidity(null)
|
||||
let result: Validity
|
||||
try {
|
||||
result = await trueValidator(value, abort)
|
||||
}
|
||||
catch(_) {
|
||||
result = false
|
||||
}
|
||||
if(!abort.aborted) {
|
||||
setValidity(result)
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
const abort = new AbortController()
|
||||
validate(value, abort.signal)
|
||||
|
||||
return () => {
|
||||
abort.abort()
|
||||
}
|
||||
},
|
||||
[validate, value]
|
||||
)
|
||||
|
||||
return {value, onSimpleChange: setValue, validity}
|
||||
}
|
22
src/types.ts
22
src/types.ts
|
@ -21,4 +21,24 @@ export interface BluelibHTMLProps<Element extends HTMLElement> extends BluelibPr
|
|||
|
||||
|
||||
export type InputValue = readonly string[] | string | number | undefined
|
||||
export type Validity = null | "running" | "ok" | "error"
|
||||
|
||||
|
||||
/**
|
||||
* An optionally async function that checks if a value is acceptable for a certain form field or not.
|
||||
*
|
||||
* See {@link Validity} to see the acceptable return values of the function.
|
||||
*
|
||||
* An {@link AbortSignal} is passed to the function to allow it to handle teardowns, allowing it to stop HTTP requests if the previous value is torn down.
|
||||
*/
|
||||
export type Validator<T> = (value: T, abort: AbortSignal) => Promise<Validity> | Validity
|
||||
|
||||
/**
|
||||
* The possible return values of a {@link Validator}.
|
||||
*
|
||||
* - `true` means that the value is acceptable for the field;
|
||||
* - `false` means that the value contains an error and should not be accepted;
|
||||
* - `undefined` means that the value has no meaning and wasn't checked (such as in the case of an empty form field);
|
||||
* - `null` means that the value is in progress of being checked.
|
||||
*/
|
||||
export type Validity = boolean | null | undefined
|
||||
|
||||
|
|
Loading…
Reference in a new issue