@playfast/forms 0.0.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/README.md +125 -0
- package/dist/cjs/form.js +298 -0
- package/dist/cjs/index.js +56 -0
- package/dist/cjs/package.json +4 -0
- package/dist/cjs/path.js +115 -0
- package/dist/cjs/validation.js +40 -0
- package/dist/dts/form.d.ts +184 -0
- package/dist/dts/form.d.ts.map +1 -0
- package/dist/dts/index.d.ts +8 -0
- package/dist/dts/index.d.ts.map +1 -0
- package/dist/dts/path.d.ts +36 -0
- package/dist/dts/path.d.ts.map +1 -0
- package/dist/dts/validation.d.ts +12 -0
- package/dist/dts/validation.d.ts.map +1 -0
- package/dist/esm/form.js +292 -0
- package/dist/esm/form.js.map +1 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +4 -0
- package/dist/esm/path.js +104 -0
- package/dist/esm/path.js.map +1 -0
- package/dist/esm/validation.js +33 -0
- package/dist/esm/validation.js.map +1 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# `@reform/forms`
|
|
2
|
+
|
|
3
|
+
Headless form state for Reform. This package owns values, validation, field
|
|
4
|
+
metadata, list operations, submit flow, and correlations with other Reform
|
|
5
|
+
state. It renders nothing and has no DOM or React API.
|
|
6
|
+
|
|
7
|
+
## Model
|
|
8
|
+
|
|
9
|
+
Forms follow the same definition / implementation split as the rest of Reform:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { Schema as S } from 'effect'
|
|
13
|
+
import { Form } from '@reform/forms'
|
|
14
|
+
import { CurrentUser, Settings } from './sources'
|
|
15
|
+
|
|
16
|
+
const CheckoutValues = S.Struct({
|
|
17
|
+
email: S.String,
|
|
18
|
+
items: S.Array(S.Struct({ name: S.String, qty: S.Number })),
|
|
19
|
+
payment: S.Union(
|
|
20
|
+
S.TaggedStruct('Card', { cardNumber: S.String, cvv: S.String }),
|
|
21
|
+
S.TaggedStruct('Paypal', { email: S.String }),
|
|
22
|
+
),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
class CheckoutForm extends Form.make('CheckoutForm', {
|
|
26
|
+
schema: CheckoutValues,
|
|
27
|
+
inputs: { user: CurrentUser, settings: Settings },
|
|
28
|
+
}) {}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`Form.make` creates a reflectable form definition. `Form.live` provides the
|
|
32
|
+
runtime behavior:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
const CheckoutFormLive = Form.live(CheckoutForm, {
|
|
36
|
+
initial: {
|
|
37
|
+
email: '',
|
|
38
|
+
items: [],
|
|
39
|
+
payment: { _tag: 'Card', cardNumber: '', cvv: '' },
|
|
40
|
+
},
|
|
41
|
+
limit: ({ inputs }) => ({
|
|
42
|
+
email: { required: true, disabled: !inputs.settings.emailEditable },
|
|
43
|
+
items: { minItems: 1, maxItems: inputs.settings.maxItems },
|
|
44
|
+
}),
|
|
45
|
+
validate: ({ values }) =>
|
|
46
|
+
values.email.includes('@')
|
|
47
|
+
? undefined
|
|
48
|
+
: Form.error('email', 'Email must contain @'),
|
|
49
|
+
submit: ({ decoded, inputs }) => saveCheckout(decoded, inputs.user.id),
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The form is read inside compositions with `Form.view(CheckoutForm)`. The view is
|
|
54
|
+
a typed, platform-neutral object:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const form = yield* Form.view(CheckoutForm)
|
|
58
|
+
|
|
59
|
+
form.field('email').set('a@example.com')
|
|
60
|
+
form.array('items').append({ name: 'Milk', qty: 1 })
|
|
61
|
+
form.submit()
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Correlations
|
|
65
|
+
|
|
66
|
+
`inputs` connects a form to other Reform state or calculations. Those inputs are
|
|
67
|
+
read through the same tracking mechanism as normal composition reads, so a view
|
|
68
|
+
that depends on the form also updates when form limitations or validation context
|
|
69
|
+
change.
|
|
70
|
+
|
|
71
|
+
Use this for permissions, calculated totals, feature flags, remote defaults, or
|
|
72
|
+
cross-field constraints that come from outside the form values.
|
|
73
|
+
|
|
74
|
+
## Limitations
|
|
75
|
+
|
|
76
|
+
`limit` returns field metadata keyed by form path. The core does not interpret
|
|
77
|
+
metadata as DOM attributes; it simply carries constraints to any renderer:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
limit: ({ values }) => ({
|
|
81
|
+
email: { required: true, maxLength: 120 },
|
|
82
|
+
'payment.cardNumber': {
|
|
83
|
+
visible: values.payment._tag === 'Card',
|
|
84
|
+
meta: { mask: 'card' },
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Renderers and custom controls decide how to display these limitations.
|
|
90
|
+
|
|
91
|
+
## Paths
|
|
92
|
+
|
|
93
|
+
Path strings are type checked from the Effect Schema encoded value:
|
|
94
|
+
|
|
95
|
+
- `field('email')`
|
|
96
|
+
- `field('items[0].qty')`
|
|
97
|
+
- `array('items')`
|
|
98
|
+
- `variantValue('payment')`
|
|
99
|
+
|
|
100
|
+
Invalid paths fail at compile time when used through the typed helpers.
|
|
101
|
+
|
|
102
|
+
## Lists
|
|
103
|
+
|
|
104
|
+
Array bindings expose stable keys and item operations:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
const items = form.array('items')
|
|
108
|
+
items.append({ name: '', qty: 1 })
|
|
109
|
+
items.move(0, 1)
|
|
110
|
+
items.remove(0)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Each item has `{ key, index, value, remove, move }`, so UI layers can map rows
|
|
114
|
+
without owning form state.
|
|
115
|
+
|
|
116
|
+
## Validation And Submit
|
|
117
|
+
|
|
118
|
+
`submit()` first decodes the Effect Schema, then runs custom validation. Schema
|
|
119
|
+
parse issues and custom errors both route into field errors. `canSubmit` is a
|
|
120
|
+
derived read from the current decoded state and stored errors; it is advisory UI
|
|
121
|
+
state, not a substitute for submit-time validation.
|
|
122
|
+
|
|
123
|
+
`validate()` runs validation without attempting submit. `reset()` restores the
|
|
124
|
+
initial values.
|
|
125
|
+
|
package/dist/cjs/form.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.view = exports.live = exports.make = exports.arrayKeysFor = exports.error = void 0;
|
|
4
|
+
const effect_1 = require("effect");
|
|
5
|
+
const reform_1 = require("reform");
|
|
6
|
+
const path_js_1 = require("./path.js");
|
|
7
|
+
const validation_js_1 = require("./validation.js");
|
|
8
|
+
Object.defineProperty(exports, "error", { enumerable: true, get: function () { return validation_js_1.error; } });
|
|
9
|
+
const unknownSchema = effect_1.Schema.Unknown;
|
|
10
|
+
let nextArrayKey = 0;
|
|
11
|
+
const makeArrayKey = () => `form-item-${nextArrayKey++}`;
|
|
12
|
+
const arrayKeysFor = (value, path = '') => {
|
|
13
|
+
const out = {};
|
|
14
|
+
const visit = (current, currentPath) => {
|
|
15
|
+
if (Array.isArray(current)) {
|
|
16
|
+
out[currentPath] = current.map(() => makeArrayKey());
|
|
17
|
+
current.forEach((item, index) => visit(item, `${currentPath}[${index}]`));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (current !== null && typeof current === 'object') {
|
|
21
|
+
for (const [key, child] of Object.entries(current)) {
|
|
22
|
+
visit(child, currentPath.length === 0 ? key : `${currentPath}.${key}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
visit(value, path);
|
|
27
|
+
return out;
|
|
28
|
+
};
|
|
29
|
+
exports.arrayKeysFor = arrayKeysFor;
|
|
30
|
+
const initialState = (initial) => ({
|
|
31
|
+
values: initial,
|
|
32
|
+
initialValues: initial,
|
|
33
|
+
touched: {},
|
|
34
|
+
errors: {},
|
|
35
|
+
dirtyPaths: [],
|
|
36
|
+
submitCount: 0,
|
|
37
|
+
validationCount: 0,
|
|
38
|
+
lastSubmittedValues: effect_1.Option.none(),
|
|
39
|
+
arrayKeys: (0, exports.arrayKeysFor)(initial),
|
|
40
|
+
});
|
|
41
|
+
const markTouched = (touched, path) => ({ ...touched, [path]: true });
|
|
42
|
+
const withValues = (state, values) => ({
|
|
43
|
+
...state,
|
|
44
|
+
values,
|
|
45
|
+
dirtyPaths: (0, path_js_1.recalculateDirtyPaths)(state.initialValues, values),
|
|
46
|
+
});
|
|
47
|
+
const currentArray = (values, path) => {
|
|
48
|
+
const current = (0, path_js_1.getNestedValue)(values, path);
|
|
49
|
+
return Array.isArray(current) ? current : [];
|
|
50
|
+
};
|
|
51
|
+
const updateKeys = (state, path, f) => {
|
|
52
|
+
const existing = state.arrayKeys[path] ?? currentArray(state.values, path).map(() => makeArrayKey());
|
|
53
|
+
return { ...state.arrayKeys, [path]: f(existing) };
|
|
54
|
+
};
|
|
55
|
+
const setAtPath = (state, path, value) => withValues(state, (0, path_js_1.setNestedValue)(state.values, path, value));
|
|
56
|
+
const appendAtPath = (state, path, value) => {
|
|
57
|
+
const items = currentArray(state.values, path);
|
|
58
|
+
const values = (0, path_js_1.setNestedValue)(state.values, path, [...items, value]);
|
|
59
|
+
return {
|
|
60
|
+
...withValues(state, values),
|
|
61
|
+
arrayKeys: updateKeys(state, path, (keys) => [...keys, makeArrayKey()]),
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
const removeAtPath = (state, path, index) => {
|
|
65
|
+
const items = currentArray(state.values, path);
|
|
66
|
+
if (index < 0 || index >= items.length)
|
|
67
|
+
return state;
|
|
68
|
+
const values = (0, path_js_1.setNestedValue)(state.values, path, items.filter((_, i) => i !== index));
|
|
69
|
+
return {
|
|
70
|
+
...withValues(state, values),
|
|
71
|
+
arrayKeys: updateKeys(state, path, (keys) => keys.filter((_, i) => i !== index)),
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
const moveAtPath = (state, path, from, to) => {
|
|
75
|
+
const items = currentArray(state.values, path);
|
|
76
|
+
const next = (0, path_js_1.moveAt)(items, from, to);
|
|
77
|
+
if (next === items)
|
|
78
|
+
return state;
|
|
79
|
+
const values = (0, path_js_1.setNestedValue)(state.values, path, next);
|
|
80
|
+
return {
|
|
81
|
+
...withValues(state, values),
|
|
82
|
+
arrayKeys: updateKeys(state, path, (keys) => (0, path_js_1.moveAt)(keys, from, to)),
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
const swapAtPath = (state, path, a, b) => {
|
|
86
|
+
const items = currentArray(state.values, path);
|
|
87
|
+
if (a < 0 || b < 0 || a >= items.length || b >= items.length || a === b)
|
|
88
|
+
return state;
|
|
89
|
+
const swapped = (0, path_js_1.replaceAt)((0, path_js_1.replaceAt)(items, a, items[b]), b, items[a]);
|
|
90
|
+
const values = (0, path_js_1.setNestedValue)(state.values, path, swapped);
|
|
91
|
+
return {
|
|
92
|
+
...withValues(state, values),
|
|
93
|
+
arrayKeys: updateKeys(state, path, (keys) => (0, path_js_1.replaceAt)((0, path_js_1.replaceAt)(keys, a, keys[b] ?? makeArrayKey()), b, keys[a] ?? makeArrayKey())),
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
const make = (name, config) => {
|
|
97
|
+
const state = reform_1.State.make(`${name}/state`, unknownSchema, {
|
|
98
|
+
title: `${name} form state`,
|
|
99
|
+
});
|
|
100
|
+
const events = {
|
|
101
|
+
set: reform_1.Event.make(`${name}/SetField`, effect_1.Schema.Struct({ path: effect_1.Schema.String, value: effect_1.Schema.Unknown })),
|
|
102
|
+
blur: reform_1.Event.make(`${name}/BlurField`, effect_1.Schema.Struct({ path: effect_1.Schema.String })),
|
|
103
|
+
reset: reform_1.Event.make(`${name}/Reset`, effect_1.Schema.Struct({})),
|
|
104
|
+
validationFinished: reform_1.Event.make(`${name}/ValidationFinished`, effect_1.Schema.Struct({
|
|
105
|
+
errors: effect_1.Schema.Record({ key: effect_1.Schema.String, value: effect_1.Schema.String }),
|
|
106
|
+
})),
|
|
107
|
+
submitAttempted: reform_1.Event.make(`${name}/SubmitAttempted`, effect_1.Schema.Struct({})),
|
|
108
|
+
submitSucceeded: reform_1.Event.make(`${name}/SubmitSucceeded`, effect_1.Schema.Struct({ values: effect_1.Schema.Unknown })),
|
|
109
|
+
append: reform_1.Event.make(`${name}/AppendItem`, effect_1.Schema.Struct({ path: effect_1.Schema.String, value: effect_1.Schema.Unknown })),
|
|
110
|
+
remove: reform_1.Event.make(`${name}/RemoveItem`, effect_1.Schema.Struct({ path: effect_1.Schema.String, index: effect_1.Schema.Number })),
|
|
111
|
+
move: reform_1.Event.make(`${name}/MoveItem`, effect_1.Schema.Struct({ path: effect_1.Schema.String, from: effect_1.Schema.Number, to: effect_1.Schema.Number })),
|
|
112
|
+
swap: reform_1.Event.make(`${name}/SwapItems`, effect_1.Schema.Struct({ path: effect_1.Schema.String, a: effect_1.Schema.Number, b: effect_1.Schema.Number })),
|
|
113
|
+
};
|
|
114
|
+
const reducer = reform_1.Reducer.make(`${name}/FormReducer`, {
|
|
115
|
+
states: [state],
|
|
116
|
+
events: Object.values(events),
|
|
117
|
+
});
|
|
118
|
+
const runtimeConfig = effect_1.Context.GenericTag(`reform/form/${name}/config`);
|
|
119
|
+
const inputs = (config.inputs ?? {});
|
|
120
|
+
return Object.assign(class {
|
|
121
|
+
}, {
|
|
122
|
+
manifest: {
|
|
123
|
+
kind: 'Form',
|
|
124
|
+
name,
|
|
125
|
+
schema: config.schema,
|
|
126
|
+
inputs,
|
|
127
|
+
},
|
|
128
|
+
state,
|
|
129
|
+
config: runtimeConfig,
|
|
130
|
+
events,
|
|
131
|
+
reducer,
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
exports.make = make;
|
|
135
|
+
const live = (form, config) => {
|
|
136
|
+
const configTag = form.config;
|
|
137
|
+
const configLayer = effect_1.Layer.effect(configTag, effect_1.Effect.map(effect_1.Effect.context(), (context) => {
|
|
138
|
+
const runtimeConfig = {
|
|
139
|
+
initial: config.initial,
|
|
140
|
+
validate: (ctx) => {
|
|
141
|
+
const result = config.validate?.(ctx);
|
|
142
|
+
const effect = effect_1.Effect.isEffect(result) ? result : effect_1.Effect.succeed(result);
|
|
143
|
+
return effect_1.Effect.map(effect_1.Effect.provide(effect, context), (value) => (0, validation_js_1.normalizeErrors)(value));
|
|
144
|
+
},
|
|
145
|
+
submit: (ctx) => {
|
|
146
|
+
const result = config.submit?.(ctx);
|
|
147
|
+
const effect = effect_1.Effect.isEffect(result) ? result : effect_1.Effect.succeed(result);
|
|
148
|
+
return effect_1.Effect.asVoid(effect_1.Effect.provide(effect, context));
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
return config.limit === undefined
|
|
152
|
+
? runtimeConfig
|
|
153
|
+
: { ...runtimeConfig, limit: config.limit };
|
|
154
|
+
}));
|
|
155
|
+
const reducerLayer = reform_1.Reducer.live(form.reducer, (state, event) => {
|
|
156
|
+
switch (event._tag) {
|
|
157
|
+
case form.events.set.tag:
|
|
158
|
+
return 'path' in event && 'value' in event ? setAtPath(state, event.path, event.value) : state;
|
|
159
|
+
case form.events.blur.tag:
|
|
160
|
+
return 'path' in event ? { ...state, touched: markTouched(state.touched, event.path) } : state;
|
|
161
|
+
case form.events.reset.tag:
|
|
162
|
+
return initialState(state.initialValues);
|
|
163
|
+
case form.events.validationFinished.tag:
|
|
164
|
+
return 'errors' in event ? { ...state, errors: event.errors, validationCount: state.validationCount + 1 } : state;
|
|
165
|
+
case form.events.submitAttempted.tag:
|
|
166
|
+
return { ...state, submitCount: state.submitCount + 1 };
|
|
167
|
+
case form.events.submitSucceeded.tag:
|
|
168
|
+
return 'values' in event ? { ...state, lastSubmittedValues: effect_1.Option.some(event.values) } : state;
|
|
169
|
+
case form.events.append.tag:
|
|
170
|
+
return 'path' in event && 'value' in event ? appendAtPath(state, event.path, event.value) : state;
|
|
171
|
+
case form.events.remove.tag:
|
|
172
|
+
return 'path' in event && 'index' in event ? removeAtPath(state, event.path, event.index) : state;
|
|
173
|
+
case form.events.move.tag:
|
|
174
|
+
return 'path' in event && 'from' in event && 'to' in event ? moveAtPath(state, event.path, event.from, event.to) : state;
|
|
175
|
+
case form.events.swap.tag:
|
|
176
|
+
return 'path' in event && 'a' in event && 'b' in event ? swapAtPath(state, event.path, event.a, event.b) : state;
|
|
177
|
+
default:
|
|
178
|
+
return state;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
return effect_1.Layer.mergeAll(configLayer, reducerLayer).pipe(effect_1.Layer.provideMerge(reform_1.State.live(form.state, initialState(config.initial))));
|
|
182
|
+
};
|
|
183
|
+
exports.live = live;
|
|
184
|
+
const readInput = (source) => effect_1.Effect.gen(function* () {
|
|
185
|
+
const store = yield* source.store;
|
|
186
|
+
const tracker = yield* effect_1.Effect.serviceOption(reform_1.CurrentTracker);
|
|
187
|
+
if (effect_1.Option.isSome(tracker))
|
|
188
|
+
tracker.value.add(store);
|
|
189
|
+
return store.getSnapshot();
|
|
190
|
+
});
|
|
191
|
+
const readInputs = (inputs) => effect_1.Effect.gen(function* () {
|
|
192
|
+
const out = {};
|
|
193
|
+
for (const source of Object.values(inputs)) {
|
|
194
|
+
out[source.name] = yield* readInput(source);
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
});
|
|
198
|
+
const limitationsFor = (limitations, path) => (limitations[path] ?? {});
|
|
199
|
+
const trigger = (eventTrigger) => eventTrigger;
|
|
200
|
+
const view = (form) => effect_1.Effect.gen(function* () {
|
|
201
|
+
const state = yield* form.state;
|
|
202
|
+
const runtimeConfig = yield* form.config;
|
|
203
|
+
// Erased boundary (same shape as the casts closing `live`/`readInputs`):
|
|
204
|
+
// under the `AnyForm` constraint `form.manifest.inputs` is the wildcard
|
|
205
|
+
// `any`, which would dissolve the generator's R to `unknown`; restate the
|
|
206
|
+
// call at the concrete `F`'s types so R keeps the declared input stores.
|
|
207
|
+
const inputs = (yield* readInputs(form.manifest.inputs));
|
|
208
|
+
const limitations = runtimeConfig.limit?.({ values: state.values, inputs }) ?? {};
|
|
209
|
+
const setField = yield* trigger(form.events.set.trigger);
|
|
210
|
+
const blurField = yield* trigger(form.events.blur.trigger);
|
|
211
|
+
const reset = yield* trigger(form.events.reset.trigger);
|
|
212
|
+
const append = yield* trigger(form.events.append.trigger);
|
|
213
|
+
const remove = yield* trigger(form.events.remove.trigger);
|
|
214
|
+
const move = yield* trigger(form.events.move.trigger);
|
|
215
|
+
const swap = yield* trigger(form.events.swap.trigger);
|
|
216
|
+
const runtime = yield* effect_1.Effect.runtime();
|
|
217
|
+
const schema = form.manifest.schema;
|
|
218
|
+
const decodeEither = effect_1.Schema.decodeUnknownEither(schema, { errors: 'all' })(state.values);
|
|
219
|
+
const canSubmit = effect_1.Either.isRight(decodeEither) && Object.keys(state.errors).length === 0;
|
|
220
|
+
const runValidation = (publishSubmitAttempt) => effect_1.Effect.gen(function* () {
|
|
221
|
+
if (publishSubmitAttempt)
|
|
222
|
+
yield* reform_1.Event.dispatch(form.events.submitAttempted, {});
|
|
223
|
+
const decoded = effect_1.Schema.decodeUnknownEither(schema, { errors: 'all' })(state.values);
|
|
224
|
+
if (effect_1.Either.isLeft(decoded)) {
|
|
225
|
+
yield* reform_1.Event.dispatch(form.events.validationFinished, { errors: (0, validation_js_1.routeParseError)(decoded.left) });
|
|
226
|
+
return effect_1.Option.none();
|
|
227
|
+
}
|
|
228
|
+
const custom = yield* runtimeConfig.validate({ values: state.values, decoded: decoded.right, inputs });
|
|
229
|
+
const errors = (0, validation_js_1.errorsToRecord)(custom);
|
|
230
|
+
yield* reform_1.Event.dispatch(form.events.validationFinished, { errors });
|
|
231
|
+
return Object.keys(errors).length === 0 ? effect_1.Option.some(decoded.right) : effect_1.Option.none();
|
|
232
|
+
});
|
|
233
|
+
const submit = () => {
|
|
234
|
+
effect_1.Runtime.runFork(runtime)(effect_1.Effect.gen(function* () {
|
|
235
|
+
const decoded = yield* runValidation(true);
|
|
236
|
+
if (effect_1.Option.isNone(decoded))
|
|
237
|
+
return;
|
|
238
|
+
yield* runtimeConfig.submit({ values: state.values, decoded: decoded.value, inputs });
|
|
239
|
+
yield* reform_1.Event.dispatch(form.events.submitSucceeded, { values: state.values });
|
|
240
|
+
}).pipe(effect_1.Effect.catchAllCause((cause) => effect_1.Effect.logError('reform form submit failed', cause))));
|
|
241
|
+
};
|
|
242
|
+
const validate = () => {
|
|
243
|
+
effect_1.Runtime.runFork(runtime)(runValidation(false));
|
|
244
|
+
};
|
|
245
|
+
const field = (path) => {
|
|
246
|
+
const value = (0, path_js_1.getNestedValue)(state.values, path);
|
|
247
|
+
return {
|
|
248
|
+
path,
|
|
249
|
+
value,
|
|
250
|
+
set: (next) => setField({
|
|
251
|
+
path,
|
|
252
|
+
value: typeof next === 'function' ? next(value) : next,
|
|
253
|
+
}),
|
|
254
|
+
blur: () => blurField({ path }),
|
|
255
|
+
error: (0, validation_js_1.firstError)(state.errors, path),
|
|
256
|
+
dirty: (0, path_js_1.isPathOrParentDirty)(state.dirtyPaths, path),
|
|
257
|
+
touched: state.touched[path] === true,
|
|
258
|
+
validating: false,
|
|
259
|
+
limitations: limitationsFor(limitations, path),
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
const array = (path) => {
|
|
263
|
+
const values = currentArray(state.values, path);
|
|
264
|
+
const keys = state.arrayKeys[path] ?? values.map((_, i) => `${path}-${i}`);
|
|
265
|
+
return {
|
|
266
|
+
path,
|
|
267
|
+
items: values.map((value, index) => ({
|
|
268
|
+
value,
|
|
269
|
+
index,
|
|
270
|
+
key: keys[index] ?? `${path}-${index}`,
|
|
271
|
+
remove: () => remove({ path, index }),
|
|
272
|
+
move: (to) => move({ path, from: index, to }),
|
|
273
|
+
})),
|
|
274
|
+
append: (value) => append({ path, value }),
|
|
275
|
+
remove: (index) => remove({ path, index }),
|
|
276
|
+
move: (from, to) => move({ path, from, to }),
|
|
277
|
+
swap: (a, b) => swap({ path, a, b }),
|
|
278
|
+
limitations: limitationsFor(limitations, path),
|
|
279
|
+
};
|
|
280
|
+
};
|
|
281
|
+
return {
|
|
282
|
+
values: state.values,
|
|
283
|
+
inputs,
|
|
284
|
+
errors: state.errors,
|
|
285
|
+
dirty: state.dirtyPaths.length > 0,
|
|
286
|
+
canSubmit,
|
|
287
|
+
submitCount: state.submitCount,
|
|
288
|
+
validationCount: state.validationCount,
|
|
289
|
+
lastSubmittedValues: state.lastSubmittedValues,
|
|
290
|
+
submit,
|
|
291
|
+
reset: () => reset({}),
|
|
292
|
+
validate,
|
|
293
|
+
field,
|
|
294
|
+
array,
|
|
295
|
+
variantValue: (path) => (0, path_js_1.getNestedValue)(state.values, path),
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
exports.view = view;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.routeParseError = exports.normalizeErrors = exports.firstError = exports.errorsToRecord = exports.setNestedValue = exports.schemaPathToFieldPath = exports.recalculateDirtyPaths = exports.moveAt = exports.joinPath = exports.isPathOrParentDirty = exports.getNestedValue = exports.view = exports.make = exports.live = exports.error = exports.unsafeArrayKeysFor = exports.Form = void 0;
|
|
37
|
+
exports.Form = __importStar(require("./form.js"));
|
|
38
|
+
var form_js_1 = require("./form.js");
|
|
39
|
+
Object.defineProperty(exports, "unsafeArrayKeysFor", { enumerable: true, get: function () { return form_js_1.arrayKeysFor; } });
|
|
40
|
+
Object.defineProperty(exports, "error", { enumerable: true, get: function () { return form_js_1.error; } });
|
|
41
|
+
Object.defineProperty(exports, "live", { enumerable: true, get: function () { return form_js_1.live; } });
|
|
42
|
+
Object.defineProperty(exports, "make", { enumerable: true, get: function () { return form_js_1.make; } });
|
|
43
|
+
Object.defineProperty(exports, "view", { enumerable: true, get: function () { return form_js_1.view; } });
|
|
44
|
+
var path_js_1 = require("./path.js");
|
|
45
|
+
Object.defineProperty(exports, "getNestedValue", { enumerable: true, get: function () { return path_js_1.getNestedValue; } });
|
|
46
|
+
Object.defineProperty(exports, "isPathOrParentDirty", { enumerable: true, get: function () { return path_js_1.isPathOrParentDirty; } });
|
|
47
|
+
Object.defineProperty(exports, "joinPath", { enumerable: true, get: function () { return path_js_1.joinPath; } });
|
|
48
|
+
Object.defineProperty(exports, "moveAt", { enumerable: true, get: function () { return path_js_1.moveAt; } });
|
|
49
|
+
Object.defineProperty(exports, "recalculateDirtyPaths", { enumerable: true, get: function () { return path_js_1.recalculateDirtyPaths; } });
|
|
50
|
+
Object.defineProperty(exports, "schemaPathToFieldPath", { enumerable: true, get: function () { return path_js_1.schemaPathToFieldPath; } });
|
|
51
|
+
Object.defineProperty(exports, "setNestedValue", { enumerable: true, get: function () { return path_js_1.setNestedValue; } });
|
|
52
|
+
var validation_js_1 = require("./validation.js");
|
|
53
|
+
Object.defineProperty(exports, "errorsToRecord", { enumerable: true, get: function () { return validation_js_1.errorsToRecord; } });
|
|
54
|
+
Object.defineProperty(exports, "firstError", { enumerable: true, get: function () { return validation_js_1.firstError; } });
|
|
55
|
+
Object.defineProperty(exports, "normalizeErrors", { enumerable: true, get: function () { return validation_js_1.normalizeErrors; } });
|
|
56
|
+
Object.defineProperty(exports, "routeParseError", { enumerable: true, get: function () { return validation_js_1.routeParseError; } });
|
package/dist/cjs/path.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.moveAt = exports.replaceAt = exports.recalculateDirtyPaths = exports.setNestedValue = exports.getNestedValue = exports.isPathOrParentDirty = exports.isPathUnderRoot = exports.joinPath = exports.schemaPathToFieldPath = void 0;
|
|
4
|
+
const effect_1 = require("effect");
|
|
5
|
+
const BRACKET_NOTATION_REGEX = /\[(\d+)\]/g;
|
|
6
|
+
const schemaPathToFieldPath = (path) => {
|
|
7
|
+
if (path.length === 0)
|
|
8
|
+
return '';
|
|
9
|
+
let result = String(path[0]);
|
|
10
|
+
for (let i = 1; i < path.length; i++) {
|
|
11
|
+
const segment = path[i];
|
|
12
|
+
result += typeof segment === 'number' ? `[${segment}]` : `.${String(segment)}`;
|
|
13
|
+
}
|
|
14
|
+
return result;
|
|
15
|
+
};
|
|
16
|
+
exports.schemaPathToFieldPath = schemaPathToFieldPath;
|
|
17
|
+
const joinPath = (prefix, path) => prefix.length === 0 ? path : path.length === 0 ? prefix : `${prefix}.${path}`;
|
|
18
|
+
exports.joinPath = joinPath;
|
|
19
|
+
const isPathUnderRoot = (path, rootPath) => path === rootPath || path.startsWith(`${rootPath}.`) || path.startsWith(`${rootPath}[`);
|
|
20
|
+
exports.isPathUnderRoot = isPathUnderRoot;
|
|
21
|
+
const isPathOrParentDirty = (dirtyPaths, path) => {
|
|
22
|
+
if (dirtyPaths.includes(path))
|
|
23
|
+
return true;
|
|
24
|
+
let parent = path;
|
|
25
|
+
while (true) {
|
|
26
|
+
const lastDot = parent.lastIndexOf('.');
|
|
27
|
+
const lastBracket = parent.lastIndexOf('[');
|
|
28
|
+
const splitIndex = Math.max(lastDot, lastBracket);
|
|
29
|
+
if (splitIndex === -1)
|
|
30
|
+
return false;
|
|
31
|
+
parent = parent.substring(0, splitIndex);
|
|
32
|
+
if (dirtyPaths.includes(parent))
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
exports.isPathOrParentDirty = isPathOrParentDirty;
|
|
37
|
+
const pathParts = (path) => path === '' ? [] : path.replace(BRACKET_NOTATION_REGEX, '.$1').split('.');
|
|
38
|
+
const getNestedValue = (obj, path) => {
|
|
39
|
+
const parts = pathParts(path);
|
|
40
|
+
let current = obj;
|
|
41
|
+
for (const part of parts) {
|
|
42
|
+
if (current === null || current === undefined || typeof current !== 'object')
|
|
43
|
+
return undefined;
|
|
44
|
+
current = current[part];
|
|
45
|
+
}
|
|
46
|
+
return current;
|
|
47
|
+
};
|
|
48
|
+
exports.getNestedValue = getNestedValue;
|
|
49
|
+
const setNestedValue = (obj, path, value) => {
|
|
50
|
+
const parts = pathParts(path);
|
|
51
|
+
if (parts.length === 0)
|
|
52
|
+
return value;
|
|
53
|
+
const result = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
54
|
+
let current = result;
|
|
55
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
56
|
+
const part = parts[i];
|
|
57
|
+
if (part === undefined)
|
|
58
|
+
return result;
|
|
59
|
+
const next = current[part];
|
|
60
|
+
current[part] = Array.isArray(next) ? [...next] : { ...next };
|
|
61
|
+
current = current[part];
|
|
62
|
+
}
|
|
63
|
+
const last = parts[parts.length - 1];
|
|
64
|
+
if (last !== undefined)
|
|
65
|
+
current[last] = value;
|
|
66
|
+
return result;
|
|
67
|
+
};
|
|
68
|
+
exports.setNestedValue = setNestedValue;
|
|
69
|
+
const recalculateDirtyPaths = (initial, values, rootPath = '') => {
|
|
70
|
+
const dirty = new Set();
|
|
71
|
+
const visit = (current, original, path) => {
|
|
72
|
+
if (effect_1.Equal.equals(current, original))
|
|
73
|
+
return;
|
|
74
|
+
if (Array.isArray(current) || Array.isArray(original)) {
|
|
75
|
+
const currentItems = Array.isArray(current) ? current : [];
|
|
76
|
+
const originalItems = Array.isArray(original) ? original : [];
|
|
77
|
+
if (currentItems.length !== originalItems.length && path.length > 0)
|
|
78
|
+
dirty.add(path);
|
|
79
|
+
const length = Math.max(currentItems.length, originalItems.length);
|
|
80
|
+
for (let i = 0; i < length; i++) {
|
|
81
|
+
visit(currentItems[i], originalItems[i], path.length === 0 ? `[${i}]` : `${path}[${i}]`);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (current !== null &&
|
|
86
|
+
original !== null &&
|
|
87
|
+
typeof current === 'object' &&
|
|
88
|
+
typeof original === 'object') {
|
|
89
|
+
const currentRecord = current;
|
|
90
|
+
const originalRecord = original;
|
|
91
|
+
const keys = new Set([...Object.keys(currentRecord), ...Object.keys(originalRecord)]);
|
|
92
|
+
for (const key of keys) {
|
|
93
|
+
visit(currentRecord[key], originalRecord[key], path.length === 0 ? key : `${path}.${key}`);
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (path.length > 0)
|
|
98
|
+
dirty.add(path);
|
|
99
|
+
};
|
|
100
|
+
visit(rootPath.length === 0 ? values : (0, exports.getNestedValue)(values, rootPath), rootPath.length === 0 ? initial : (0, exports.getNestedValue)(initial, rootPath), rootPath);
|
|
101
|
+
return Array.from(dirty).sort();
|
|
102
|
+
};
|
|
103
|
+
exports.recalculateDirtyPaths = recalculateDirtyPaths;
|
|
104
|
+
const replaceAt = (values, index, value) => values.map((item, i) => (i === index ? value : item));
|
|
105
|
+
exports.replaceAt = replaceAt;
|
|
106
|
+
const moveAt = (values, from, to) => {
|
|
107
|
+
if (from < 0 || from >= values.length || to < 0 || to > values.length || from === to)
|
|
108
|
+
return values;
|
|
109
|
+
const next = [...values];
|
|
110
|
+
const item = next.splice(from, 1)[0];
|
|
111
|
+
if (item !== undefined)
|
|
112
|
+
next.splice(to, 0, item);
|
|
113
|
+
return next;
|
|
114
|
+
};
|
|
115
|
+
exports.moveAt = moveAt;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.firstError = exports.routeParseError = exports.errorsToRecord = exports.normalizeErrors = exports.error = void 0;
|
|
4
|
+
const effect_1 = require("effect");
|
|
5
|
+
const path_js_1 = require("./path.js");
|
|
6
|
+
const error = (path, message) => ({ path, message });
|
|
7
|
+
exports.error = error;
|
|
8
|
+
const normalizeErrors = (value) => {
|
|
9
|
+
if (value === undefined)
|
|
10
|
+
return [];
|
|
11
|
+
if (Array.isArray(value))
|
|
12
|
+
return value;
|
|
13
|
+
return [value];
|
|
14
|
+
};
|
|
15
|
+
exports.normalizeErrors = normalizeErrors;
|
|
16
|
+
const errorsToRecord = (errors) => {
|
|
17
|
+
const out = {};
|
|
18
|
+
for (const entry of errors) {
|
|
19
|
+
if (out[entry.path] === undefined)
|
|
20
|
+
out[entry.path] = entry.message;
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
};
|
|
24
|
+
exports.errorsToRecord = errorsToRecord;
|
|
25
|
+
const routeParseError = (parseError) => {
|
|
26
|
+
const formatted = effect_1.ParseResult.ArrayFormatter.formatErrorSync(parseError);
|
|
27
|
+
const out = {};
|
|
28
|
+
for (const issue of formatted) {
|
|
29
|
+
const path = (0, path_js_1.schemaPathToFieldPath)(issue.path);
|
|
30
|
+
if (out[path] === undefined)
|
|
31
|
+
out[path] = issue.message;
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
};
|
|
35
|
+
exports.routeParseError = routeParseError;
|
|
36
|
+
const firstError = (errors, path) => {
|
|
37
|
+
const message = errors[path];
|
|
38
|
+
return message === undefined ? effect_1.Option.none() : effect_1.Option.some(message);
|
|
39
|
+
};
|
|
40
|
+
exports.firstError = firstError;
|