added more input field types and improved typing

This commit is contained in:
dswbx
2025-02-07 17:28:01 +01:00
parent 11a6f5c9b9
commit 2e3ee65aa7
6 changed files with 153 additions and 41 deletions

View File

@@ -100,7 +100,7 @@ const ArrayIterator = memo(
({ name, children }: any) => {
return children(useFormValue(name));
},
(prev, next) => prev.value.length === next.value.length
(prev, next) => prev.value?.length === next.value?.length
);
const ArrayAdd = ({ schema, path }: { schema: JsonSchema; path: string }) => {
@@ -114,7 +114,6 @@ const ArrayAdd = ({ schema, path }: { schema: JsonSchema; path: string }) => {
const itemsMultiSchema = getMultiSchema(schema.items);
function handleAdd(template?: any) {
//const currentIndex = value?.length ?? 0;
const newPointer = `${path}/${currentIndex}`.replace(/\/+/g, "/");
setValue(newPointer, template ?? ctx.lib.getTemplate(undefined, schema!.items));
}

View File

@@ -81,12 +81,61 @@ export const FieldComponent = ({
}
if (isType(schema.type, ["number", "integer"])) {
return <Formy.Input type="number" id={props.name} {...props} value={props.value ?? ""} />;
const additional = {
min: schema.minimum,
max: schema.maximum,
step: schema.multipleOf
};
return (
<Formy.Input
type="number"
id={props.name}
{...props}
value={props.value ?? ""}
{...additional}
/>
);
}
if (isType(schema.type, "boolean")) {
return <Formy.Switch id={props.name} {...(props as any)} checked={value as any} />;
return <Formy.Switch id={props.name} {...(props as any)} checked={value === true} />;
}
return <Formy.Input id={props.name} {...props} value={props.value ?? ""} />;
if (isType(schema.type, "string") && schema.format === "date-time") {
const value = props.value ? new Date(props.value as string).toISOString().slice(0, 16) : "";
return (
<Formy.DateInput
id={props.name}
{...props}
value={value}
type="datetime-local"
onChange={(e) => {
const date = new Date(e.target.value);
props.onChange?.({
// @ts-ignore
target: { value: date.toISOString() }
});
}}
/>
);
}
if (isType(schema.type, "string") && schema.format === "date") {
return <Formy.DateInput id={props.name} {...props} value={props.value ?? ""} />;
}
const additional = {
maxLength: schema.maxLength,
minLength: schema.minLength,
pattern: schema.pattern
} as any;
if (schema.format) {
if (["password", "hidden", "url", "email", "tel"].includes(schema.format)) {
additional.type = schema.format;
}
}
return <Formy.Input id={props.name} {...props} value={props.value ?? ""} {...additional} />;
};

View File

@@ -5,7 +5,11 @@ import { Children, type ReactElement, type ReactNode, cloneElement, isValidEleme
import { IconButton } from "ui/components/buttons/IconButton";
import { JsonViewer } from "ui/components/code/JsonViewer";
import * as Formy from "ui/components/form/Formy";
import { useFormError } from "ui/components/form/json-schema-form/Form";
import {
useFormContext,
useFormError,
useFormValue
} from "ui/components/form/json-schema-form/Form";
import { getLabel } from "./utils";
export type FieldwrapperProps = {
@@ -24,7 +28,6 @@ export function FieldWrapper({
label: _label,
required,
schema,
debug,
wrapper,
hidden,
children
@@ -41,29 +44,7 @@ export function FieldWrapper({
as={wrapper === "fieldset" ? "fieldset" : "div"}
className={hidden ? "hidden" : "relative"}
>
{debug && (
<div className="absolute right-0 top-0">
{/* @todo: use radix */}
<Popover>
<Popover.Target>
<IconButton Icon={IconBug} size="xs" className="opacity-30" />
</Popover.Target>
<Popover.Dropdown>
<JsonViewer
json={{
...(typeof debug === "object" ? debug : {}),
name,
required,
schema,
errors
}}
expand={6}
className="p-0"
/>
</Popover.Dropdown>
</Popover>
</div>
)}
<FieldDebug name={name} schema={schema} required={required} />
{label && (
<Formy.Label
@@ -98,3 +79,38 @@ export function FieldWrapper({
</Formy.Group>
);
}
const FieldDebug = ({
name,
schema,
required
}: Pick<FieldwrapperProps, "name" | "schema" | "required">) => {
const { options } = useFormContext();
if (!options?.debug) return null;
const { value } = useFormValue(name);
const errors = useFormError(name, { strict: true });
return (
<div className="absolute right-0 top-0">
{/* @todo: use radix */}
<Popover>
<Popover.Target>
<IconButton Icon={IconBug} size="xs" className="opacity-30" />
</Popover.Target>
<Popover.Dropdown>
<JsonViewer
json={{
name,
value,
required,
schema,
errors
}}
expand={6}
className="p-0"
/>
</Popover.Dropdown>
</Popover>
</div>
);
};

View File

@@ -45,32 +45,32 @@ type FormState<Data = any> = {
export type FormProps<
Schema extends JSONSchema = JSONSchema,
Data = Schema extends JSONSchema ? FromSchema<JSONSchema> : any
> = Omit<ComponentPropsWithoutRef<"form">, "onChange"> & {
Data = Schema extends JSONSchema ? FromSchema<Schema> : any,
InitialData = Schema extends JSONSchema ? FromSchema<Schema> : any
> = Omit<ComponentPropsWithoutRef<"form">, "onChange" | "onSubmit"> & {
schema: Schema;
validateOn?: "change" | "submit";
initialValues?: Partial<Data>;
initialOpts?: LibTemplateOptions;
ignoreKeys?: string[];
onChange?: (data: Partial<Data>, name: string, value: any) => void;
onSubmit?: (data: Partial<Data>) => void | Promise<void>;
onSubmit?: (data: Data) => void | Promise<void>;
onInvalidSubmit?: (errors: JsonError[], data: Partial<Data>) => void;
hiddenSubmit?: boolean;
options?: {
debug?: boolean;
keepEmpty?: boolean;
};
initialValues?: InitialData;
};
export type FormContext<Data> = {
data: Data;
setData: (data: Data) => void;
setValue: (pointer: string, value: any) => void;
deleteValue: (pointer: string) => void;
errors: JsonError[];
dirty: boolean;
submitting: boolean;
schema: JSONSchema;
schema: LibJsonSchema;
lib: Draft2019;
options: FormProps["options"];
root: string;
@@ -82,7 +82,7 @@ FormContext.displayName = "FormContext";
export function Form<
Schema extends JSONSchema = JSONSchema,
Data = Schema extends JSONSchema ? FromSchema<JSONSchema> : any
Data = Schema extends JSONSchema ? FromSchema<Schema> : any
>({
schema: _schema,
initialValues: _initialValues,
@@ -126,7 +126,7 @@ export function Form<
try {
const { data, errors } = validate();
if (errors.length === 0) {
await onSubmit(data);
await onSubmit(data as Data);
} else {
console.log("invalid", errors);
onInvalidSubmit?.(errors, data);
@@ -200,6 +200,7 @@ export function Form<
<form {...props} ref={formRef} onSubmit={handleSubmit}>
<FormContext.Provider value={context}>
{children ? children : <Field name="" />}
{options?.debug && <FormDebug />}
</FormContext.Provider>
{hiddenSubmit && (
<button style={{ visibility: "hidden" }} type="submit">

View File

@@ -141,7 +141,7 @@ export function isRequired(pointer: string, schema: JsonSchema, data?: any) {
const lib = new Draft2019(schema as any);
const childSchema = lib.getSchema({ pointer, data });
if (typeof childSchema === "object" && ("const" in childSchema || "enum" in childSchema)) {
if (typeof childSchema === "object" && "const" in childSchema) {
return true;
}

View File

@@ -1,3 +1,4 @@
import type { JSONSchema } from "json-schema-to-ts";
import { useBknd } from "ui/client/bknd";
import { Button } from "ui/components/buttons/Button";
import {
@@ -237,7 +238,7 @@ export default function JsonSchemaForm3() {
<FormDebug force />
</Form>*/}
<CustomMediaForm />
{/*<CustomMediaForm />*/}
{/*<Form schema={schema.media} initialValues={config.media} validateOn="change">
<Field name="" />
</Form>*/}
@@ -255,11 +256,57 @@ export default function JsonSchemaForm3() {
>
<AutoForm />
</Form>*/}
<Form
schema={ss}
initialValues={{
name: "Peter",
age: 20,
interested: true,
dinnerTime: "2023-12-31T23:59:59+02:00"
}}
ignoreKeys={["what"]}
onChange={(state) => console.log(state)}
onSubmit={(state) => console.log(state)}
validateOn="change"
options={{ debug: true }}
/>
</div>
</Scrollable>
);
}
const ss = {
type: "object",
properties: {
name: { type: "string" },
email: { type: "string", format: "email" },
interested: { type: "boolean" },
bla: {
type: "string",
enum: ["small", "medium", "large"]
},
password: { type: "string", format: "password" },
birthdate: { type: "string", format: "date" },
dinnerTime: { type: "string", format: "date-time" },
age: { type: "number", minimum: 0, multipleOf: 5 },
tags: {
type: "array",
items: {
type: "string"
}
},
config: {
type: "object",
properties: {
min: { type: "number" }
}
}
},
required: ["name"],
additionalProperties: false
} as const satisfies JSONSchema;
function CustomMediaForm() {
const { schema, config } = useBknd();
@@ -269,7 +316,7 @@ function CustomMediaForm() {
return (
<Form
schema={schema.media}
initialValues={config.media}
initialValues={config.media as any}
className="flex flex-col gap-3"
validateOn="change"
>