1
Fork 0
mirror of https://github.com/Steffo99/bluelib.git synced 2024-12-22 11:34:21 +00:00

BREAKING: Add useValidatedState hook

This commit is contained in:
Steffo 2021-09-14 22:58:49 +02:00
parent 15a14bc174
commit 2f65134826
Signed by: steffo
GPG key ID: 6965406171929D01
6 changed files with 204 additions and 53 deletions

View file

@ -72,6 +72,7 @@
<option name="processLiterals" value="true" /> <option name="processLiterals" value="true" />
<option name="processComments" value="true" /> <option name="processComments" value="true" />
</inspection_tool> </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"> <inspection_tool class="UnnecessaryLocalVariableJS" enabled="false" level="WARNING" enabled_by_default="false">
<option name="m_ignoreImmediatelyReturnedVariables" value="false" /> <option name="m_ignoreImmediatelyReturnedVariables" value="false" />
<option name="m_ignoreAnnotatedVariables" value="false" /> <option name="m_ignoreAnnotatedVariables" value="false" />

View file

@ -12,6 +12,7 @@ import { FormCheckboxGroup } from "./FormCheckboxGroup"
import { FormRow } from "./FormRow" import { FormRow } from "./FormRow"
import { Button } from "../inputs/Button" import { Button } from "../inputs/Button"
import { Parenthesis } from "../panels/Parenthesis" import { Parenthesis } from "../panels/Parenthesis"
import { useFormState } from "../../hooks/useValidatedState"
export default { export default {
@ -22,53 +23,99 @@ export default {
} }
export const Form = props => ( export const Form = props => {
<FormComponent {...props}> const username = useFormState("", val => {
<FormComponent.Field label={"Username"}/> if(val === "") return undefined
<FormComponent.Field label={"Password"} type={"password"}/> return true
<FormComponent.Row> })
<Parenthesis>Enter the details of your characters below.</Parenthesis>
</FormComponent.Row> const password = useFormState("", val => {
<FormComponent.Field label={"Name"}/> if(val === "") return undefined
<FormComponent.Area label={"Backstory"}/> if(val.length < 8) return false
<FormComponent.Select label={"Gender"}> return true
<FormComponent.Select.Option value={"Male"}/> })
<FormComponent.Select.Option value={"Female"}/>
<FormComponent.Select.Option value={"Non-binary"}/> const name = useFormState("", val => {
</FormComponent.Select> if(val === "") return undefined
<FormComponent.Field label={"Level"} type={"number"} min={1} max={20}/> return true
<FormComponent.Radios label={"Alignment"} row={true} options={[ })
"Lawful good",
"Lawful neutral", const backstory = useFormState("", val => {
"Lawful evil", if(val === "") return undefined
"Neutral good", if(val.split("\n").length < 3) return false
"Neutral", return true
"Neutral evil", })
"Chaotic good",
"Chaotic neutral", const gender = useFormState(null, val => {
"Chaotic evil", if(val === null) return undefined
"Other", return true
]}/> })
<FormComponent.Checkboxes label={"Classes"} row={false} options={[
"Artificer", const level = useFormState(1, val => {
"Barbarian", if(val < 1) return false
"Bard", if(val > 20) return false
"Cleric", return true
"Druid", })
"Fighter",
"Monk", const alignment = useFormState([], val => {
"Paladin", if(val.length === 0) return undefined
"Ranger", return true
"Rogue", })
"Sorcerer",
"Warlock", const classes = useFormState([], val => {
"Wizard", if(val.length === 0) return undefined
]}/> if(val.length > level.value) return false
<FormComponent.Row> return true
<FormComponent.Button>Throw fireball</FormComponent.Button> })
<FormComponent.Button>Shoot a magic missile</FormComponent.Button>
<FormComponent.Button>Save character</FormComponent.Button> return (
</FormComponent.Row> <FormComponent {...props}>
</FormComponent> <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 = {} Form.args = {}

View file

@ -23,13 +23,13 @@ export function FormPair({id, label, input, validity, bluelibClassNames, customC
} }
let validityClass = "" let validityClass = ""
if(validity === "running") { if(validity === null) {
validityClass = "color-yellow" validityClass = "color-yellow"
} }
else if(validity === "ok") { else if(validity === true) {
validityClass = "color-lime" validityClass = "color-lime"
} }
else if(validity === "error") { else if(validity === false) {
validityClass = "color-red" validityClass = "color-red"
} }

19
src/hooks/usePromise.js Normal file
View 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]
)
}

View 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}
}

View file

@ -21,4 +21,24 @@ export interface BluelibHTMLProps<Element extends HTMLElement> extends BluelibPr
export type InputValue = readonly string[] | string | number | undefined 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