mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +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" />
|
<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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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={{
|
||||||
|
|||||||
Reference in New Issue
Block a user