diff options
Diffstat (limited to 'packages')
-rw-r--r-- | packages/web-util/src/forms/FormProvider.tsx | 45 | ||||
-rw-r--r-- | packages/web-util/src/forms/InputAbsoluteTime.tsx | 55 | ||||
-rw-r--r-- | packages/web-util/src/forms/InputAmount.tsx | 12 | ||||
-rw-r--r-- | packages/web-util/src/forms/InputArray.tsx | 29 | ||||
-rw-r--r-- | packages/web-util/src/forms/InputChoiceHorizontal.tsx | 22 | ||||
-rw-r--r-- | packages/web-util/src/forms/InputChoiceStacked.tsx | 14 | ||||
-rw-r--r-- | packages/web-util/src/forms/InputFile.tsx | 21 | ||||
-rw-r--r-- | packages/web-util/src/forms/InputLine.tsx | 30 | ||||
-rw-r--r-- | packages/web-util/src/forms/InputSelectMultiple.tsx | 174 | ||||
-rw-r--r-- | packages/web-util/src/forms/InputSelectOne.tsx | 27 | ||||
-rw-r--r-- | packages/web-util/src/forms/InputToggle.tsx | 50 | ||||
-rw-r--r-- | packages/web-util/src/forms/forms.ts | 34 | ||||
-rw-r--r-- | packages/web-util/src/forms/useField.ts | 30 |
13 files changed, 321 insertions, 222 deletions
diff --git a/packages/web-util/src/forms/FormProvider.tsx b/packages/web-util/src/forms/FormProvider.tsx index f4616525b..de19a6315 100644 --- a/packages/web-util/src/forms/FormProvider.tsx +++ b/packages/web-util/src/forms/FormProvider.tsx @@ -4,10 +4,7 @@ import { TranslatedString, } from "@gnu-taler/taler-util"; import { ComponentChildren, VNode, createContext, h } from "preact"; -import { - MutableRef, - useState -} from "preact/hooks"; +import { MutableRef, useState } from "preact/hooks"; export interface FormType<T extends object> { value: MutableRef<Partial<T>>; @@ -17,8 +14,7 @@ export interface FormType<T extends object> { computeFormState?: (v: Partial<T>) => FormState<T>; } -//@ts-ignore -export const FormContext = createContext<FormType<any>>({}); +export const FormContext = createContext<FormType<any>| undefined>(undefined); /** * Map of {[field]:FieldUIOptions} @@ -26,21 +22,21 @@ export const FormContext = createContext<FormType<any>>({}); * - any native (string, number, etc...) * - absoluteTime * - amountJson - * - * except for: + * + * except for: * - object => recurse into * - array => behavior result and element field */ export type FormState<T extends object | undefined> = { [field in keyof T]?: T[field] extends AbsoluteTime - ? FieldUIOptions - : T[field] extends AmountJson - ? FieldUIOptions - : T[field] extends Array<infer P extends object> - ? InputArrayFieldState<P> - : T[field] extends (object | undefined) - ? FormState<T[field]> - : FieldUIOptions; + ? FieldUIOptions + : T[field] extends AmountJson + ? FieldUIOptions + : T[field] extends Array<infer P extends object> + ? InputArrayFieldState<P> + : T[field] extends object | undefined + ? FormState<T[field]> + : FieldUIOptions; }; /** @@ -63,13 +59,13 @@ export type FieldUIOptions = { /* show a mark as required*/ required?: boolean; -} +}; /** * properties only to be defined on design time */ -export interface UIFormProps<T extends object, K extends keyof T> extends FieldUIOptions { - +export interface UIFormProps<T extends object, K extends keyof T> + extends FieldUIOptions { // property name of the object name: K; @@ -80,8 +76,16 @@ export interface UIFormProps<T extends object, K extends keyof T> extends FieldU // converter to string and back converter?: StringConverter<T[K]>; + + handler?: UIField; } +export type UIField = { + value: string | undefined; + onChange: (s: string) => void; + state: FieldUIOptions; +}; + export interface IconAddon { type: "icon"; icon: VNode; @@ -109,7 +113,7 @@ export interface InputArrayFieldState<P extends object> extends FieldUIOptions { export type FormProviderProps<T extends object> = Omit<FormType<T>, "value"> & { onSubmit?: (v: Partial<T>, s: FormState<T> | undefined) => void; children?: ComponentChildren; -} +}; export function FormProvider<T extends object>({ children, @@ -119,7 +123,6 @@ export function FormProvider<T extends object>({ computeFormState, readOnly, }: FormProviderProps<T>): VNode { - const [state, setState] = useState<Partial<T>>(initial ?? {}); const value = { current: state }; const onUpdate = (v: typeof state) => { diff --git a/packages/web-util/src/forms/InputAbsoluteTime.tsx b/packages/web-util/src/forms/InputAbsoluteTime.tsx index ee18e5592..772ab1813 100644 --- a/packages/web-util/src/forms/InputAbsoluteTime.tsx +++ b/packages/web-util/src/forms/InputAbsoluteTime.tsx @@ -1,35 +1,50 @@ import { AbsoluteTime } from "@gnu-taler/taler-util"; -import { InputLine } from "./InputLine.js"; -import { Fragment, VNode, h } from "preact"; import { format, parse } from "date-fns"; -import { Dialog } from "./Dialog.js"; -import { Calendar } from "./Calendar.js"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { useField } from "./useField.js"; +import { Calendar } from "./Calendar.js"; +import { Dialog } from "./Dialog.js"; import { UIFormProps } from "./FormProvider.js"; -import { TimePicker } from "./TimePicker.js"; +import { InputLine } from "./InputLine.js"; +import { useField } from "./useField.js"; +import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; export function InputAbsoluteTime<T extends object, K extends keyof T>( props: { pattern?: string } & UIFormProps<T, K>, ): VNode { const pattern = props.pattern ?? "dd/MM/yyyy"; - const [open, setOpen] = useState(false) - const { value, onChange } = useField<T, K>(props.name); + const [open, setOpen] = useState(false); + + //FIXME: remove deprecated + const fieldCtx = useField<T, K>(props.name); + const { value, onChange } = + props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.name); return ( <Fragment> - <InputLine<T, K> type="text" after={{ type: "button", onClick: () => { - setOpen(true) + setOpen(true); }, // icon: <CalendarIcon class="h-6 w-6" />, children: ( - <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> - <path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" /> - </svg>) + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-6 h-6" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" + /> + </svg> + ), }} converter={{ //@ts-ignore @@ -53,15 +68,17 @@ export function InputAbsoluteTime<T extends object, K extends keyof T>( }} {...props} /> - {open && + {open && ( <Dialog onClose={() => setOpen(false)}> - <Calendar value={value as AbsoluteTime ?? AbsoluteTime.now()} + <Calendar + value={(value as AbsoluteTime) ?? AbsoluteTime.now()} onChange={(v) => { - onChange(v as any) - setOpen(false) - }} /> + onChange(v as any); + setOpen(false); + }} + /> </Dialog> - } + )} {/* {open && <Dialog onClose={() => setOpen(false)} > <TimePicker value={value as AbsoluteTime ?? AbsoluteTime.now()} diff --git a/packages/web-util/src/forms/InputAmount.tsx b/packages/web-util/src/forms/InputAmount.tsx index 7a8c08f76..31e83350e 100644 --- a/packages/web-util/src/forms/InputAmount.tsx +++ b/packages/web-util/src/forms/InputAmount.tsx @@ -3,11 +3,15 @@ import { VNode, h } from "preact"; import { UIFormProps } from "./FormProvider.js"; import { InputLine } from "./InputLine.js"; import { useField } from "./useField.js"; +import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; export function InputAmount<T extends object, K extends keyof T>( props: { currency?: string } & UIFormProps<T, K>, ): VNode { - const { value } = useField<T, K>(props.name); + //FIXME: remove deprecated + const fieldCtx = useField<T, K>(props.name); + const { value } = + props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.name); const currency = !value || !(value as any).currency ? props.currency @@ -22,8 +26,10 @@ export function InputAmount<T extends object, K extends keyof T>( converter={{ //@ts-ignore fromStringUI: (v): AmountJson => { - - return Amounts.parse(`${currency}:${v}`) ?? Amounts.zeroOfCurrency(currency); + return ( + Amounts.parse(`${currency}:${v}`) ?? + Amounts.zeroOfCurrency(currency) + ); }, //@ts-ignore toStringUI: (v: AmountJson) => { diff --git a/packages/web-util/src/forms/InputArray.tsx b/packages/web-util/src/forms/InputArray.tsx index 7d9a1b378..ac4617c8c 100644 --- a/packages/web-util/src/forms/InputArray.tsx +++ b/packages/web-util/src/forms/InputArray.tsx @@ -71,6 +71,14 @@ function Option({ ); } +export function noHandlerPropsAndNoContextForField( + field: string | number | symbol, +): never { + throw Error( + `Field ${field.toString()} doesn't have handler and is not in a form provider context.`, + ); +} + export function InputArray<T extends object, K extends keyof T>( props: { fields: UIFormField[]; @@ -78,7 +86,15 @@ export function InputArray<T extends object, K extends keyof T>( } & UIFormProps<T, K>, ): VNode { const { fields, labelField, name, label, required, tooltip } = props; - const { value, onChange, state } = useField<T, K>(name); + // const { value, onChange, state } = useField<T, K>(name); + //FIXME: remove deprecated + const fieldCtx = useField<T, K>(props.name); + if (!props.handler && !fieldCtx) { + throw Error(""); + } + const { value, onChange, state } = + props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.name); + const list = (value ?? []) as Array<Record<string, string | undefined>>; const [selectedIndex, setSelected] = useState<number | undefined>(undefined); const selected = @@ -97,6 +113,7 @@ export function InputArray<T extends object, K extends keyof T>( return ( <Option label={v[labelField] as TranslatedString} + key={idx} isSelected={selectedIndex === idx} isLast={idx === list.length - 1} disabled={selectedIndex !== undefined && selectedIndex !== idx} @@ -107,7 +124,7 @@ export function InputArray<T extends object, K extends keyof T>( /> ); })} - {!state.disabled && + {!state.disabled && ( <div class="pt-2"> <Option label={"Add..." as TranslatedString} @@ -124,7 +141,7 @@ export function InputArray<T extends object, K extends keyof T>( }} /> </div> - } + )} </div> {selectedIndex !== undefined && ( /** @@ -145,13 +162,13 @@ export function InputArray<T extends object, K extends keyof T>( onSubmit={(v) => { const newValue = [...list]; newValue.splice(selectedIndex, 1, v); - onChange(newValue as T[K]); + onChange(newValue as any); setSelected(undefined); }} onUpdate={(v) => { const newValue = [...list]; newValue.splice(selectedIndex, 1, v); - onChange(newValue as T[K]); + onChange(newValue as any); }} > <div class="px-4 py-6"> @@ -170,7 +187,7 @@ export function InputArray<T extends object, K extends keyof T>( onClick={() => { const newValue = [...list]; newValue.splice(selectedIndex, 1); - onChange(newValue as T[K]); + onChange(newValue as any); setSelected(undefined); }} class="block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 " diff --git a/packages/web-util/src/forms/InputChoiceHorizontal.tsx b/packages/web-util/src/forms/InputChoiceHorizontal.tsx index 778b73c75..82a7c3115 100644 --- a/packages/web-util/src/forms/InputChoiceHorizontal.tsx +++ b/packages/web-util/src/forms/InputChoiceHorizontal.tsx @@ -3,6 +3,7 @@ import { Fragment, VNode, h } from "preact"; import { UIFormProps } from "./FormProvider.js"; import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; import { useField } from "./useField.js"; +import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; export interface ChoiceH<V> { label: TranslatedString; @@ -14,19 +15,11 @@ export function InputChoiceHorizontal<T extends object, K extends keyof T>( choices: ChoiceH<T[K]>[]; } & UIFormProps<T, K>, ): VNode { - const { - choices, - name, - label, - tooltip, - help, - placeholder, - required, - before, - after, - converter, - } = props; - const { value, onChange, state, isDirty } = useField<T, K>(name); + const { choices, label, tooltip, help, required } = props; + //FIXME: remove deprecated + const fieldCtx = useField<T, K>(props.name); + const { value, onChange, state } = + props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.name); if (state.hidden) { return <Fragment />; } @@ -62,12 +55,13 @@ export function InputChoiceHorizontal<T extends object, K extends keyof T>( return ( <button type="button" + key={idx} disabled={state.disabled} label={choice.label} class={clazz} onClick={(e) => { onChange( - (value === choice.value ? undefined : choice.value) as T[K], + (value === choice.value ? undefined : choice.value) as any, ); }} > diff --git a/packages/web-util/src/forms/InputChoiceStacked.tsx b/packages/web-util/src/forms/InputChoiceStacked.tsx index 234bb2255..1928f4365 100644 --- a/packages/web-util/src/forms/InputChoiceStacked.tsx +++ b/packages/web-util/src/forms/InputChoiceStacked.tsx @@ -3,6 +3,7 @@ import { Fragment, VNode, h } from "preact"; import { UIFormProps } from "./FormProvider.js"; import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; import { useField } from "./useField.js"; +import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; export interface ChoiceS<V> { label: TranslatedString; @@ -27,7 +28,12 @@ export function InputChoiceStacked<T extends object, K extends keyof T>( after, converter, } = props; - const { value, onChange, state, isDirty } = useField<T, K>(name); + + //FIXME: remove deprecated + const fieldCtx = useField<T, K>(props.name); + const { value, onChange, state } = + props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.name); + if (state.hidden) { return <Fragment />; } @@ -41,7 +47,7 @@ export function InputChoiceStacked<T extends object, K extends keyof T>( /> <fieldset class="mt-2"> <div class="space-y-4"> - {choices.map((choice) => { + {choices.map((choice, idx) => { // const currentValue = !converter // ? choice.value // : converter.fromStringUI(choice.value) ?? ""; @@ -56,7 +62,7 @@ export function InputChoiceStacked<T extends object, K extends keyof T>( } return ( - <label class={clazz}> + <label key={idx} class={clazz}> <input type="radio" name="server-size" @@ -71,7 +77,7 @@ export function InputChoiceStacked<T extends object, K extends keyof T>( onChange( (value === choice.value ? undefined - : choice.value) as T[K], + : choice.value) as any, ); }} class="sr-only" diff --git a/packages/web-util/src/forms/InputFile.tsx b/packages/web-util/src/forms/InputFile.tsx index 6337d0902..6147eae59 100644 --- a/packages/web-util/src/forms/InputFile.tsx +++ b/packages/web-util/src/forms/InputFile.tsx @@ -2,6 +2,7 @@ import { Fragment, VNode, h } from "preact"; import { UIFormProps } from "./FormProvider.js"; import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; import { useField } from "./useField.js"; +import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; export function InputFile<T extends object, K extends keyof T>( props: { maxBites: number; accept?: string } & UIFormProps<T, K>, @@ -16,8 +17,12 @@ export function InputFile<T extends object, K extends keyof T>( maxBites, accept, } = props; - const { value, onChange, state } = useField<T, K>(name); - const help = propsHelp ?? state.help + //FIXME: remove deprecated + const fieldCtx = useField<T, K>(props.name); + const { value, onChange, state } = + props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.name); + + const help = propsHelp ?? state.help; if (state.hidden) { return <div />; } @@ -43,7 +48,7 @@ export function InputFile<T extends object, K extends keyof T>( clip-rule="evenodd" /> </svg> - {!state.disabled && + {!state.disabled && ( <div class="my-2 flex text-sm leading-6 text-gray-600"> <label for="file-upload" @@ -71,14 +76,16 @@ export function InputFile<T extends object, K extends keyof T>( "", ), ); - return onChange(`data:${f[0].type};base64,${b64}` as any); + return onChange( + `data:${f[0].type};base64,${b64}` as any, + ); }); }} /> </label> {/* <p class="pl-1">or drag and drop</p> */} </div> - } + )} </div> </div> ) : ( @@ -88,7 +95,7 @@ export function InputFile<T extends object, K extends keyof T>( class=" h-24 w-full object-cover relative" /> - {!state.disabled && + {!state.disabled && ( <div class="opacity-0 hover:opacity-70 duration-300 absolute rounded-lg border inset-0 z-10 flex justify-center text-xl items-center bg-black text-white cursor-pointer " onClick={() => { @@ -97,7 +104,7 @@ export function InputFile<T extends object, K extends keyof T>( > Clear </div> - } + )} </div> )} {help && <p class="text-xs leading-5 text-gray-600 mt-2">{help}</p>} diff --git a/packages/web-util/src/forms/InputLine.tsx b/packages/web-util/src/forms/InputLine.tsx index b8879f9ec..c7f69dd8a 100644 --- a/packages/web-util/src/forms/InputLine.tsx +++ b/packages/web-util/src/forms/InputLine.tsx @@ -3,6 +3,7 @@ import { ComponentChildren, Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; import { UIFormProps } from "./FormProvider.js"; import { useField } from "./useField.js"; +import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; //@ts-ignore const TooltipIcon = ( @@ -78,7 +79,11 @@ function InputWrapper<T extends object, K extends keyof T>({ error, disabled, required, -}: { error?: string; disabled: boolean, children: ComponentChildren } & UIFormProps<T, K>): VNode { +}: { + error?: string; + disabled: boolean; + children: ComponentChildren; +} & UIFormProps<T, K>): VNode { return ( <div class="sm:col-span-6"> <LabelWithTooltipMaybeRequired @@ -156,19 +161,22 @@ export function InputLine<T extends object, K extends keyof T>( props: { type: InputType } & UIFormProps<T, K>, ): VNode { const { name, placeholder, before, after, converter, type } = props; - const { value, onChange, state, isDirty } = useField<T, K>(name); + //FIXME: remove deprecated + const fieldCtx = useField<T, K>(props.name); + const { value, onChange, state } = + props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.name); - const [text, setText] = useState("") + const [text, setText] = useState(""); const fromString: (s: string) => any = converter?.fromStringUI ?? defaultFromString; const toString: (s: any) => string = converter?.toStringUI ?? defaultToString; useEffect(() => { - const newValue = toString(value) + const newValue = toString(value); if (newValue) { - setText(newValue) + setText(newValue); } - }, [value]) + }, [value]); if (state.hidden) return <div />; @@ -206,7 +214,7 @@ export function InputLine<T extends object, K extends keyof T>( } } } - const showError = isDirty && state.error; + const showError = value !== undefined && state.error; if (showError) { clazz += " text-red-900 ring-red-300 placeholder:text-red-300 focus:ring-red-500"; @@ -242,15 +250,17 @@ export function InputLine<T extends object, K extends keyof T>( } return ( - <InputWrapper<T, K> {...props} + <InputWrapper<T, K> + {...props} help={props.help ?? state.help} - disabled={state.disabled ?? false} error={showError ? state.error : undefined} + disabled={state.disabled ?? false} + error={showError ? state.error : undefined} > <input name={String(name)} type={type} onChange={(e) => { - setText(e.currentTarget.value) + setText(e.currentTarget.value); }} placeholder={placeholder ? placeholder : undefined} value={text} diff --git a/packages/web-util/src/forms/InputSelectMultiple.tsx b/packages/web-util/src/forms/InputSelectMultiple.tsx index a67eb23b7..972389cb3 100644 --- a/packages/web-util/src/forms/InputSelectMultiple.tsx +++ b/packages/web-util/src/forms/InputSelectMultiple.tsx @@ -4,6 +4,7 @@ import { UIFormProps } from "./FormProvider.js"; import { ChoiceS } from "./InputChoiceStacked.js"; import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; import { useField } from "./useField.js"; +import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; export function InputSelectMultiple<T extends object, K extends keyof T>( props: { @@ -12,23 +13,28 @@ export function InputSelectMultiple<T extends object, K extends keyof T>( max?: number; } & UIFormProps<T, K>, ): VNode { - const { name, label, choices, placeholder, tooltip, required, unique, max } = - props; - const { value, onChange, state } = useField<T, K>(name); + const { label, choices, placeholder, tooltip, required, unique, max } = props; + //FIXME: remove deprecated + const fieldCtx = useField<T, K>(props.name); + const { value, onChange, state } = + props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.name); const [filter, setFilter] = useState<string | undefined>(undefined); const regex = new RegExp(`.*${filter}.*`, "i"); - const choiceMap = choices.reduce((prev, curr) => { - return { ...prev, [curr.value as string]: curr.label }; - }, {} as Record<string, string>); + const choiceMap = choices.reduce( + (prev, curr) => { + return { ...prev, [curr.value as string]: curr.label }; + }, + {} as Record<string, string>, + ); const list = (value ?? []) as string[]; const filteredChoices = filter === undefined ? undefined : choices.filter((v) => { - return regex.test(v.label); - }); + return regex.test(v.label); + }); return ( <div class="sm:col-span-6"> <LabelWithTooltipMaybeRequired @@ -38,7 +44,10 @@ export function InputSelectMultiple<T extends object, K extends keyof T>( /> {list.map((v, idx) => { return ( - <span class="inline-flex items-center gap-x-0.5 rounded-md bg-gray-100 p-1 mr-2 text-xs font-medium text-gray-600"> + <span + key={idx} + class="inline-flex items-center gap-x-0.5 rounded-md bg-gray-100 p-1 mr-2 text-xs font-medium text-gray-600" + > {choiceMap[v]} <button type="button" @@ -46,7 +55,7 @@ export function InputSelectMultiple<T extends object, K extends keyof T>( onClick={() => { const newValue = [...list]; newValue.splice(idx, 1); - onChange(newValue as T[K]); + onChange(newValue as any); setFilter(undefined); }} class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20" @@ -64,91 +73,94 @@ export function InputSelectMultiple<T extends object, K extends keyof T>( ); })} - {!state.disabled && <div class="relative mt-2"> - <input - id="combobox" - type="text" - value={filter ?? ""} - onChange={(e) => { - setFilter(e.currentTarget.value); - }} - placeholder={placeholder} - class="w-full rounded-md border-0 bg-white py-1.5 pl-3 pr-12 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" - role="combobox" - aria-controls="options" - aria-expanded="false" - /> - <button - type="button" - disabled={state.disabled} - onClick={() => { - setFilter(filter === undefined ? "" : undefined); - }} - class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none" - > - <svg - class="h-5 w-5 text-gray-400" - viewBox="0 0 20 20" - fill="currentColor" - aria-hidden="true" + {!state.disabled && ( + <div class="relative mt-2"> + <input + id="combobox" + type="text" + value={filter ?? ""} + onChange={(e) => { + setFilter(e.currentTarget.value); + }} + placeholder={placeholder} + class="w-full rounded-md border-0 bg-white py-1.5 pl-3 pr-12 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + role="combobox" + aria-controls="options" + aria-expanded="false" + /> + <button + type="button" + disabled={state.disabled} + onClick={() => { + setFilter(filter === undefined ? "" : undefined); + }} + class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none" > - <path - fill-rule="evenodd" - d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z" - clip-rule="evenodd" - /> - </svg> - </button> + <svg + class="h-5 w-5 text-gray-400" + viewBox="0 0 20 20" + fill="currentColor" + aria-hidden="true" + > + <path + fill-rule="evenodd" + d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z" + clip-rule="evenodd" + /> + </svg> + </button> - {filteredChoices !== undefined && ( - <ul - class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" - id="options" - role="listbox" - > - {filteredChoices.map((v, idx) => { - return ( - <li - class="relative cursor-pointer select-none py-2 pl-3 pr-9 text-gray-900 hover:text-white hover:bg-indigo-600" - id="option-0" - role="option" - onClick={() => { - setFilter(undefined); - if (unique && list.indexOf(v.value as string) !== -1) { - return; - } - if (max !== undefined && list.length >= max) { - return; - } - const newValue = [...list]; - newValue.splice(0, 0, v.value as string); - onChange(newValue as T[K]); - }} + {filteredChoices !== undefined && ( + <ul + class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" + id="options" + role="listbox" + > + {filteredChoices.map((v, idx) => { + return ( + <li + key={idx} + class="relative cursor-pointer select-none py-2 pl-3 pr-9 text-gray-900 hover:text-white hover:bg-indigo-600" + id="option-0" + role="option" + onClick={() => { + setFilter(undefined); + if (unique && list.indexOf(v.value as string) !== -1) { + return; + } + if (max !== undefined && list.length >= max) { + return; + } + const newValue = [...list]; + newValue.splice(0, 0, v.value as string); + onChange(newValue as any); + }} - // tabindex="-1" - > - {/* <!-- Selected: "font-semibold" --> */} - <span class="block truncate">{v.label}</span> + // tabindex="-1" + > + {/* <!-- Selected: "font-semibold" --> */} + <span class="block truncate">{v.label}</span> - {/* <!-- + {/* <!-- Checkmark, only display for selected option. Active: "text-white", Not Active: "text-indigo-600" --> */} - </li> - ); - })} + </li> + ); + })} - {/* <!-- + {/* <!-- Combobox option, manage highlight styles based on mouseenter/mouseleave and keyboard navigation. Active: "text-white bg-indigo-600", Not Active: "text-gray-900" --> */} - {/* <!-- More items... --> */} - </ul> - )} - </div>} + {/* <!-- More items... --> */} + </ul> + )} + </div> + )} </div> ); } diff --git a/packages/web-util/src/forms/InputSelectOne.tsx b/packages/web-util/src/forms/InputSelectOne.tsx index d100b079d..26f887b08 100644 --- a/packages/web-util/src/forms/InputSelectOne.tsx +++ b/packages/web-util/src/forms/InputSelectOne.tsx @@ -4,27 +4,35 @@ import { UIFormProps } from "./FormProvider.js"; import { ChoiceS } from "./InputChoiceStacked.js"; import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; import { useField } from "./useField.js"; +import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; export function InputSelectOne<T extends object, K extends keyof T>( props: { choices: ChoiceS<T[K]>[]; } & UIFormProps<T, K>, ): VNode { - const { name, label, choices, placeholder, tooltip, required } = props; - const { value, onChange } = useField<T, K>(name); + const { label, choices, placeholder, tooltip, required } = props; + //FIXME: remove deprecated + const fieldCtx = useField<T, K>(props.name); + const { value, onChange } = + props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.name); + const [filter, setFilter] = useState<string | undefined>(undefined); const regex = new RegExp(`.*${filter}.*`, "i"); - const choiceMap = choices.reduce((prev, curr) => { - return { ...prev, [curr.value as string]: curr.label }; - }, {} as Record<string, string>); + const choiceMap = choices.reduce( + (prev, curr) => { + return { ...prev, [curr.value as string]: curr.label }; + }, + {} as Record<string, string>, + ); const filteredChoices = filter === undefined ? undefined : choices.filter((v) => { - return regex.test(v.label); - }); + return regex.test(v.label); + }); return ( <div class="sm:col-span-6"> <LabelWithTooltipMaybeRequired @@ -97,15 +105,16 @@ export function InputSelectOne<T extends object, K extends keyof T>( {filteredChoices.map((v, idx) => { return ( <li + key={idx} class="relative cursor-pointer select-none py-2 pl-3 pr-9 text-gray-900 hover:text-white hover:bg-indigo-600" id="option-0" role="option" onClick={() => { setFilter(undefined); - onChange(v.value as T[K]); + onChange(v.value as any); }} - // tabindex="-1" + // tabindex="-1" > {/* <!-- Selected: "font-semibold" --> */} <span class="block truncate">{v.label}</span> diff --git a/packages/web-util/src/forms/InputToggle.tsx b/packages/web-util/src/forms/InputToggle.tsx index 56b29c502..c56efec3a 100644 --- a/packages/web-util/src/forms/InputToggle.tsx +++ b/packages/web-util/src/forms/InputToggle.tsx @@ -2,6 +2,7 @@ import { VNode, h } from "preact"; import { UIFormProps } from "./FormProvider.js"; import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; import { useField } from "./useField.js"; +import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; export function InputToggle<T extends object, K extends keyof T>( props: UIFormProps<T, K>, @@ -17,22 +18,39 @@ export function InputToggle<T extends object, K extends keyof T>( after, converter, } = props; - const { value, onChange, state, isDirty } = useField<T, K>(name); + //FIXME: remove deprecated + const fieldCtx = useField<T, K>(props.name); + const { value, onChange } = + props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.name); - const isOn = !!value - return <div class="sm:col-span-6"> - <div class="flex items-center justify-between"> - <LabelWithTooltipMaybeRequired - label={label} - required={required} - tooltip={tooltip} - /> - <button type="button" data-enabled={isOn} - class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" - role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description" - onClick={() => { onChange(!isOn as any); }}> - <span aria-hidden="true" data-enabled={isOn} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span> - </button> + const isOn = !!value; + return ( + <div class="sm:col-span-6"> + <div class="flex items-center justify-between"> + <LabelWithTooltipMaybeRequired + label={label} + required={required} + tooltip={tooltip} + /> + <button + type="button" + data-enabled={isOn} + class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" + role="switch" + aria-checked="false" + aria-labelledby="availability-label" + aria-describedby="availability-description" + onClick={() => { + onChange(!isOn as any); + }} + > + <span + aria-hidden="true" + data-enabled={isOn} + class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out" + ></span> + </button> + </div> </div> - </div> + ); } diff --git a/packages/web-util/src/forms/forms.ts b/packages/web-util/src/forms/forms.ts index 3b8620bfb..1ad3508ae 100644 --- a/packages/web-util/src/forms/forms.ts +++ b/packages/web-util/src/forms/forms.ts @@ -109,26 +109,26 @@ export function RenderAllFieldsByUiConfig({ ); } -type FormSet<T extends object> = { - Provider: typeof FormProvider<T>; - InputLine: <K extends keyof T>() => typeof InputLine<T, K>; - InputChoiceHorizontal: <K extends keyof T>() => typeof InputChoiceHorizontal<T, K>; -}; +// type FormSet<T extends object> = { +// Provider: typeof FormProvider<T>; +// InputLine: <K extends keyof T>() => typeof InputLine<T, K>; +// InputChoiceHorizontal: <K extends keyof T>() => typeof InputChoiceHorizontal<T, K>; +// }; /** * Helper function that created a typed object. * * @returns */ -export function createNewForm<T extends object>() { - const res: FormSet<T> = { - Provider: FormProvider, - InputLine: () => InputLine, - InputChoiceHorizontal: () => InputChoiceHorizontal, - }; - return { - Provider: res.Provider, - InputLine: res.InputLine(), - InputChoiceHorizontal: res.InputChoiceHorizontal(), - }; -} +// export function createNewForm<T extends object>() { +// const res: FormSet<T> = { +// Provider: FormProvider, +// InputLine: () => InputLine, +// InputChoiceHorizontal: () => InputChoiceHorizontal, +// }; +// return { +// Provider: res.Provider, +// InputLine: res.InputLine(), +// InputChoiceHorizontal: res.InputChoiceHorizontal(), +// }; +// } diff --git a/packages/web-util/src/forms/useField.ts b/packages/web-util/src/forms/useField.ts index eed8cebea..fad53ebac 100644 --- a/packages/web-util/src/forms/useField.ts +++ b/packages/web-util/src/forms/useField.ts @@ -8,15 +8,25 @@ export interface InputFieldHandler<Type> { isDirty: boolean; } +/** + * @depreacted removing this so we don't depend on context to create a form + * @param name + * @returns + */ export function useField<T extends object, K extends keyof T>( name: K, -): InputFieldHandler<T[K]> { +): InputFieldHandler<T[K]> | undefined { + const ctx = useContext(FormContext); + if (!ctx) { + //no context, can't be used + return undefined; + } const { value: formValue, computeFormState, onUpdate: notifyUpdate, readOnly: readOnlyForm, - } = useContext(FormContext); + } = ctx type P = typeof name; type V = T[P]; @@ -24,7 +34,7 @@ export function useField<T extends object, K extends keyof T>( const fieldValue = readField(formValue.current, String(name)) as V; // console.log("USE FIELD", String(name), formValue.current, fieldValue); - const [currentValue, setCurrentValue] = useState<any | undefined>(fieldValue); + // const [currentValue, setCurrentValue] = useState<any | undefined>(fieldValue); const fieldState = readField<Partial<FieldUIOptions>>(formState, String(name)) ?? {}; @@ -38,7 +48,7 @@ export function useField<T extends object, K extends keyof T>( }; function onChange(value: V): void { - setCurrentValue(value); + // setCurrentValue(value); formValue.current = setValueDeeper( formValue.current, String(name).split("."), @@ -52,7 +62,7 @@ export function useField<T extends object, K extends keyof T>( return { value: fieldValue, onChange, - isDirty: currentValue !== undefined, + isDirty: fieldValue !== undefined, state, }; } @@ -67,18 +77,8 @@ export function useField<T extends object, K extends keyof T>( function readField<T>( object: any, name: string, - debug?: boolean, ): T | undefined { return name.split(".").reduce((prev, current) => { - if (debug) { - console.log( - "READ", - name, - prev, - current, - prev ? prev[current] : undefined, - ); - } return prev ? prev[current] : undefined; }, object); } |