-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Expand file tree
/
Copy pathform.js
More file actions
369 lines (320 loc) · 10.1 KB
/
form.js
File metadata and controls
369 lines (320 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
/** @import { RemoteFormInput, RemoteForm, InvalidField } from '@sveltejs/kit' */
/** @import { InternalRemoteFormIssue, MaybePromise, RemoteInfo } from 'types' */
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
import { get_request_store } from '@sveltejs/kit/internal/server';
import { DEV } from 'esm-env';
import {
create_field_proxy,
set_nested_value,
throw_on_old_property_access,
deep_set,
normalize_issue,
flatten_issues
} from '../../../form-utils.js';
import { get_cache, run_remote_function } from './shared.js';
import { ValidationError } from '@sveltejs/kit/internal';
/**
* Creates a form object that can be spread onto a `<form>` element.
*
* See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
*
* @template Output
* @overload
* @param {() => MaybePromise<Output>} fn
* @returns {RemoteForm<void, Output>}
* @since 2.27
*/
/**
* Creates a form object that can be spread onto a `<form>` element.
*
* See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
*
* @template {RemoteFormInput} Input
* @template Output
* @overload
* @param {'unchecked'} validate
* @param {(data: Input, issue: InvalidField<Input>) => MaybePromise<Output>} fn
* @returns {RemoteForm<Input, Output>}
* @since 2.27
*/
/**
* Creates a form object that can be spread onto a `<form>` element.
*
* See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
*
* @template {StandardSchemaV1<RemoteFormInput, Record<string, any>>} Schema
* @template Output
* @overload
* @param {Schema} validate
* @param {(data: StandardSchemaV1.InferOutput<Schema>, issue: InvalidField<StandardSchemaV1.InferInput<Schema>>) => MaybePromise<Output>} fn
* @returns {RemoteForm<StandardSchemaV1.InferInput<Schema>, Output>}
* @since 2.27
*/
/**
* @template {RemoteFormInput} Input
* @template Output
* @param {any} validate_or_fn
* @param {(data_or_issue: any, issue?: any) => MaybePromise<Output>} [maybe_fn]
* @returns {RemoteForm<Input, Output>}
* @since 2.27
*/
/*@__NO_SIDE_EFFECTS__*/
// @ts-ignore we don't want to prefix `fn` with an underscore, as that will be user-visible
export function form(validate_or_fn, maybe_fn) {
/** @type {any} */
const fn = maybe_fn ?? validate_or_fn;
/** @type {StandardSchemaV1 | null} */
const schema =
!maybe_fn || validate_or_fn === 'unchecked' ? null : /** @type {any} */ (validate_or_fn);
/**
* @param {string | number | boolean} [key]
*/
function create_instance(key) {
/** @type {RemoteForm<Input, Output>} */
const instance = {};
instance.method = 'POST';
Object.defineProperty(instance, 'enhance', {
value: () => {
return { action: instance.action, method: instance.method };
}
});
/** @type {RemoteInfo} */
const __ = {
type: 'form',
name: '',
id: '',
fn: async (data, meta, form_data) => {
// TODO 3.0 remove this warning
if (DEV && !data) {
const error = () => {
throw new Error(
'Remote form functions no longer get passed a FormData object. ' +
"`form` now has the same signature as `query` or `command`, i.e. it expects to be invoked like `form(schema, callback)` or `form('unchecked', callback)`. " +
'The payload of the callback function is now a POJO instead of a FormData object. See https://kit.svelte.dev/docs/remote-functions#form for details.'
);
};
data = {};
for (const key of [
'append',
'delete',
'entries',
'forEach',
'get',
'getAll',
'has',
'keys',
'set',
'values'
]) {
Object.defineProperty(data, key, { get: error });
}
}
/** @type {{ submission: true, input?: Record<string, any>, issues?: InternalRemoteFormIssue[], result: Output }} */
const output = {};
// make it possible to differentiate between user submission and programmatic `field.set(...)` updates
output.submission = true;
const { event, state } = get_request_store();
const validated = await schema?.['~standard'].validate(data);
if (meta.validate_only) {
return validated?.issues?.map((issue) => normalize_issue(issue, true)) ?? [];
}
if (validated?.issues !== undefined) {
handle_issues(output, validated.issues, form_data);
} else {
if (validated !== undefined) {
data = validated.value;
}
state.refreshes ??= {};
const issue = create_issues();
try {
output.result = await run_remote_function(
event,
state,
true,
() => data,
(data) => (!maybe_fn ? fn() : fn(data, issue))
);
} catch (e) {
if (e instanceof ValidationError) {
handle_issues(output, e.issues, form_data);
} else {
throw e;
}
}
}
// We don't need to care about args or deduplicating calls, because uneval results are only relevant in full page reloads
// where only one form submission is active at the same time
if (!event.isRemoteRequest) {
get_cache(__, state)[''] ??= output;
}
return output;
}
};
Object.defineProperty(instance, '__', { value: __ });
Object.defineProperty(instance, 'action', {
get: () => `?/remote=${__.id}`,
enumerable: true
});
Object.defineProperty(instance, 'fields', {
get() {
const data = get_cache(__)?.[''];
const issues = flatten_issues(data?.issues ?? []);
return create_field_proxy(
{},
() => data?.input ?? {},
(path, value) => {
if (data?.submission) {
// don't override a submission
return;
}
const input =
path.length === 0 ? value : deep_set(data?.input ?? {}, path.map(String), value);
(get_cache(__)[''] ??= {}).input = input;
},
() => issues
);
}
});
// TODO 3.0 remove
if (DEV) {
throw_on_old_property_access(instance);
Object.defineProperty(instance, 'buttonProps', {
get() {
throw new Error(
'`form.buttonProps` has been removed: Instead of `<button {...form.buttonProps}>, use `<button {...form.fields.action.as("submit", "value")}>`.' +
' See the PR for more info: https://github.com/sveltejs/kit/pull/14622'
);
}
});
}
Object.defineProperty(instance, 'result', {
get() {
try {
return get_cache(__)?.['']?.result;
} catch {
return undefined;
}
}
});
// On the server, pending is always 0
Object.defineProperty(instance, 'pending', {
get: () => 0
});
Object.defineProperty(instance, 'preflight', {
// preflight is a noop on the server
value: () => instance
});
Object.defineProperty(instance, 'validate', {
value: () => {
throw new Error('Cannot call validate() on the server');
}
});
if (key == undefined) {
Object.defineProperty(instance, 'for', {
/** @type {RemoteForm<any, any>['for']} */
value: (key) => {
const { state } = get_request_store();
const cache_key = __.id + '|' + JSON.stringify(key);
let instance = (state.form_instances ??= new Map()).get(cache_key);
if (!instance) {
instance = create_instance(key);
instance.__.id = `${__.id}/${encodeURIComponent(JSON.stringify(key))}`;
instance.__.name = __.name;
state.form_instances.set(cache_key, instance);
}
return instance;
}
});
}
return instance;
}
return create_instance();
}
/**
* @param {{ issues?: InternalRemoteFormIssue[], input?: Record<string, any>, result: any }} output
* @param {readonly StandardSchemaV1.Issue[]} issues
* @param {FormData | null} form_data - null if the form is progressively enhanced
*/
function handle_issues(output, issues, form_data) {
output.issues = issues.map((issue) => normalize_issue(issue, true));
// if it was a progressively-enhanced submission, we don't need
// to return the input — it's already there
if (form_data) {
output.input = {};
for (let key of form_data.keys()) {
// redact sensitive fields
if (/^[.\]]?_/.test(key)) continue;
const is_array = key.endsWith('[]');
const values = form_data.getAll(key).filter((value) => typeof value === 'string');
if (is_array) key = key.slice(0, -2);
set_nested_value(
/** @type {Record<string, any>} */ (output.input),
key,
is_array ? values : values[0]
);
}
}
}
/**
* Creates an invalid function that can be used to imperatively mark form fields as invalid
* @returns {InvalidField<any>}
*/
function create_issues() {
return /** @type {InvalidField<any>} */ (
new Proxy(
/** @param {string} message */
(message) => {
// TODO 3.0 remove
if (typeof message !== 'string') {
throw new Error(
'`invalid` should now be imported from `@sveltejs/kit` to throw validation issues. ' +
"The second parameter provided to the form function (renamed to `issue`) is still used to construct issues, e.g. `invalid(issue.field('message'))`. " +
'For more info see https://github.com/sveltejs/kit/pulls/14768'
);
}
return create_issue(message);
},
{
get(target, prop) {
if (typeof prop === 'symbol') return /** @type {any} */ (target)[prop];
return create_issue_proxy(prop, []);
}
}
)
);
/**
* @param {string} message
* @param {(string | number)[]} path
* @returns {StandardSchemaV1.Issue}
*/
function create_issue(message, path = []) {
return {
message,
path
};
}
/**
* Creates a proxy that builds up a path and returns a function to create an issue
* @param {string | number} key
* @param {(string | number)[]} path
*/
function create_issue_proxy(key, path) {
const new_path = [...path, key];
/**
* @param {string} message
* @returns {StandardSchemaV1.Issue}
*/
const issue_func = (message) => create_issue(message, new_path);
return new Proxy(issue_func, {
get(target, prop) {
if (typeof prop === 'symbol') return /** @type {any} */ (target)[prop];
// Handle array access like invalid.items[0]
if (/^\d+$/.test(prop)) {
return create_issue_proxy(parseInt(prop, 10), new_path);
}
// Handle property access like invalid.field.nested
return create_issue_proxy(prop, new_path);
}
});
}
}