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:
parent
15a14bc174
commit
2f65134826
6 changed files with 204 additions and 53 deletions
|
@ -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" />
|
||||||
|
|
|
@ -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,21 +23,66 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const Form = props => (
|
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 {...props}>
|
||||||
<FormComponent.Field label={"Username"}/>
|
<FormComponent.Field label={"Username"} {...username}/>
|
||||||
<FormComponent.Field label={"Password"} type={"password"}/>
|
<FormComponent.Field label={"Password"} type={"password"} {...password}/>
|
||||||
<FormComponent.Row>
|
<FormComponent.Row>
|
||||||
<Parenthesis>Enter the details of your characters below.</Parenthesis>
|
<Parenthesis>Enter the details of your characters below.</Parenthesis>
|
||||||
</FormComponent.Row>
|
</FormComponent.Row>
|
||||||
<FormComponent.Field label={"Name"}/>
|
<FormComponent.Field label={"Name"} {...name}/>
|
||||||
<FormComponent.Area label={"Backstory"}/>
|
<FormComponent.Area label={"Backstory"} {...backstory}/>
|
||||||
<FormComponent.Select label={"Gender"}>
|
<FormComponent.Select label={"Gender"} {...gender}>
|
||||||
<FormComponent.Select.Option value={"Male"}/>
|
<FormComponent.Select.Option value={"Male"}/>
|
||||||
<FormComponent.Select.Option value={"Female"}/>
|
<FormComponent.Select.Option value={"Female"}/>
|
||||||
<FormComponent.Select.Option value={"Non-binary"}/>
|
<FormComponent.Select.Option value={"Non-binary"}/>
|
||||||
</FormComponent.Select>
|
</FormComponent.Select>
|
||||||
<FormComponent.Field label={"Level"} type={"number"} min={1} max={20}/>
|
<FormComponent.Field label={"Level"} type={"number"} min={1} max={20} {...level}/>
|
||||||
<FormComponent.Radios label={"Alignment"} row={true} options={[
|
<FormComponent.Radios label={"Alignment"} row={true} options={[
|
||||||
"Lawful good",
|
"Lawful good",
|
||||||
"Lawful neutral",
|
"Lawful neutral",
|
||||||
|
@ -48,7 +94,7 @@ export const Form = props => (
|
||||||
"Chaotic neutral",
|
"Chaotic neutral",
|
||||||
"Chaotic evil",
|
"Chaotic evil",
|
||||||
"Other",
|
"Other",
|
||||||
]}/>
|
]} {...alignment}/>
|
||||||
<FormComponent.Checkboxes label={"Classes"} row={false} options={[
|
<FormComponent.Checkboxes label={"Classes"} row={false} options={[
|
||||||
"Artificer",
|
"Artificer",
|
||||||
"Barbarian",
|
"Barbarian",
|
||||||
|
@ -63,7 +109,7 @@ export const Form = props => (
|
||||||
"Sorcerer",
|
"Sorcerer",
|
||||||
"Warlock",
|
"Warlock",
|
||||||
"Wizard",
|
"Wizard",
|
||||||
]}/>
|
]} {...classes}/>
|
||||||
<FormComponent.Row>
|
<FormComponent.Row>
|
||||||
<FormComponent.Button>Throw fireball</FormComponent.Button>
|
<FormComponent.Button>Throw fireball</FormComponent.Button>
|
||||||
<FormComponent.Button>Shoot a magic missile</FormComponent.Button>
|
<FormComponent.Button>Shoot a magic missile</FormComponent.Button>
|
||||||
|
@ -71,4 +117,5 @@ export const Form = props => (
|
||||||
</FormComponent.Row>
|
</FormComponent.Row>
|
||||||
</FormComponent>
|
</FormComponent>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
Form.args = {}
|
Form.args = {}
|
||||||
|
|
|
@ -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
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 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