kayforms 0.1.1
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/LICENSE +21 -0
- package/README.md +337 -0
- package/examples/react-demo/README.md +337 -0
- package/examples/react-demo/eslint.config.js +22 -0
- package/examples/react-demo/index.html +13 -0
- package/examples/react-demo/package.json +33 -0
- package/examples/react-demo/public/apple-touch-icon.png +0 -0
- package/examples/react-demo/public/favicon-96x96.png +0 -0
- package/examples/react-demo/public/favicon.ico +0 -0
- package/examples/react-demo/public/favicon.svg +17 -0
- package/examples/react-demo/public/icons.svg +24 -0
- package/examples/react-demo/public/site.webmanifest +21 -0
- package/examples/react-demo/public/web-app-manifest-192x192.png +0 -0
- package/examples/react-demo/public/web-app-manifest-512x512.png +0 -0
- package/examples/react-demo/src/App.css +184 -0
- package/examples/react-demo/src/App.tsx +825 -0
- package/examples/react-demo/src/assets/hero.png +0 -0
- package/examples/react-demo/src/assets/react.svg +1 -0
- package/examples/react-demo/src/assets/vite.svg +1 -0
- package/examples/react-demo/src/index.css +627 -0
- package/examples/react-demo/src/main.tsx +10 -0
- package/examples/react-demo/tsconfig.app.json +25 -0
- package/examples/react-demo/tsconfig.json +7 -0
- package/examples/react-demo/tsconfig.node.json +24 -0
- package/examples/react-demo/vite.config.ts +7 -0
- package/kayforms.jpg +0 -0
- package/package.json +26 -0
- package/packages/angular/package.json +43 -0
- package/packages/angular/src/index.ts +198 -0
- package/packages/angular/tsconfig.json +8 -0
- package/packages/angular/tsup.config.ts +17 -0
- package/packages/core/README.md +337 -0
- package/packages/core/package.json +37 -0
- package/packages/core/src/batch.ts +106 -0
- package/packages/core/src/devtools.ts +329 -0
- package/packages/core/src/field.ts +167 -0
- package/packages/core/src/form.ts +448 -0
- package/packages/core/src/index.ts +71 -0
- package/packages/core/src/registry.ts +126 -0
- package/packages/core/src/signal.ts +399 -0
- package/packages/core/src/time-travel.ts +275 -0
- package/packages/core/src/validation.ts +243 -0
- package/packages/core/tsconfig.json +8 -0
- package/packages/core/tsup.config.ts +16 -0
- package/packages/devtools/extension/background.js +35 -0
- package/packages/devtools/extension/content-script.js +10 -0
- package/packages/devtools/extension/devtools.html +9 -0
- package/packages/devtools/extension/devtools.js +8 -0
- package/packages/devtools/extension/manifest.json +19 -0
- package/packages/devtools/extension/panel.css +505 -0
- package/packages/devtools/extension/panel.html +108 -0
- package/packages/devtools/extension/panel.js +354 -0
- package/packages/devtools/package.json +38 -0
- package/packages/devtools/src/index.ts +95 -0
- package/packages/devtools/src/panel.ts +226 -0
- package/packages/devtools/src/styles.ts +422 -0
- package/packages/devtools/src/timeline.ts +283 -0
- package/packages/devtools/tsconfig.json +8 -0
- package/packages/devtools/tsup.config.ts +17 -0
- package/packages/react/package.json +46 -0
- package/packages/react/src/index.ts +279 -0
- package/packages/react/tsconfig.json +8 -0
- package/packages/react/tsup.config.ts +17 -0
- package/packages/solid/package.json +42 -0
- package/packages/solid/src/index.ts +206 -0
- package/packages/solid/tsconfig.json +8 -0
- package/packages/solid/tsup.config.ts +17 -0
- package/packages/svelte/package.json +42 -0
- package/packages/svelte/src/index.ts +199 -0
- package/packages/svelte/tsconfig.json +8 -0
- package/packages/svelte/tsup.config.ts +17 -0
- package/packages/vanilla/package.json +38 -0
- package/packages/vanilla/src/index.ts +254 -0
- package/packages/vanilla/tsconfig.json +8 -0
- package/packages/vanilla/tsup.config.ts +17 -0
- package/packages/vue/package.json +42 -0
- package/packages/vue/src/index.ts +217 -0
- package/packages/vue/tsconfig.json +8 -0
- package/packages/vue/tsup.config.ts +17 -0
- package/pnpm-workspace.yaml +3 -0
- package/tsconfig.base.json +21 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// @kayforms/vanilla — Vanilla JS Adapter
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Binds Kayforms signal-based forms to plain DOM elements.
|
|
5
|
+
// Supports both programmatic binding and declarative data-attribute binding.
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
createEffect,
|
|
10
|
+
type FormStore,
|
|
11
|
+
type FieldNode,
|
|
12
|
+
} from "@kayforms/core";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Types
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export interface BindFieldOptions {
|
|
19
|
+
/** Custom value extractor (default: el.value) */
|
|
20
|
+
getValue?: (el: HTMLElement) => unknown;
|
|
21
|
+
/** Custom value setter (default: el.value = ...) */
|
|
22
|
+
setValue?: (el: HTMLElement, value: unknown) => void;
|
|
23
|
+
/** CSS class to add when field has error */
|
|
24
|
+
errorClass?: string;
|
|
25
|
+
/** Element to display error message (created automatically if not provided) */
|
|
26
|
+
errorElement?: HTMLElement;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface BindFormOptions extends BindFieldOptions {
|
|
30
|
+
/** CSS class for error messages (default: 'kayform-error') */
|
|
31
|
+
errorClass?: string;
|
|
32
|
+
/** Whether to prevent default form submit (default: true) */
|
|
33
|
+
preventDefault?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// bindField — Bind a single input to a FieldNode
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Bind a DOM input element to a Kayforms FieldNode.
|
|
42
|
+
* Automatically syncs value, error state, and touched state.
|
|
43
|
+
* Returns an unbind function.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* const form = createForm({ initialValues: { email: '' } });
|
|
48
|
+
* const emailInput = document.querySelector('#email');
|
|
49
|
+
* const unbind = bindField(emailInput, form.getField('email'));
|
|
50
|
+
* // Later: unbind();
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function bindField(
|
|
54
|
+
element: HTMLElement,
|
|
55
|
+
field: FieldNode,
|
|
56
|
+
options: BindFieldOptions = {}
|
|
57
|
+
): () => void {
|
|
58
|
+
const {
|
|
59
|
+
getValue = (el) => (el as HTMLInputElement).value,
|
|
60
|
+
setValue = (el, v) => {
|
|
61
|
+
(el as HTMLInputElement).value = String(v ?? "");
|
|
62
|
+
},
|
|
63
|
+
errorClass = "kayform-error",
|
|
64
|
+
} = options;
|
|
65
|
+
|
|
66
|
+
const cleanups: (() => void)[] = [];
|
|
67
|
+
|
|
68
|
+
// --- Sync value: signal → DOM ---
|
|
69
|
+
const disposeValueEffect = createEffect(() => {
|
|
70
|
+
const val = field.value.value;
|
|
71
|
+
setValue(element, val);
|
|
72
|
+
});
|
|
73
|
+
cleanups.push(disposeValueEffect);
|
|
74
|
+
|
|
75
|
+
// --- Sync value: DOM → signal ---
|
|
76
|
+
const handleInput = () => {
|
|
77
|
+
const domValue = getValue(element);
|
|
78
|
+
field.onChange(domValue);
|
|
79
|
+
};
|
|
80
|
+
element.addEventListener("input", handleInput);
|
|
81
|
+
cleanups.push(() => element.removeEventListener("input", handleInput));
|
|
82
|
+
|
|
83
|
+
// For select, radio, checkbox
|
|
84
|
+
const handleChange = () => {
|
|
85
|
+
const el = element as HTMLInputElement;
|
|
86
|
+
if (el.type === "checkbox") {
|
|
87
|
+
field.onChange(el.checked);
|
|
88
|
+
} else if (el.type === "radio") {
|
|
89
|
+
if (el.checked) field.onChange(el.value);
|
|
90
|
+
} else {
|
|
91
|
+
field.onChange(getValue(element));
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
element.addEventListener("change", handleChange);
|
|
95
|
+
cleanups.push(() => element.removeEventListener("change", handleChange));
|
|
96
|
+
|
|
97
|
+
// --- Blur → touched ---
|
|
98
|
+
const handleBlur = () => field.onBlur();
|
|
99
|
+
element.addEventListener("blur", handleBlur);
|
|
100
|
+
cleanups.push(() => element.removeEventListener("blur", handleBlur));
|
|
101
|
+
|
|
102
|
+
// --- Error display ---
|
|
103
|
+
let errorEl = options.errorElement;
|
|
104
|
+
if (!errorEl) {
|
|
105
|
+
errorEl = document.createElement("span");
|
|
106
|
+
errorEl.className = errorClass;
|
|
107
|
+
errorEl.setAttribute("role", "alert");
|
|
108
|
+
errorEl.setAttribute("aria-live", "polite");
|
|
109
|
+
element.parentNode?.insertBefore(errorEl, element.nextSibling);
|
|
110
|
+
cleanups.push(() => errorEl?.remove());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const disposeErrorEffect = createEffect(() => {
|
|
114
|
+
const err = field.error.value;
|
|
115
|
+
const isTouched = field.touched.value;
|
|
116
|
+
if (errorEl) {
|
|
117
|
+
errorEl.textContent = isTouched && err ? err : "";
|
|
118
|
+
errorEl.style.display = isTouched && err ? "" : "none";
|
|
119
|
+
}
|
|
120
|
+
if (isTouched && err) {
|
|
121
|
+
element.classList.add(errorClass);
|
|
122
|
+
element.setAttribute("aria-invalid", "true");
|
|
123
|
+
} else {
|
|
124
|
+
element.classList.remove(errorClass);
|
|
125
|
+
element.removeAttribute("aria-invalid");
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
cleanups.push(disposeErrorEffect);
|
|
129
|
+
|
|
130
|
+
// Return unbind function
|
|
131
|
+
return () => {
|
|
132
|
+
for (const cleanup of cleanups) {
|
|
133
|
+
cleanup();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// bindForm — Bind an entire form element
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Bind a form element to a Kayforms FormStore.
|
|
144
|
+
* Automatically binds all child inputs with `name` attributes.
|
|
145
|
+
* Returns an unbind function.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* const form = createForm({
|
|
150
|
+
* initialValues: { email: '', password: '' },
|
|
151
|
+
* onSubmit: (values) => console.log(values),
|
|
152
|
+
* });
|
|
153
|
+
* const unbind = bindForm(document.querySelector('form'), form);
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
export function bindForm(
|
|
157
|
+
formElement: HTMLFormElement,
|
|
158
|
+
store: FormStore,
|
|
159
|
+
options: BindFormOptions = {}
|
|
160
|
+
): () => void {
|
|
161
|
+
const { preventDefault = true, ...fieldOptions } = options;
|
|
162
|
+
const cleanups: (() => void)[] = [];
|
|
163
|
+
|
|
164
|
+
// Find all named inputs
|
|
165
|
+
const inputs = formElement.querySelectorAll<HTMLElement>(
|
|
166
|
+
"input[name], select[name], textarea[name]"
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
for (const input of inputs) {
|
|
170
|
+
const name = input.getAttribute("name");
|
|
171
|
+
if (!name) continue;
|
|
172
|
+
|
|
173
|
+
const field = store.getField(name);
|
|
174
|
+
const unbind = bindField(input, field, fieldOptions);
|
|
175
|
+
cleanups.push(unbind);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Handle form submit
|
|
179
|
+
const handleSubmit = (e: Event) => {
|
|
180
|
+
if (preventDefault) {
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
}
|
|
183
|
+
store.submit();
|
|
184
|
+
};
|
|
185
|
+
formElement.addEventListener("submit", handleSubmit);
|
|
186
|
+
cleanups.push(() =>
|
|
187
|
+
formElement.removeEventListener("submit", handleSubmit)
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
return () => {
|
|
191
|
+
for (const cleanup of cleanups) {
|
|
192
|
+
cleanup();
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// autoBindForm — Data-attribute-based declarative binding
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Automatically bind form elements using `data-kayform` attributes.
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```html
|
|
206
|
+
* <form id="login">
|
|
207
|
+
* <input data-kayform="email" type="email" />
|
|
208
|
+
* <input data-kayform="password" type="password" />
|
|
209
|
+
* <button type="submit">Login</button>
|
|
210
|
+
* </form>
|
|
211
|
+
* ```
|
|
212
|
+
* ```ts
|
|
213
|
+
* const form = createForm({ initialValues: { email: '', password: '' } });
|
|
214
|
+
* const unbind = autoBindForm(document.querySelector('#login'), form);
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
export function autoBindForm(
|
|
218
|
+
formElement: HTMLFormElement,
|
|
219
|
+
store: FormStore,
|
|
220
|
+
options: BindFormOptions = {}
|
|
221
|
+
): () => void {
|
|
222
|
+
const { preventDefault = true, ...fieldOptions } = options;
|
|
223
|
+
const cleanups: (() => void)[] = [];
|
|
224
|
+
|
|
225
|
+
// Find all elements with data-kayform attribute
|
|
226
|
+
const elements = formElement.querySelectorAll<HTMLElement>("[data-kayform]");
|
|
227
|
+
|
|
228
|
+
for (const el of elements) {
|
|
229
|
+
const path = el.getAttribute("data-kayform");
|
|
230
|
+
if (!path) continue;
|
|
231
|
+
|
|
232
|
+
const field = store.getField(path);
|
|
233
|
+
const unbind = bindField(el, field, fieldOptions);
|
|
234
|
+
cleanups.push(unbind);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Handle form submit
|
|
238
|
+
const handleSubmit = (e: Event) => {
|
|
239
|
+
if (preventDefault) {
|
|
240
|
+
e.preventDefault();
|
|
241
|
+
}
|
|
242
|
+
store.submit();
|
|
243
|
+
};
|
|
244
|
+
formElement.addEventListener("submit", handleSubmit);
|
|
245
|
+
cleanups.push(() =>
|
|
246
|
+
formElement.removeEventListener("submit", handleSubmit)
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
return () => {
|
|
250
|
+
for (const cleanup of cleanups) {
|
|
251
|
+
cleanup();
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineConfig } from "tsup";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ["src/index.ts"],
|
|
5
|
+
format: ["esm", "cjs"],
|
|
6
|
+
dts: true,
|
|
7
|
+
clean: true,
|
|
8
|
+
sourcemap: true,
|
|
9
|
+
minify: false,
|
|
10
|
+
treeshake: true,
|
|
11
|
+
external: ["@kayforms/core"],
|
|
12
|
+
outExtension({ format }) {
|
|
13
|
+
return {
|
|
14
|
+
js: format === "cjs" ? ".cjs" : ".js",
|
|
15
|
+
};
|
|
16
|
+
},
|
|
17
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kayforms/vue",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Vue 3 adapter for Kayforms reactive form library",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": ["dist"],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsup",
|
|
24
|
+
"dev": "tsup --watch",
|
|
25
|
+
"clean": "rimraf dist",
|
|
26
|
+
"typecheck": "tsc --noEmit"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@kayforms/core": "workspace:*"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"vue": "^3.0.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"vue": "^3.4.0",
|
|
36
|
+
"tsup": "^8.0.0",
|
|
37
|
+
"typescript": "^5.5.0",
|
|
38
|
+
"rimraf": "^5.0.0"
|
|
39
|
+
},
|
|
40
|
+
"sideEffects": false,
|
|
41
|
+
"license": "MIT"
|
|
42
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// @kayforms/vue — Vue 3 Adapter
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Bridges Kayforms reactive signals to Vue's reactive ref system.
|
|
5
|
+
// Each useField() injects the parent form context and resolves field states.
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
inject,
|
|
10
|
+
provide,
|
|
11
|
+
shallowRef,
|
|
12
|
+
onUnmounted,
|
|
13
|
+
defineComponent,
|
|
14
|
+
type InjectionKey,
|
|
15
|
+
type ShallowRef,
|
|
16
|
+
} from "vue";
|
|
17
|
+
import {
|
|
18
|
+
createForm,
|
|
19
|
+
createEffect,
|
|
20
|
+
type FormConfig,
|
|
21
|
+
type FormStore,
|
|
22
|
+
type FormErrors,
|
|
23
|
+
type FieldNode,
|
|
24
|
+
type Signal,
|
|
25
|
+
type Computed,
|
|
26
|
+
} from "@kayforms/core";
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Signal → Vue Ref bridge
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Hook to convert a Kayforms Signal or Computed to a Vue Ref.
|
|
34
|
+
* Subscribes to the signal and updates the ref value, cleaning up on unmount.
|
|
35
|
+
*/
|
|
36
|
+
export function useSignalValue<T>(signal: Signal<T> | Computed<T>): ShallowRef<T> {
|
|
37
|
+
const r = shallowRef<T>(signal.peek());
|
|
38
|
+
const dispose = createEffect(() => {
|
|
39
|
+
r.value = signal.value;
|
|
40
|
+
});
|
|
41
|
+
onUnmounted(dispose);
|
|
42
|
+
return r;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Injection Context
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
const FORM_KEY: InjectionKey<FormStore> = Symbol("kayforms");
|
|
50
|
+
|
|
51
|
+
/** Provide form context in a parent component */
|
|
52
|
+
export function provideForm(store: FormStore) {
|
|
53
|
+
provide(FORM_KEY, store);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Inject form context in a child component */
|
|
57
|
+
export function useFormContext(): FormStore {
|
|
58
|
+
const store = inject(FORM_KEY);
|
|
59
|
+
if (!store) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
"[kayforms/vue] useField must be used inside a form context. " +
|
|
62
|
+
"Wrap your form with <FormProvider :form=\"...\"> or call provideForm(form.store)."
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return store;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// useForm
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
export interface UseFormReturn<T extends Record<string, unknown>> {
|
|
73
|
+
/** The reactive form store */
|
|
74
|
+
store: FormStore<T>;
|
|
75
|
+
/** Current values (reactive) */
|
|
76
|
+
values: ShallowRef<T>;
|
|
77
|
+
/** Current errors (reactive) */
|
|
78
|
+
errors: ShallowRef<FormErrors<T>>;
|
|
79
|
+
/** Whether the form is dirty */
|
|
80
|
+
dirty: ShallowRef<boolean>;
|
|
81
|
+
/** Whether the form is valid */
|
|
82
|
+
valid: ShallowRef<boolean>;
|
|
83
|
+
/** Whether the form is submitting */
|
|
84
|
+
submitting: ShallowRef<boolean>;
|
|
85
|
+
/** Whether any async validation is running */
|
|
86
|
+
validating: ShallowRef<boolean>;
|
|
87
|
+
/** Handle form submission */
|
|
88
|
+
handleSubmit: (e?: Event) => void;
|
|
89
|
+
/** Reset the form */
|
|
90
|
+
reset: (values?: Partial<T>) => void;
|
|
91
|
+
/** Get a field node by path */
|
|
92
|
+
getField: <V = unknown>(path: string) => FieldNode<V>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create and manage a Kayforms form in a Vue component.
|
|
97
|
+
*/
|
|
98
|
+
export function useForm<T extends Record<string, unknown>>(
|
|
99
|
+
config: FormConfig<T>
|
|
100
|
+
): UseFormReturn<T> {
|
|
101
|
+
const store = createForm(config);
|
|
102
|
+
|
|
103
|
+
onUnmounted(() => {
|
|
104
|
+
store.dispose();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const values = useSignalValue(store.values);
|
|
108
|
+
const errors = useSignalValue(store.errors);
|
|
109
|
+
const dirty = useSignalValue(store.dirty);
|
|
110
|
+
const valid = useSignalValue(store.valid);
|
|
111
|
+
const submitting = useSignalValue(store.submitting);
|
|
112
|
+
const validating = useSignalValue(store.validating);
|
|
113
|
+
|
|
114
|
+
const handleSubmit = (e?: Event) => {
|
|
115
|
+
e?.preventDefault();
|
|
116
|
+
store.submit();
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const reset = (newValues?: Partial<T>) => {
|
|
120
|
+
store.reset(newValues);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
store,
|
|
125
|
+
values,
|
|
126
|
+
errors,
|
|
127
|
+
dirty,
|
|
128
|
+
valid,
|
|
129
|
+
submitting,
|
|
130
|
+
validating,
|
|
131
|
+
handleSubmit,
|
|
132
|
+
reset,
|
|
133
|
+
getField: store.getField,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// useField
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
export interface UseFieldReturn<V = unknown> {
|
|
142
|
+
/** Current field value */
|
|
143
|
+
value: ShallowRef<V>;
|
|
144
|
+
/** Current error message (undefined = valid) */
|
|
145
|
+
error: ShallowRef<string | undefined>;
|
|
146
|
+
/** Whether the field has been touched */
|
|
147
|
+
touched: ShallowRef<boolean>;
|
|
148
|
+
/** Whether the field value differs from initial */
|
|
149
|
+
dirty: ShallowRef<boolean>;
|
|
150
|
+
/** Whether async validation is in progress */
|
|
151
|
+
validating: ShallowRef<boolean>;
|
|
152
|
+
/** Update the field value */
|
|
153
|
+
onChange: (value: V) => void;
|
|
154
|
+
/** Mark as touched */
|
|
155
|
+
onBlur: () => void;
|
|
156
|
+
/** Input props for binding */
|
|
157
|
+
inputProps: {
|
|
158
|
+
value: V;
|
|
159
|
+
onInput: (e: Event) => void;
|
|
160
|
+
onBlur: () => void;
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Subscribe to a single field's reactive state in a Vue component.
|
|
166
|
+
*/
|
|
167
|
+
export function useField<V = unknown>(path: string): UseFieldReturn<V> {
|
|
168
|
+
const store = useFormContext();
|
|
169
|
+
const field = store.getField<V>(path);
|
|
170
|
+
|
|
171
|
+
const value = useSignalValue(field.value);
|
|
172
|
+
const error = useSignalValue(field.error);
|
|
173
|
+
const touched = useSignalValue(field.touched);
|
|
174
|
+
const dirty = useSignalValue(field.dirty);
|
|
175
|
+
const validating = useSignalValue(field.validating);
|
|
176
|
+
|
|
177
|
+
const onChange = (v: V) => field.onChange(v);
|
|
178
|
+
const onBlur = () => field.onBlur();
|
|
179
|
+
|
|
180
|
+
const handleInput = (e: Event) => {
|
|
181
|
+
const val = (e.target as HTMLInputElement).value;
|
|
182
|
+
field.onChange(val as unknown as V);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
value,
|
|
187
|
+
error,
|
|
188
|
+
touched,
|
|
189
|
+
dirty,
|
|
190
|
+
validating,
|
|
191
|
+
onChange,
|
|
192
|
+
onBlur,
|
|
193
|
+
inputProps: {
|
|
194
|
+
value: value.value,
|
|
195
|
+
onInput: handleInput,
|
|
196
|
+
onBlur,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// FormProvider
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
export const FormProvider = defineComponent({
|
|
206
|
+
name: "FormProvider",
|
|
207
|
+
props: {
|
|
208
|
+
form: {
|
|
209
|
+
type: Object as () => FormStore<any>,
|
|
210
|
+
required: true,
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
setup(props, { slots }) {
|
|
214
|
+
provideForm(props.form);
|
|
215
|
+
return () => slots.default?.();
|
|
216
|
+
},
|
|
217
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineConfig } from "tsup";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ["src/index.ts"],
|
|
5
|
+
format: ["esm", "cjs"],
|
|
6
|
+
dts: true,
|
|
7
|
+
clean: true,
|
|
8
|
+
sourcemap: true,
|
|
9
|
+
minify: false,
|
|
10
|
+
treeshake: true,
|
|
11
|
+
external: ["vue", "@kayforms/core"],
|
|
12
|
+
outExtension({ format }) {
|
|
13
|
+
return {
|
|
14
|
+
js: format === "cjs" ? ".cjs" : ".js",
|
|
15
|
+
};
|
|
16
|
+
},
|
|
17
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"noUnusedLocals": true,
|
|
17
|
+
"noUnusedParameters": true,
|
|
18
|
+
"noFallthroughCasesInSwitch": true,
|
|
19
|
+
"jsx": "react-jsx"
|
|
20
|
+
}
|
|
21
|
+
}
|