initially reduced form rerenders

This commit is contained in:
dswbx
2025-02-06 17:12:43 +01:00
parent 78d4f928ba
commit 02e7e1ca95
4 changed files with 175 additions and 79 deletions

View File

@@ -191,6 +191,10 @@ export class AdminController extends Controller {
/>
<link rel="icon" href={favicon} type="image/x-icon" />
<title>BKND</title>
<script
crossOrigin="anonymous"
src="//unpkg.com/react-scan/dist/auto.global.js"
/>
{isProd ? (
<Fragment>
<script

View File

@@ -1,6 +1,7 @@
import type { JSONSchema } from "json-schema-to-ts";
import type { ChangeEvent, ComponentPropsWithoutRef } from "react";
import * as Formy from "ui/components/form/Formy";
import { useEvent } from "ui/hooks/use-event";
import { ArrayField } from "./ArrayField";
import { FieldWrapper } from "./FieldWrapper";
import { useFieldContext } from "./Form";
@@ -32,7 +33,7 @@ export const Field = ({ name, schema: _schema, onChange, label: _label, hidden }
const disabled = schema.readOnly ?? "const" in schema ?? false;
//console.log("field", name, disabled, schema, ctx.schema, _schema);
function handleChange(e: ChangeEvent<HTMLInputElement>) {
const handleChange = useEvent((e: ChangeEvent<HTMLInputElement>) => {
// don't remove for now, causes issues in anyOf
/*const value = coerce(e.target.value, schema as any);
setValue(pointer, value as any);*/
@@ -45,7 +46,7 @@ export const Field = ({ name, schema: _schema, onChange, label: _label, hidden }
//console.log("setValue", pointer, value);
setValue(pointer, value);
}
}
});
return (
<FieldWrapper

View File

@@ -1,3 +1,5 @@
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
import { selectAtom } from "jotai/utils";
import { Draft2019, type JsonError } from "json-schema-library";
import type { TemplateOptions as LibTemplateOptions } from "json-schema-library/dist/lib/getTemplate";
import type { JsonSchema as LibJsonSchema } from "json-schema-library/dist/lib/types";
@@ -9,16 +11,32 @@ import {
type FormEvent,
type ReactNode,
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState
} from "react";
import { JsonViewer } from "ui/components/code/JsonViewer";
import { useEvent } from "ui/hooks/use-event";
import { Field } from "./Field";
import { isRequired, normalizePath, omitSchema, prefixPointer } from "./utils";
type JSONSchema = Exclude<$JSONSchema, boolean>;
type FormState<Data = any> = {
dirty: boolean;
submitting: boolean;
errors: JsonError[];
data: Data;
};
const formStateAtom = atom<FormState>({
dirty: false,
submitting: false,
errors: [] as JsonError[],
data: {} as any
});
export type FormProps<
Schema extends JSONSchema = JSONSchema,
@@ -72,19 +90,22 @@ export function Form<
...props
}: FormProps<Schema, Data>) {
const [schema, initial] = omitSchema(_schema, ignoreKeys, _initialValues);
const lib = new Draft2019(schema);
const lib = useMemo(() => new Draft2019(schema), [JSON.stringify(schema)]);
const initialValues = initial ?? lib.getTemplate(undefined, schema, initialOpts);
const [data, setData] = useState<Partial<Data>>(initialValues);
const [dirty, setDirty] = useState<boolean>(false);
const [errors, setErrors] = useState<JsonError[]>([]);
const [submitting, setSubmitting] = useState<boolean>(false);
const [formState, setFormState] = useAtom<FormState<Data>>(formStateAtom);
const formRef = useRef<HTMLFormElement | null>(null);
useEffect(() => {
console.log("setting data");
setFormState((prev) => ({ ...prev, data: initialValues }));
}, []);
// @ts-ignore
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
if (onSubmit) {
e.preventDefault();
setSubmitting(true);
setFormState((prev) => ({ ...prev, submitting: true }));
//setSubmitting(true);
try {
const { data, errors } = validate();
@@ -97,72 +118,89 @@ export function Form<
} catch (e) {
console.warn(e);
}
setFormState((prev) => ({ ...prev, submitting: false }));
setSubmitting(false);
//setSubmitting(false);
return false;
}
}
function setValue(pointer: string, value: any) {
const setValue = useEvent((pointer: string, value: any) => {
const normalized = normalizePath(pointer);
//console.log("setValue", { pointer, normalized, value });
const key = normalized.substring(2).replace(/\//g, ".");
setData((prev) => {
setFormState((state) => {
const prev = state.data;
const changed = immutable.set(prev, key, value);
onChange?.(changed, key, value);
//console.log("changed", prev, changed, { key, value });
return { ...state, data: changed };
});
/*setData((prev) => {
const changed = immutable.set(prev, key, value);
onChange?.(changed, key, value);
//console.log("changed", prev, changed, { key, value });
return changed;
});
}
});*/
});
function deleteValue(pointer: string) {
const deleteValue = useEvent((pointer: string) => {
const normalized = normalizePath(pointer);
const key = normalized.substring(2).replace(/\//g, ".");
setData((prev) => {
setFormState((state) => {
const prev = state.data;
const changed = immutable.del(prev, key);
onChange?.(changed, key, undefined);
//console.log("changed", prev, changed, { key, value });
return { ...state, data: changed };
});
/*setData((prev) => {
const changed = immutable.del(prev, key);
onChange?.(changed, key, undefined);
//console.log("changed", prev, changed, { key });
return changed;
});
}
});*/
});
useEffect(() => {
setDirty(!isEqual(initialValues, data));
//setDirty(!isEqual(initialValues, data));
//setFormState((prev => ({ ...prev, dirty: !isEqual(initialValues, data) })));
if (validateOn === "change") {
validate();
} else if (errors.length > 0) {
} else if (formState?.errors?.length > 0) {
validate();
}
}, [data]);
}, [formState?.data]);
function validate(_data?: Partial<Data>) {
const actual = _data ?? data;
const actual = _data ?? formState?.data;
const errors = lib.validate(actual, schema);
//console.log("errors", errors);
setErrors(errors);
setFormState((prev) => ({ ...prev, errors }));
//setErrors(errors);
return { data: actual, errors };
}
const context = {
data: data ?? {},
dirty,
submitting,
setData,
setValue,
deleteValue,
errors,
schema,
lib,
options
} as any;
const context = useMemo(
() => ({
setValue,
deleteValue,
schema,
lib,
options
}),
[]
) as any;
//console.log("context", context);
const Component = useMemo(() => {
return children ? children : <Field name="" />;
}, []);
return (
<form {...props} ref={formRef} onSubmit={handleSubmit}>
<FormContext.Provider value={context}>
{children ? children : <Field name="" />}
</FormContext.Provider>
<FormContext.Provider value={context}>{Component}</FormContext.Provider>
{hiddenSubmit && (
<button style={{ visibility: "hidden" }} type="submit">
Submit
@@ -210,28 +248,62 @@ export function FormContextOverride({
return <FormContext.Provider value={context}>{children}</FormContext.Provider>;
}
export function useFormValue(name: string) {
const pointer = normalizePath(name);
const isRootPointer = pointer === "#/";
const selected = selectAtom(
formStateAtom,
useCallback(
(state) => {
const data = state.data;
console.log("data", data);
return isRootPointer ? data : get(data, pointer.substring(2).replace(/\//g, "."));
},
[pointer]
),
isEqual
);
return useAtom(selected)[0];
}
export function useFieldContext(name: string) {
const { data, lib, schema, errors: formErrors, ...rest } = useFormContext();
const { lib, schema, errors: formErrors = [], ...rest } = useFormContext();
const pointer = normalizePath(name);
const isRootPointer = pointer === "#/";
//console.log("pointer", pointer);
const value = isRootPointer ? data : get(data, pointer.substring(2).replace(/\//g, "."));
const errors = formErrors.filter((error) => error.data.pointer.startsWith(pointer));
const fieldSchema = isRootPointer
? (schema as LibJsonSchema)
: lib.getSchema({ pointer, data, schema });
const required = isRequired(pointer, schema, data);
const data = {};
return {
...rest,
lib,
value,
errors,
schema: fieldSchema,
pointer,
required
};
const value = useFormValue(name);
console.log("value", pointer, value);
//const value = isRootPointer ? data : get(data, pointer.substring(2).replace(/\//g, "."));
const errors = useMemo(
() => formErrors.filter((error) => error.data.pointer.startsWith(pointer)),
[name]
);
const fieldSchema = useMemo(
() => (isRootPointer ? (schema as LibJsonSchema) : lib.getSchema({ pointer, data, schema })),
[name]
);
const required = false; // isRequired(pointer, schema, data);
const options = useMemo(() => ({}), []);
return useMemo(
() => ({
...rest,
dirty: false,
submitting: false,
options,
lib,
value,
errors,
schema: fieldSchema,
pointer,
required
}),
[JSON.stringify([value])]
);
}
useFieldContext.displayName = "useFieldContext";
export function Subscribe({ children }: { children: (ctx: FormContext<any>) => ReactNode }) {
const ctx = useFormContext();
@@ -244,3 +316,21 @@ export function FormDebug() {
return <JsonViewer json={{ dirty, submitting, data, errors }} expand={99} />;
}
function useFieldContext2(name: string) {
const ctx = useRef(useFormContext());
const pointer = normalizePath(name);
const isRootPointer = pointer === "#/";
//console.log("pointer", pointer);
const data = {};
const options = useMemo(() => ({}), []);
const required = false;
const value = useFormValue(name);
return { value, options, dirty: false, submitting: false, required, pointer };
}
export function FormDebug2({ name }: any) {
const { ...ctx } = useFieldContext2(name);
return <pre>{JSON.stringify({ ctx })}</pre>;
}

View File

@@ -6,42 +6,43 @@ import {
Form,
FormContextOverride,
FormDebug,
FormDebug2,
ObjectField
} from "ui/components/form/json-schema-form";
import { Scrollable } from "ui/layouts/AppShell/AppShell";
const schema2 = {
type: "object",
properties: {
name: { type: "string", default: "Peter" },
age: { type: "number" },
gender: {
type: "string",
enum: ["male", "female", "uni"]
},
deep: {
type: "object",
properties: {
nested: { type: "string" }
}
}
},
required: ["age"]
};
export default function JsonSchemaForm3() {
const { schema, config } = useBknd();
return (
<Scrollable>
<div className="flex flex-col p-3">
{/*<Form
schema={{
type: "object",
properties: {
name: { type: "string", default: "Peter" },
age: { type: "number" },
gender: {
type: "string",
enum: ["male", "female", "uni"]
},
deep: {
type: "object",
properties: {
nested: { type: "string" }
}
}
},
required: ["age"]
}}
className="flex flex-col gap-3"
>
<Form schema={schema2} className="flex flex-col gap-3">
<div>random thing</div>
<Field name="name" />
<Field name="age" />
<Field name="gender" />
<Field name="deep" />
</Form>*/}
<FormDebug />
<FormDebug2 name="name" />
</Form>
{/*<Form
schema={{
@@ -90,7 +91,7 @@ export default function JsonSchemaForm3() {
>
<AutoForm />
</Form>*/}
<Form
{/*<Form
schema={{
type: "object",
properties: {
@@ -118,7 +119,7 @@ export default function JsonSchemaForm3() {
>
<Field name="" />
<FormDebug />
</Form>
</Form>*/}
{/*<Form
schema={{