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" /> <link rel="icon" href={favicon} type="image/x-icon" />
<title>BKND</title> <title>BKND</title>
<script
crossOrigin="anonymous"
src="//unpkg.com/react-scan/dist/auto.global.js"
/>
{isProd ? ( {isProd ? (
<Fragment> <Fragment>
<script <script

View File

@@ -1,6 +1,7 @@
import type { JSONSchema } from "json-schema-to-ts"; import type { JSONSchema } from "json-schema-to-ts";
import type { ChangeEvent, ComponentPropsWithoutRef } from "react"; import type { ChangeEvent, ComponentPropsWithoutRef } from "react";
import * as Formy from "ui/components/form/Formy"; import * as Formy from "ui/components/form/Formy";
import { useEvent } from "ui/hooks/use-event";
import { ArrayField } from "./ArrayField"; import { ArrayField } from "./ArrayField";
import { FieldWrapper } from "./FieldWrapper"; import { FieldWrapper } from "./FieldWrapper";
import { useFieldContext } from "./Form"; 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; const disabled = schema.readOnly ?? "const" in schema ?? false;
//console.log("field", name, disabled, schema, ctx.schema, _schema); //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 // don't remove for now, causes issues in anyOf
/*const value = coerce(e.target.value, schema as any); /*const value = coerce(e.target.value, schema as any);
setValue(pointer, value 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); //console.log("setValue", pointer, value);
setValue(pointer, value); setValue(pointer, value);
} }
} });
return ( return (
<FieldWrapper <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 { Draft2019, type JsonError } from "json-schema-library";
import type { TemplateOptions as LibTemplateOptions } from "json-schema-library/dist/lib/getTemplate"; import type { TemplateOptions as LibTemplateOptions } from "json-schema-library/dist/lib/getTemplate";
import type { JsonSchema as LibJsonSchema } from "json-schema-library/dist/lib/types"; import type { JsonSchema as LibJsonSchema } from "json-schema-library/dist/lib/types";
@@ -9,16 +11,32 @@ import {
type FormEvent, type FormEvent,
type ReactNode, type ReactNode,
createContext, createContext,
useCallback,
useContext, useContext,
useEffect, useEffect,
useMemo,
useRef, useRef,
useState useState
} from "react"; } from "react";
import { JsonViewer } from "ui/components/code/JsonViewer"; import { JsonViewer } from "ui/components/code/JsonViewer";
import { useEvent } from "ui/hooks/use-event";
import { Field } from "./Field"; import { Field } from "./Field";
import { isRequired, normalizePath, omitSchema, prefixPointer } from "./utils"; import { isRequired, normalizePath, omitSchema, prefixPointer } from "./utils";
type JSONSchema = Exclude<$JSONSchema, boolean>; 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< export type FormProps<
Schema extends JSONSchema = JSONSchema, Schema extends JSONSchema = JSONSchema,
@@ -72,19 +90,22 @@ export function Form<
...props ...props
}: FormProps<Schema, Data>) { }: FormProps<Schema, Data>) {
const [schema, initial] = omitSchema(_schema, ignoreKeys, _initialValues); 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 initialValues = initial ?? lib.getTemplate(undefined, schema, initialOpts);
const [data, setData] = useState<Partial<Data>>(initialValues); const [formState, setFormState] = useAtom<FormState<Data>>(formStateAtom);
const [dirty, setDirty] = useState<boolean>(false);
const [errors, setErrors] = useState<JsonError[]>([]);
const [submitting, setSubmitting] = useState<boolean>(false);
const formRef = useRef<HTMLFormElement | null>(null); const formRef = useRef<HTMLFormElement | null>(null);
useEffect(() => {
console.log("setting data");
setFormState((prev) => ({ ...prev, data: initialValues }));
}, []);
// @ts-ignore // @ts-ignore
async function handleSubmit(e: FormEvent<HTMLFormElement>) { async function handleSubmit(e: FormEvent<HTMLFormElement>) {
if (onSubmit) { if (onSubmit) {
e.preventDefault(); e.preventDefault();
setSubmitting(true); setFormState((prev) => ({ ...prev, submitting: true }));
//setSubmitting(true);
try { try {
const { data, errors } = validate(); const { data, errors } = validate();
@@ -97,72 +118,89 @@ export function Form<
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
} }
setFormState((prev) => ({ ...prev, submitting: false }));
setSubmitting(false); //setSubmitting(false);
return false; return false;
} }
} }
function setValue(pointer: string, value: any) { const setValue = useEvent((pointer: string, value: any) => {
const normalized = normalizePath(pointer); const normalized = normalizePath(pointer);
//console.log("setValue", { pointer, normalized, value }); //console.log("setValue", { pointer, normalized, value });
const key = normalized.substring(2).replace(/\//g, "."); 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); const changed = immutable.set(prev, key, value);
onChange?.(changed, key, value); onChange?.(changed, key, value);
//console.log("changed", prev, changed, { key, value }); //console.log("changed", prev, changed, { key, value });
return changed; return changed;
}); });*/
} });
function deleteValue(pointer: string) { const deleteValue = useEvent((pointer: string) => {
const normalized = normalizePath(pointer); const normalized = normalizePath(pointer);
const key = normalized.substring(2).replace(/\//g, "."); 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); const changed = immutable.del(prev, key);
onChange?.(changed, key, undefined); onChange?.(changed, key, undefined);
//console.log("changed", prev, changed, { key }); //console.log("changed", prev, changed, { key });
return changed; return changed;
}); });*/
} });
useEffect(() => { useEffect(() => {
setDirty(!isEqual(initialValues, data)); //setDirty(!isEqual(initialValues, data));
//setFormState((prev => ({ ...prev, dirty: !isEqual(initialValues, data) })));
if (validateOn === "change") { if (validateOn === "change") {
validate(); validate();
} else if (errors.length > 0) { } else if (formState?.errors?.length > 0) {
validate(); validate();
} }
}, [data]); }, [formState?.data]);
function validate(_data?: Partial<Data>) { function validate(_data?: Partial<Data>) {
const actual = _data ?? data; const actual = _data ?? formState?.data;
const errors = lib.validate(actual, schema); const errors = lib.validate(actual, schema);
//console.log("errors", errors); //console.log("errors", errors);
setErrors(errors); setFormState((prev) => ({ ...prev, errors }));
//setErrors(errors);
return { data: actual, errors }; return { data: actual, errors };
} }
const context = { const context = useMemo(
data: data ?? {}, () => ({
dirty, setValue,
submitting, deleteValue,
setData, schema,
setValue, lib,
deleteValue, options
errors, }),
schema, []
lib, ) as any;
options
} as any;
//console.log("context", context); //console.log("context", context);
const Component = useMemo(() => {
return children ? children : <Field name="" />;
}, []);
return ( return (
<form {...props} ref={formRef} onSubmit={handleSubmit}> <form {...props} ref={formRef} onSubmit={handleSubmit}>
<FormContext.Provider value={context}> <FormContext.Provider value={context}>{Component}</FormContext.Provider>
{children ? children : <Field name="" />}
</FormContext.Provider>
{hiddenSubmit && ( {hiddenSubmit && (
<button style={{ visibility: "hidden" }} type="submit"> <button style={{ visibility: "hidden" }} type="submit">
Submit Submit
@@ -210,28 +248,62 @@ export function FormContextOverride({
return <FormContext.Provider value={context}>{children}</FormContext.Provider>; 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) { 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 pointer = normalizePath(name);
const isRootPointer = pointer === "#/"; const isRootPointer = pointer === "#/";
//console.log("pointer", pointer); //console.log("pointer", pointer);
const value = isRootPointer ? data : get(data, pointer.substring(2).replace(/\//g, ".")); const data = {};
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);
return { const value = useFormValue(name);
...rest, console.log("value", pointer, value);
lib, //const value = isRootPointer ? data : get(data, pointer.substring(2).replace(/\//g, "."));
value, const errors = useMemo(
errors, () => formErrors.filter((error) => error.data.pointer.startsWith(pointer)),
schema: fieldSchema, [name]
pointer, );
required 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 }) { export function Subscribe({ children }: { children: (ctx: FormContext<any>) => ReactNode }) {
const ctx = useFormContext(); const ctx = useFormContext();
@@ -244,3 +316,21 @@ export function FormDebug() {
return <JsonViewer json={{ dirty, submitting, data, errors }} expand={99} />; 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, Form,
FormContextOverride, FormContextOverride,
FormDebug, FormDebug,
FormDebug2,
ObjectField ObjectField
} from "ui/components/form/json-schema-form"; } from "ui/components/form/json-schema-form";
import { Scrollable } from "ui/layouts/AppShell/AppShell"; 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() { export default function JsonSchemaForm3() {
const { schema, config } = useBknd(); const { schema, config } = useBknd();
return ( return (
<Scrollable> <Scrollable>
<div className="flex flex-col p-3"> <div className="flex flex-col p-3">
{/*<Form <Form schema={schema2} className="flex flex-col gap-3">
schema={{ <div>random thing</div>
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"
>
<Field name="name" /> <Field name="name" />
<Field name="age" /> <Field name="age" />
<Field name="gender" /> <FormDebug />
<Field name="deep" /> <FormDebug2 name="name" />
</Form>*/} </Form>
{/*<Form {/*<Form
schema={{ schema={{
@@ -90,7 +91,7 @@ export default function JsonSchemaForm3() {
> >
<AutoForm /> <AutoForm />
</Form>*/} </Form>*/}
<Form {/*<Form
schema={{ schema={{
type: "object", type: "object",
properties: { properties: {
@@ -118,7 +119,7 @@ export default function JsonSchemaForm3() {
> >
<Field name="" /> <Field name="" />
<FormDebug /> <FormDebug />
</Form> </Form>*/}
{/*<Form {/*<Form
schema={{ schema={{