-
-
Notifications
You must be signed in to change notification settings - Fork 48
/
formAction$.ts
184 lines (168 loc) · 5.27 KB
/
formAction$.ts
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
import { $, type QRL, implicit$FirstArg } from '@builder.io/qwik';
import {
type RequestEventAction,
globalActionQrl,
type Action,
} from '@builder.io/qwik-city';
import { AbortMessage } from '@builder.io/qwik-city/middleware/request-handler';
import { isDev } from '@builder.io/qwik/build';
import { FormError } from '../exceptions';
import type {
FieldValues,
ResponseData,
FormResponse,
Maybe,
FormErrors,
MaybePromise,
ValidateForm,
FormDataInfo,
FormActionStore,
PartialValues,
} from '../types';
import { getFormDataValues } from '../utils';
/**
* Value type of the form action result.
*/
export type FormActionResult<
TFieldValues extends FieldValues,
TResponseData extends ResponseData
> = FormResponse<TResponseData> & {
errors?: Maybe<FormErrors<TFieldValues>>;
};
/**
* Function type of the form action.
*/
export type FormActionFunction<
TFieldValues extends FieldValues,
TResponseData extends ResponseData
> = (
values: TFieldValues,
event: RequestEventAction
) => MaybePromise<FormActionResult<TFieldValues, TResponseData> | void>;
/**
* Value type of the second form action argument.
*/
export type FormActionArg2<TFieldValues extends FieldValues> =
| QRL<ValidateForm<TFieldValues>>
| (FormDataInfo<TFieldValues> & {
validate: QRL<ValidateForm<TFieldValues>>;
});
/**
* See {@link formAction$}
*/
export function formActionQrl<
TFieldValues extends FieldValues,
TResponseData extends ResponseData = undefined
>(
action: QRL<FormActionFunction<TFieldValues, TResponseData>>,
arg2: FormActionArg2<TFieldValues>
): Action<
FormActionStore<TFieldValues, TResponseData>,
PartialValues<TFieldValues>,
true
> {
return globalActionQrl(
$(
async (
jsonData: unknown,
event: RequestEventAction
): Promise<FormActionStore<TFieldValues, TResponseData>> => {
// Destructure validate function and form data info
const { validate, ...formDataInfo } =
typeof arg2 === 'object' ? arg2 : { validate: arg2 };
// Get content type of request
const type = event.request.headers
.get('content-type')
?.split(/[;,]/, 1)[0];
// Get form values from form or JSON data
const values: PartialValues<TFieldValues> =
type === 'application/x-www-form-urlencoded' ||
type === 'multipart/form-data'
? getFormDataValues(await event.request.formData(), formDataInfo)
: (jsonData as PartialValues<TFieldValues>);
// Validate values and get errors if necessary
const errors = validate ? await validate(values) : {};
// Create form action store object
let formActionStore: FormActionStore<TFieldValues, TResponseData> = {
values,
errors,
response: {},
};
// Try to run submit action if form has no errors
if (!Object.keys(errors).length) {
try {
const result = await action(values as TFieldValues, event);
// Add result to form action store if necessary
if (result && typeof result === 'object') {
formActionStore = {
values,
errors: result.errors || {},
response: {
status: result.status,
message: result.message,
data: result.data,
},
};
}
// If an abort message was thrown (e.g. a redirect), forward it
} catch (error) {
if (
error instanceof AbortMessage ||
(isDev &&
(error?.constructor?.name === 'AbortMessage' ||
error?.constructor?.name === 'RedirectMessage'))
) {
throw error;
// Otherwise log error and set error response
} else {
console.error(error);
// If it is an expected error, use its error info
if (error instanceof FormError) {
formActionStore = {
values,
errors: error.errors,
response: {
status: 'error',
message: error.message,
},
};
// Otherwise return a generic message to avoid leaking
// sensetive information
} else {
formActionStore.response = {
status: 'error',
message: 'An unknown error has occurred.',
};
}
}
}
}
// Return form action store object
return formActionStore;
}
),
{
id: action.getHash(),
}
);
}
/**
* Creates an action for progressively enhanced forms that handles validation
* and submission on the server.
*
* @param action The server action function.
* @param arg2 Validation and/or form data info.
*
* @returns Form action constructor.
*/
export const formAction$: <
TFieldValues extends FieldValues,
TResponseData extends ResponseData = undefined
>(
actionQrl: FormActionFunction<TFieldValues, TResponseData>,
arg2: FormActionArg2<TFieldValues>
) => Action<
FormActionStore<TFieldValues, TResponseData>,
PartialValues<TFieldValues>,
true
> = implicit$FirstArg(formActionQrl);