feat: add application productivity foundations
Quality / quality (ubuntu-latest) (push) Failing after 22s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-23 11:13:03 +05:30
parent 46195462c3
commit 64ab20cc95
42 changed files with 1533 additions and 40 deletions
+75
View File
@@ -0,0 +1,75 @@
import { computed, signal, type ReadonlySignal, type Signal } from "./signal.ts";
export interface FormState<T extends Record<string, unknown>> {
values: Signal<T>;
errors: Signal<Partial<Record<keyof T, string>>>;
submitting: Signal<boolean>;
dirty: ReadonlySignal<boolean>;
field<K extends keyof T>(
name: K,
): {
value: T[K];
onInput(payload: { value?: unknown; checked?: boolean }): void;
};
set<K extends keyof T>(name: K, value: T[K]): void;
reset(next?: T): void;
validate(): Promise<boolean>;
submit<R>(operation: (values: T) => R | Promise<R>): Promise<R>;
}
export function useForm<T extends Record<string, unknown>>(
initial: T,
validator?: (
values: T,
) => Partial<Record<keyof T, string>> | Promise<Partial<Record<keyof T, string>>>,
): FormState<T> {
let baseline = structuredClone(initial);
const values = signal(structuredClone(initial));
const errors = signal<Partial<Record<keyof T, string>>>({});
const submitting = signal(false);
const dirty = computed(() => JSON.stringify(values.get()) !== JSON.stringify(baseline));
const set = <K extends keyof T>(name: K, value: T[K]) => {
values.set({ ...values.get(), [name]: value });
if (errors.get()[name]) errors.set({ ...errors.get(), [name]: undefined });
};
const validate = async () => {
errors.set((await validator?.(values.get())) ?? {});
return Object.values(errors.get()).every((value) => !value);
};
return {
values,
errors,
submitting,
dirty,
field(name) {
return {
value: values.get()[name],
onInput(payload) {
const current = values.get()[name];
set(
name,
(typeof current === "boolean"
? Boolean(payload.checked)
: payload.value) as T[typeof name],
);
},
};
},
set,
reset(next = initial) {
baseline = structuredClone(next);
values.set(structuredClone(next));
errors.set({});
},
validate,
async submit(operation) {
if (!(await validate())) throw new Error("WRN-FORM-INVALID");
submitting.set(true);
try {
return await operation(values.get());
} finally {
submitting.set(false);
}
},
};
}
+2
View File
@@ -17,6 +17,8 @@ export {
createTimeline,
urlSignal,
} from "./advanced.ts";
export { useForm } from "./form.ts";
export type { FormState } from "./form.ts";
export type {
HistorySignal,
ReactiveContext,