rastack 0.0.25 → 0.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [0.0.27](https://github.com/theserverkid/reactapistack/compare/v0.0.26...v0.0.27) (2026-07-10)
6
+
7
+ ### [0.0.26](https://github.com/theserverkid/reactapistack/compare/v0.0.25...v0.0.26) (2026-07-10)
8
+
5
9
  ### [0.0.25](https://github.com/theserverkid/reactapistack/compare/v0.0.24...v0.0.25) (2026-07-10)
6
10
 
7
11
  ### [0.0.24](https://github.com/theserverkid/reactapistack/compare/v0.0.23...v0.0.24) (2026-07-09)
@@ -0,0 +1,254 @@
1
+ import React, { useState } from "react";
2
+ import { tokens } from "../style";
3
+ import type { AutoFormLike } from "../types";
4
+ import { Field } from "../primitives/field";
5
+ import {
6
+ Input,
7
+ Textarea,
8
+ Select,
9
+ Checkbox,
10
+ Button,
11
+ } from "../primitives/controls";
12
+ import {
13
+ useAutoForm,
14
+ type AutoFormFieldModel,
15
+ type AutoFormModel,
16
+ } from "./use-auto-form";
17
+ import type { AutoFormInputKind, AutoFormOptions } from "./fields";
18
+
19
+ export interface AutoFormProps extends AutoFormOptions {
20
+ /** The form hook result (`useUpdateXForm(...)`) driving the form. */
21
+ form: AutoFormLike;
22
+ submitLabel?: React.ReactNode;
23
+ /** Renders a secondary button that calls this. */
24
+ onCancel?: () => void;
25
+ cancelLabel?: React.ReactNode;
26
+ /** Called after the form's own submit fires (e.g. to close a modal). */
27
+ onSubmit?: () => void;
28
+ /** Hide the built-in actions row (render your own submit control). */
29
+ hideActions?: boolean;
30
+ /** Field-grid column count. Default 1. */
31
+ columns?: number;
32
+ /** Total-control escape hatch: receives the headless model. */
33
+ children?: (model: AutoFormModel) => React.ReactNode;
34
+ style?: React.CSSProperties;
35
+ className?: string;
36
+ }
37
+
38
+ /** Format a stored value for a native date / datetime-local input. */
39
+ function toInputValue(kind: AutoFormInputKind, value: unknown): string {
40
+ if (value == null) return "";
41
+ const s = String(value);
42
+ if (kind === "date") return s.slice(0, 10);
43
+ if (kind === "datetime-local") return s.slice(0, 16);
44
+ return s;
45
+ }
46
+
47
+ /**
48
+ * A form that builds itself from a form hook.
49
+ *
50
+ * ```tsx
51
+ * <AutoForm form={useUpdateAirportForm({ formValues: { code: "", name: "" } })} />
52
+ * ```
53
+ *
54
+ * Each field's control is chosen from the datatype the form hook derives from
55
+ * the write schema — booleans become checkboxes, `date-time` fields date
56
+ * pickers, enums dropdowns, and so on. Override any field (label, control,
57
+ * options for an FK picker, custom render) via the `fields` option.
58
+ */
59
+ export function AutoForm(props: AutoFormProps): React.JSX.Element {
60
+ const {
61
+ form,
62
+ fields,
63
+ exclude,
64
+ submitLabel = "Save",
65
+ onCancel,
66
+ cancelLabel = "Cancel",
67
+ onSubmit,
68
+ hideActions = false,
69
+ columns = 1,
70
+ children,
71
+ style,
72
+ className,
73
+ } = props;
74
+
75
+ const model = useAutoForm(form, { fields, exclude });
76
+ const [touched, setTouched] = useState<Set<string>>(() => new Set());
77
+ const [submitted, setSubmitted] = useState(false);
78
+
79
+ const markTouched = (name: string) => {
80
+ setTouched((prev) => {
81
+ if (prev.has(name)) return prev;
82
+ const next = new Set(prev);
83
+ next.add(name);
84
+ return next;
85
+ });
86
+ };
87
+
88
+ const handleSubmit = (e: React.FormEvent) => {
89
+ e.preventDefault();
90
+ setSubmitted(true);
91
+ model.submit();
92
+ onSubmit?.();
93
+ };
94
+
95
+ const renderControl = (f: AutoFormFieldModel): React.ReactNode => {
96
+ const { field, input, id } = f;
97
+ const blur = () => {
98
+ markTouched(f.name);
99
+ field.setTouched(true);
100
+ };
101
+ const invalid = (touched.has(f.name) || submitted) && field.isError;
102
+
103
+ if (f.render) return f.render(field, { name: f.name, id });
104
+
105
+ switch (input) {
106
+ case "checkbox":
107
+ return (
108
+ <Checkbox
109
+ id={id}
110
+ label={f.label}
111
+ checked={Boolean(field.value)}
112
+ onChange={(e) => field.setValue(e.target.checked)}
113
+ onBlur={blur}
114
+ />
115
+ );
116
+ case "select": {
117
+ const options = f.options ?? [];
118
+ return (
119
+ <Select
120
+ id={id}
121
+ invalid={invalid}
122
+ value={field.value == null ? "" : String(field.value)}
123
+ placeholder={field.isRequired ? undefined : (f.placeholder ?? "—")}
124
+ options={options}
125
+ onChange={(e) => {
126
+ const match = options.find(
127
+ (o) => String(o.value) === e.target.value,
128
+ );
129
+ field.setValue(match ? match.value : e.target.value);
130
+ }}
131
+ onBlur={blur}
132
+ />
133
+ );
134
+ }
135
+ case "textarea":
136
+ return (
137
+ <Textarea
138
+ id={id}
139
+ invalid={invalid}
140
+ value={field.value == null ? "" : String(field.value)}
141
+ placeholder={f.placeholder}
142
+ onChange={(e) => field.setValue(e.target.value)}
143
+ onBlur={blur}
144
+ />
145
+ );
146
+ default: {
147
+ const htmlType =
148
+ input === "number"
149
+ ? "number"
150
+ : input === "date"
151
+ ? "date"
152
+ : input === "datetime-local"
153
+ ? "datetime-local"
154
+ : input === "email"
155
+ ? "email"
156
+ : input === "url"
157
+ ? "url"
158
+ : "text";
159
+ return (
160
+ <Input
161
+ id={id}
162
+ type={htmlType}
163
+ invalid={invalid}
164
+ maxLength={
165
+ input === "text" || input === "email" || input === "url"
166
+ ? field.maxLength
167
+ : undefined
168
+ }
169
+ value={toInputValue(input, field.value)}
170
+ placeholder={f.placeholder}
171
+ onChange={(e) => field.setValue(e.target.value)}
172
+ onBlur={blur}
173
+ />
174
+ );
175
+ }
176
+ }
177
+ };
178
+
179
+ const renderField = (f: AutoFormFieldModel): React.ReactNode => {
180
+ const invalid = (touched.has(f.name) || submitted) && f.field.isError;
181
+ const errorText = invalid ? f.field.error : undefined;
182
+ // Checkboxes carry their own inline label; don't repeat it above.
183
+ const label = f.input === "checkbox" ? undefined : f.label;
184
+ return (
185
+ <Field
186
+ key={f.name}
187
+ label={label}
188
+ htmlFor={f.id}
189
+ required={f.field.isRequired}
190
+ error={errorText}
191
+ hint={f.hint}
192
+ >
193
+ {renderControl(f)}
194
+ </Field>
195
+ );
196
+ };
197
+
198
+ return (
199
+ <form
200
+ onSubmit={handleSubmit}
201
+ className={className}
202
+ style={{
203
+ display: "flex",
204
+ flexDirection: "column",
205
+ gap: tokens.space.lg,
206
+ ...style,
207
+ }}
208
+ noValidate
209
+ >
210
+ {children ? (
211
+ children(model)
212
+ ) : (
213
+ <div
214
+ style={{
215
+ display: "grid",
216
+ gridTemplateColumns: `repeat(${Math.max(1, columns)}, minmax(0, 1fr))`,
217
+ gap: tokens.space.lg,
218
+ }}
219
+ >
220
+ {model.fields.map(renderField)}
221
+ </div>
222
+ )}
223
+
224
+ {!hideActions && !children ? (
225
+ <div
226
+ style={{
227
+ display: "flex",
228
+ gap: tokens.space.sm,
229
+ justifyContent: "flex-end",
230
+ }}
231
+ >
232
+ {onCancel ? (
233
+ <Button
234
+ type="button"
235
+ variant="outline"
236
+ onClick={onCancel}
237
+ disabled={model.isSubmitting}
238
+ >
239
+ {cancelLabel}
240
+ </Button>
241
+ ) : null}
242
+ <Button
243
+ type="submit"
244
+ variant="primary"
245
+ loading={model.isSubmitting}
246
+ disabled={!model.canSubmit}
247
+ >
248
+ {submitLabel}
249
+ </Button>
250
+ </div>
251
+ ) : null}
252
+ </form>
253
+ );
254
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Auto-form field resolution — the pure core.
3
+ *
4
+ * Two decisions live here, both React-free and unit tested:
5
+ * 1. **Which fields, in what order** — from an explicit `fields` list, else the
6
+ * keys of the form's current values — minus any `exclude`d.
7
+ * 2. **Which control each field gets** — mapped from the enriched
8
+ * {@link FormType} the form hook now derives from the write JSON Schema
9
+ * (`boolean → checkbox`, `date → date picker`, `select → dropdown`, …).
10
+ * `<AutoForm>` turns these specs into live fields; you can also call
11
+ * {@link inputKindForFieldType} yourself to drive a custom form.
12
+ */
13
+
14
+ import type React from "react";
15
+ import type { FieldOption, FormType } from "../../hooks/form/interfaces";
16
+ import type { AutoFormField } from "../types";
17
+ import { humanizeKey } from "../util";
18
+
19
+ /** The concrete control an auto-form field renders as. */
20
+ export type AutoFormInputKind =
21
+ | "text"
22
+ | "textarea"
23
+ | "number"
24
+ | "checkbox"
25
+ | "date"
26
+ | "datetime-local"
27
+ | "select"
28
+ | "email"
29
+ | "url";
30
+
31
+ /** Per-field overrides. */
32
+ export interface AutoFormFieldConfig {
33
+ label?: React.ReactNode;
34
+ /** Force the control kind (e.g. render an FK `relation` as a `select`). */
35
+ input?: AutoFormInputKind;
36
+ /** Options for a `select` — e.g. records loaded for a foreign key. */
37
+ options?: FieldOption[];
38
+ hint?: React.ReactNode;
39
+ placeholder?: string;
40
+ /** Fully custom render for this field, given its live structure. */
41
+ render?: (
42
+ field: AutoFormField,
43
+ ctx: { name: string; id: string },
44
+ ) => React.ReactNode;
45
+ }
46
+
47
+ /** A field entry with its name, usable in the `fields` option. */
48
+ export interface AutoFormFieldSpec extends AutoFormFieldConfig {
49
+ name: string;
50
+ }
51
+
52
+ export interface AutoFormOptions {
53
+ /**
54
+ * Explicit fields — sets the order and, optionally, per-field config. A bare
55
+ * string includes a field with defaults; an object configures it. When
56
+ * omitted, every key of the form's values is used.
57
+ */
58
+ fields?: Array<string | AutoFormFieldSpec>;
59
+ /** Names to drop (applied whether fields are explicit or inferred). */
60
+ exclude?: string[];
61
+ }
62
+
63
+ /** Map an enriched {@link FormType} to the control the auto-form renders. */
64
+ export function inputKindForFieldType(type: FormType): AutoFormInputKind {
65
+ switch (type) {
66
+ case "boolean":
67
+ return "checkbox";
68
+ case "number":
69
+ return "number";
70
+ case "date":
71
+ return "date";
72
+ case "datetime":
73
+ return "datetime-local";
74
+ case "select":
75
+ return "select";
76
+ case "email":
77
+ return "email";
78
+ case "url":
79
+ return "url";
80
+ case "array":
81
+ return "textarea";
82
+ case "text":
83
+ default:
84
+ return "text";
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Decide the ordered, configured field specs for a form. Pure: it reads only the
90
+ * field-name list (the caller passes `Object.keys(formValues)`) and the options.
91
+ */
92
+ export function resolveFieldSpecs(
93
+ fieldNames: string[],
94
+ options: AutoFormOptions = {},
95
+ ): AutoFormFieldSpec[] {
96
+ const exclude = new Set(options.exclude ?? []);
97
+
98
+ let specs: AutoFormFieldSpec[];
99
+ if (options.fields && options.fields.length > 0) {
100
+ specs = options.fields.map((f) =>
101
+ typeof f === "string" ? { name: f } : { ...f },
102
+ );
103
+ } else {
104
+ specs = fieldNames.map((name) => ({ name }));
105
+ }
106
+
107
+ return specs.filter((s) => !exclude.has(s.name));
108
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * `useAutoForm` — the headless form model.
3
+ *
4
+ * Give it a `useForm()` result and it returns one descriptor per field —
5
+ * resolved label, the control kind picked from the field's datatype, and the
6
+ * live field structure (value, setter, error, options) — plus `submit`/`reset`
7
+ * and submit-state flags. `<AutoForm>` renders these; consume it directly to lay
8
+ * out a bespoke form while keeping the datatype-driven wiring.
9
+ */
10
+
11
+ import type React from "react";
12
+ import type { FieldOption } from "../../hooks/form/interfaces";
13
+ import type { AutoFormField, AutoFormLike } from "../types";
14
+ import { humanizeKey } from "../util";
15
+ import {
16
+ inputKindForFieldType,
17
+ resolveFieldSpecs,
18
+ type AutoFormInputKind,
19
+ type AutoFormOptions,
20
+ } from "./fields";
21
+
22
+ export interface AutoFormFieldModel {
23
+ name: string;
24
+ /** DOM id linking label ↔ control. */
25
+ id: string;
26
+ label: React.ReactNode;
27
+ input: AutoFormInputKind;
28
+ /** The live field: `value`, `setValue`, `error`, `isError`, `isRequired`, `maxLength`, … */
29
+ field: AutoFormField;
30
+ /** Resolved choices for a `select` (config options, else the field's enum). */
31
+ options?: FieldOption[];
32
+ hint?: React.ReactNode;
33
+ placeholder?: string;
34
+ render?: (
35
+ field: AutoFormField,
36
+ ctx: { name: string; id: string },
37
+ ) => React.ReactNode;
38
+ }
39
+
40
+ export interface AutoFormModel {
41
+ fields: AutoFormFieldModel[];
42
+ submit: () => void;
43
+ reset: () => void;
44
+ isSubmitting: boolean;
45
+ /** `true` when the form is valid and not mid-submit. */
46
+ canSubmit: boolean;
47
+ errors: Record<string, unknown>;
48
+ }
49
+
50
+ /** Pure model assembly — see {@link useAutoForm}. Testable with a mock form. */
51
+ export function buildAutoFormModel(
52
+ form: AutoFormLike,
53
+ options: AutoFormOptions = {},
54
+ ): AutoFormModel {
55
+ const fieldNames = Object.keys(form.formValues ?? {});
56
+ const specs = resolveFieldSpecs(fieldNames, options);
57
+
58
+ const fields: AutoFormFieldModel[] = specs.map((spec) => {
59
+ const field = form.getFormField(spec.name);
60
+ return {
61
+ name: spec.name,
62
+ id: `af-${spec.name}`,
63
+ label: spec.label ?? humanizeKey(spec.name),
64
+ input: spec.input ?? inputKindForFieldType(field.type),
65
+ field,
66
+ options: spec.options ?? field.options,
67
+ hint: spec.hint,
68
+ placeholder: spec.placeholder,
69
+ render: spec.render,
70
+ };
71
+ });
72
+
73
+ return {
74
+ fields,
75
+ submit: () => form.onSubmit(),
76
+ reset: () => form.resetForm(),
77
+ isSubmitting: form.isSubmitting,
78
+ canSubmit: !form.isSubmitDisabled && !form.isSubmitting,
79
+ errors: (form.errors as Record<string, unknown>) ?? {},
80
+ };
81
+ }
82
+
83
+ /** React hook wrapper around {@link buildAutoFormModel}. */
84
+ export function useAutoForm(
85
+ form: AutoFormLike,
86
+ options: AutoFormOptions = {},
87
+ ): AutoFormModel {
88
+ return buildAutoFormModel(form, options);
89
+ }