mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
initially reduced form rerenders
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
const context = useMemo(
|
||||
() => ({
|
||||
setValue,
|
||||
deleteValue,
|
||||
errors,
|
||||
schema,
|
||||
lib,
|
||||
options
|
||||
} as any;
|
||||
}),
|
||||
[]
|
||||
) 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 {
|
||||
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>;
|
||||
}
|
||||
|
||||
@@ -6,18 +6,12 @@ import {
|
||||
Form,
|
||||
FormContextOverride,
|
||||
FormDebug,
|
||||
FormDebug2,
|
||||
ObjectField
|
||||
} from "ui/components/form/json-schema-form";
|
||||
import { Scrollable } from "ui/layouts/AppShell/AppShell";
|
||||
|
||||
export default function JsonSchemaForm3() {
|
||||
const { schema, config } = useBknd();
|
||||
|
||||
return (
|
||||
<Scrollable>
|
||||
<div className="flex flex-col p-3">
|
||||
{/*<Form
|
||||
schema={{
|
||||
const schema2 = {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", default: "Peter" },
|
||||
@@ -34,14 +28,21 @@ export default function JsonSchemaForm3() {
|
||||
}
|
||||
},
|
||||
required: ["age"]
|
||||
}}
|
||||
className="flex flex-col gap-3"
|
||||
>
|
||||
};
|
||||
|
||||
export default function JsonSchemaForm3() {
|
||||
const { schema, config } = useBknd();
|
||||
|
||||
return (
|
||||
<Scrollable>
|
||||
<div className="flex flex-col p-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={{
|
||||
|
||||
Reference in New Issue
Block a user