1
Fork 0
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:
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="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" />

View file

@ -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 = {}

View file

@ -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
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 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