@octanejs/inertia 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/LICENSE +21 -0
- package/README.md +33 -0
- package/package.json +64 -0
- package/src/HeadContext.ts +5 -0
- package/src/PageContext.ts +5 -0
- package/src/index.ts +26 -0
- package/src/layoutProps.ts +25 -0
- package/src/server.ts +1 -0
- package/src/useForm.ts +318 -0
- package/src/useFormState.ts +585 -0
- package/src/useHttp.ts +457 -0
- package/src/usePage.ts +15 -0
- package/src/usePoll.ts +49 -0
- package/src/usePrefetch.ts +59 -0
- package/src/useRemember.ts +58 -0
package/src/useHttp.ts
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CancelToken,
|
|
3
|
+
Errors,
|
|
4
|
+
ErrorValue,
|
|
5
|
+
FormDataConvertible,
|
|
6
|
+
FormDataErrors,
|
|
7
|
+
FormDataKeys,
|
|
8
|
+
FormDataType,
|
|
9
|
+
FormDataValues,
|
|
10
|
+
hasFiles,
|
|
11
|
+
http,
|
|
12
|
+
HttpCancelledError,
|
|
13
|
+
HttpProgressEvent,
|
|
14
|
+
HttpResponseError,
|
|
15
|
+
mergeDataIntoQueryString,
|
|
16
|
+
Method,
|
|
17
|
+
objectToFormData,
|
|
18
|
+
Progress,
|
|
19
|
+
UrlMethodPair,
|
|
20
|
+
UseFormArguments,
|
|
21
|
+
UseFormTransformCallback,
|
|
22
|
+
UseFormUtils,
|
|
23
|
+
UseFormWithPrecognitionArguments,
|
|
24
|
+
UseHttpSubmitArguments,
|
|
25
|
+
UseHttpSubmitOptions,
|
|
26
|
+
} from '@inertiajs/core';
|
|
27
|
+
import { cloneDeep } from 'es-toolkit';
|
|
28
|
+
import {
|
|
29
|
+
NamedInputEvent,
|
|
30
|
+
toSimpleValidationErrors,
|
|
31
|
+
ValidationConfig,
|
|
32
|
+
Validator,
|
|
33
|
+
} from 'laravel-precognition';
|
|
34
|
+
import { useCallback, useMemo, useRef, useState } from 'octane';
|
|
35
|
+
import useFormState, { SetDataAction } from './useFormState';
|
|
36
|
+
import useRemember from './useRemember';
|
|
37
|
+
|
|
38
|
+
const slots = Object.fromEntries(
|
|
39
|
+
[
|
|
40
|
+
'abortController',
|
|
41
|
+
'excludeKeys',
|
|
42
|
+
'pendingOptimistic',
|
|
43
|
+
'response',
|
|
44
|
+
'submit',
|
|
45
|
+
'cancel',
|
|
46
|
+
'submitWithArgs',
|
|
47
|
+
'submitMethods',
|
|
48
|
+
].map((name) => [name, Symbol(`Inertia.useHttp.${name}`)]),
|
|
49
|
+
) as Record<string, symbol>;
|
|
50
|
+
|
|
51
|
+
type PrecognitionValidationConfig<TKeys> = ValidationConfig & {
|
|
52
|
+
only?: TKeys[] | Iterable<TKeys> | ArrayLike<TKeys>;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export interface UseHttpProps<TForm extends object, TResponse = unknown> {
|
|
56
|
+
data: TForm;
|
|
57
|
+
isDirty: boolean;
|
|
58
|
+
errors: FormDataErrors<TForm>;
|
|
59
|
+
hasErrors: boolean;
|
|
60
|
+
processing: boolean;
|
|
61
|
+
progress: Progress | null;
|
|
62
|
+
wasSuccessful: boolean;
|
|
63
|
+
recentlySuccessful: boolean;
|
|
64
|
+
response: TResponse | null;
|
|
65
|
+
setData: SetDataAction<TForm>;
|
|
66
|
+
transform: (callback: UseFormTransformCallback<TForm>) => void;
|
|
67
|
+
setDefaults: {
|
|
68
|
+
(): void;
|
|
69
|
+
<T extends FormDataKeys<TForm>>(field: T, value: FormDataValues<TForm, T>): void;
|
|
70
|
+
(fields: Partial<TForm>): void;
|
|
71
|
+
};
|
|
72
|
+
reset: <K extends FormDataKeys<TForm>>(...fields: K[]) => void;
|
|
73
|
+
clearErrors: <K extends FormDataKeys<TForm>>(...fields: K[]) => void;
|
|
74
|
+
resetAndClearErrors: <K extends FormDataKeys<TForm>>(...fields: K[]) => void;
|
|
75
|
+
setError: {
|
|
76
|
+
<K extends FormDataKeys<TForm>>(field: K, value: ErrorValue): void;
|
|
77
|
+
(errors: FormDataErrors<TForm>): void;
|
|
78
|
+
};
|
|
79
|
+
submit: (...args: UseHttpSubmitArguments<TResponse, TForm>) => Promise<TResponse>;
|
|
80
|
+
get: (url: string, options?: UseHttpSubmitOptions<TResponse, TForm>) => Promise<TResponse>;
|
|
81
|
+
post: (url: string, options?: UseHttpSubmitOptions<TResponse, TForm>) => Promise<TResponse>;
|
|
82
|
+
put: (url: string, options?: UseHttpSubmitOptions<TResponse, TForm>) => Promise<TResponse>;
|
|
83
|
+
patch: (url: string, options?: UseHttpSubmitOptions<TResponse, TForm>) => Promise<TResponse>;
|
|
84
|
+
delete: (url: string, options?: UseHttpSubmitOptions<TResponse, TForm>) => Promise<TResponse>;
|
|
85
|
+
cancel: () => void;
|
|
86
|
+
dontRemember: <K extends FormDataKeys<TForm>>(...fields: K[]) => UseHttpProps<TForm, TResponse>;
|
|
87
|
+
optimistic: (callback: (currentData: TForm) => Partial<TForm>) => UseHttpProps<TForm, TResponse>;
|
|
88
|
+
withAllErrors: () => UseHttpProps<TForm, TResponse>;
|
|
89
|
+
withPrecognition: (
|
|
90
|
+
...args: UseFormWithPrecognitionArguments
|
|
91
|
+
) => UseHttpPrecognitiveProps<TForm, TResponse>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface UseHttpValidationProps<TForm extends object, TResponse = unknown> {
|
|
95
|
+
invalid: <K extends FormDataKeys<TForm>>(field: K) => boolean;
|
|
96
|
+
setValidationTimeout: (duration: number) => UseHttpPrecognitiveProps<TForm, TResponse>;
|
|
97
|
+
touch: <K extends FormDataKeys<TForm>>(
|
|
98
|
+
field: K | NamedInputEvent | Array<K>,
|
|
99
|
+
...fields: K[]
|
|
100
|
+
) => UseHttpPrecognitiveProps<TForm, TResponse>;
|
|
101
|
+
touched: <K extends FormDataKeys<TForm>>(field?: K) => boolean;
|
|
102
|
+
valid: <K extends FormDataKeys<TForm>>(field: K) => boolean;
|
|
103
|
+
validate: <K extends FormDataKeys<TForm>>(
|
|
104
|
+
field?: K | NamedInputEvent | PrecognitionValidationConfig<K>,
|
|
105
|
+
config?: PrecognitionValidationConfig<K>,
|
|
106
|
+
) => UseHttpPrecognitiveProps<TForm, TResponse>;
|
|
107
|
+
validateFiles: () => UseHttpPrecognitiveProps<TForm, TResponse>;
|
|
108
|
+
validating: boolean;
|
|
109
|
+
validator: () => Validator;
|
|
110
|
+
withAllErrors: () => UseHttpPrecognitiveProps<TForm, TResponse>;
|
|
111
|
+
withoutFileValidation: () => UseHttpPrecognitiveProps<TForm, TResponse>;
|
|
112
|
+
setErrors: (errors: FormDataErrors<TForm>) => UseHttpPrecognitiveProps<TForm, TResponse>;
|
|
113
|
+
forgetError: <K extends FormDataKeys<TForm> | NamedInputEvent>(
|
|
114
|
+
field: K,
|
|
115
|
+
) => UseHttpPrecognitiveProps<TForm, TResponse>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type UseHttp<TForm extends object, TResponse = unknown> = UseHttpProps<TForm, TResponse>;
|
|
119
|
+
export type UseHttpPrecognitiveProps<TForm extends object, TResponse = unknown> = UseHttpProps<
|
|
120
|
+
TForm,
|
|
121
|
+
TResponse
|
|
122
|
+
> &
|
|
123
|
+
UseHttpValidationProps<TForm, TResponse>;
|
|
124
|
+
|
|
125
|
+
export default function useHttp<TForm extends FormDataType<TForm>, TResponse = unknown>(
|
|
126
|
+
method: Method | (() => Method),
|
|
127
|
+
url: string | (() => string),
|
|
128
|
+
data: TForm | (() => TForm),
|
|
129
|
+
): UseHttpPrecognitiveProps<TForm, TResponse>;
|
|
130
|
+
export default function useHttp<TForm extends FormDataType<TForm>, TResponse = unknown>(
|
|
131
|
+
urlMethodPair: UrlMethodPair | (() => UrlMethodPair),
|
|
132
|
+
data: TForm | (() => TForm),
|
|
133
|
+
): UseHttpPrecognitiveProps<TForm, TResponse>;
|
|
134
|
+
export default function useHttp<TForm extends FormDataType<TForm>, TResponse = unknown>(
|
|
135
|
+
rememberKey: string,
|
|
136
|
+
data: TForm | (() => TForm),
|
|
137
|
+
): UseHttp<TForm, TResponse>;
|
|
138
|
+
export default function useHttp<TForm extends FormDataType<TForm>, TResponse = unknown>(
|
|
139
|
+
data: TForm | (() => TForm),
|
|
140
|
+
): UseHttp<TForm, TResponse>;
|
|
141
|
+
export default function useHttp<TForm extends FormDataType<TForm>, TResponse = unknown>(): UseHttp<
|
|
142
|
+
TForm,
|
|
143
|
+
TResponse
|
|
144
|
+
>;
|
|
145
|
+
export default function useHttp<TForm extends FormDataType<TForm>, TResponse = unknown>(
|
|
146
|
+
...args: UseFormArguments<TForm>
|
|
147
|
+
): UseHttp<TForm, TResponse> | UseHttpPrecognitiveProps<TForm, TResponse> {
|
|
148
|
+
const publicArgs =
|
|
149
|
+
typeof args[args.length - 1] === 'symbol'
|
|
150
|
+
? (args.slice(0, -1) as UseFormArguments<TForm>)
|
|
151
|
+
: args;
|
|
152
|
+
const { rememberKey, data, precognitionEndpoint } = UseFormUtils.parseUseFormArguments<TForm>(
|
|
153
|
+
...publicArgs,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Resolve initial data for remember functionality hooks
|
|
157
|
+
const initialDefaults = typeof data === 'function' ? cloneDeep(data()) : cloneDeep(data);
|
|
158
|
+
|
|
159
|
+
const abortController = useRef<AbortController | null>(null, slots.abortController);
|
|
160
|
+
const excludeKeysRef = useRef<FormDataKeys<TForm>[]>([], slots.excludeKeys);
|
|
161
|
+
const pendingOptimisticRef = useRef<((currentData: TForm) => Partial<TForm>) | null>(
|
|
162
|
+
null,
|
|
163
|
+
slots.pendingOptimistic,
|
|
164
|
+
);
|
|
165
|
+
const [response, setResponse] = useState<TResponse | null>(null, slots.response);
|
|
166
|
+
|
|
167
|
+
// For remember functionality, we need custom state hooks
|
|
168
|
+
const useDataState = rememberKey
|
|
169
|
+
? () => useRemember<TForm>(initialDefaults, `${rememberKey}:data`, excludeKeysRef)
|
|
170
|
+
: undefined;
|
|
171
|
+
|
|
172
|
+
const useErrorsState = rememberKey
|
|
173
|
+
? () => useRemember<FormDataErrors<TForm>>({} as FormDataErrors<TForm>, `${rememberKey}:errors`)
|
|
174
|
+
: undefined;
|
|
175
|
+
|
|
176
|
+
const {
|
|
177
|
+
form: baseForm,
|
|
178
|
+
setDefaultsState,
|
|
179
|
+
transformRef,
|
|
180
|
+
dataRef,
|
|
181
|
+
precognitionEndpointRef,
|
|
182
|
+
isMounted,
|
|
183
|
+
setProcessing,
|
|
184
|
+
setProgress,
|
|
185
|
+
markAsSuccessful,
|
|
186
|
+
clearErrors,
|
|
187
|
+
setError,
|
|
188
|
+
defaultsCalledInOnSuccessRef,
|
|
189
|
+
resetBeforeSubmit,
|
|
190
|
+
finishProcessing,
|
|
191
|
+
withAllErrors,
|
|
192
|
+
} = useFormState<TForm>({
|
|
193
|
+
data,
|
|
194
|
+
precognitionEndpoint,
|
|
195
|
+
useDataState,
|
|
196
|
+
useErrorsState,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const submit = useCallback(
|
|
200
|
+
async (
|
|
201
|
+
method: Method,
|
|
202
|
+
url: string,
|
|
203
|
+
options: UseHttpSubmitOptions<TResponse, TForm>,
|
|
204
|
+
): Promise<TResponse> => {
|
|
205
|
+
const onBefore = options.onBefore?.();
|
|
206
|
+
|
|
207
|
+
if (onBefore === false) {
|
|
208
|
+
return Promise.reject(new Error('Request cancelled by onBefore'));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
defaultsCalledInOnSuccessRef.current = false;
|
|
212
|
+
|
|
213
|
+
if (isMounted.current) {
|
|
214
|
+
resetBeforeSubmit();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
abortController.current = new AbortController();
|
|
218
|
+
|
|
219
|
+
const cancelToken: CancelToken = {
|
|
220
|
+
cancel: () => abortController.current?.abort(),
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
options.onCancelToken?.(cancelToken);
|
|
224
|
+
|
|
225
|
+
options.optimistic = options.optimistic ?? pendingOptimisticRef.current ?? undefined;
|
|
226
|
+
pendingOptimisticRef.current = null;
|
|
227
|
+
|
|
228
|
+
let snapshot: TForm | undefined;
|
|
229
|
+
|
|
230
|
+
if (options.optimistic) {
|
|
231
|
+
snapshot = cloneDeep(dataRef.current);
|
|
232
|
+
const optimisticData = options.optimistic(cloneDeep(snapshot));
|
|
233
|
+
|
|
234
|
+
baseForm.setData((current: TForm) => ({ ...current, ...optimisticData }));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (isMounted.current) {
|
|
238
|
+
setProcessing(true);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
options.onStart?.();
|
|
242
|
+
|
|
243
|
+
const transformedData = transformRef.current(dataRef.current);
|
|
244
|
+
const useFormData = hasFiles(transformedData as Record<string, FormDataConvertible>);
|
|
245
|
+
|
|
246
|
+
let requestUrl = url;
|
|
247
|
+
let requestData: FormData | string | undefined;
|
|
248
|
+
let contentType: string | undefined;
|
|
249
|
+
|
|
250
|
+
if (method === 'get') {
|
|
251
|
+
const [urlWithParams] = mergeDataIntoQueryString(
|
|
252
|
+
method,
|
|
253
|
+
url,
|
|
254
|
+
transformedData as Record<string, FormDataConvertible>,
|
|
255
|
+
);
|
|
256
|
+
requestUrl = urlWithParams;
|
|
257
|
+
} else {
|
|
258
|
+
if (useFormData) {
|
|
259
|
+
requestData = objectToFormData(transformedData as Record<string, FormDataConvertible>);
|
|
260
|
+
} else {
|
|
261
|
+
requestData = JSON.stringify(transformedData);
|
|
262
|
+
contentType = 'application/json';
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const httpResponse = await http.getClient().request({
|
|
268
|
+
method,
|
|
269
|
+
url: requestUrl,
|
|
270
|
+
data: requestData,
|
|
271
|
+
headers: {
|
|
272
|
+
Accept: 'application/json',
|
|
273
|
+
...(contentType ? { 'Content-Type': contentType } : {}),
|
|
274
|
+
...options.headers,
|
|
275
|
+
},
|
|
276
|
+
signal: abortController.current.signal,
|
|
277
|
+
onUploadProgress: (event: HttpProgressEvent) => {
|
|
278
|
+
if (isMounted.current) {
|
|
279
|
+
setProgress(event);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
options.onProgress?.(event);
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const responseData = (
|
|
287
|
+
httpResponse.data ? JSON.parse(httpResponse.data) : null
|
|
288
|
+
) as TResponse;
|
|
289
|
+
|
|
290
|
+
if (httpResponse.status >= 200 && httpResponse.status < 300) {
|
|
291
|
+
if (isMounted.current) {
|
|
292
|
+
markAsSuccessful();
|
|
293
|
+
setResponse(responseData);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
options.onSuccess?.(responseData, httpResponse);
|
|
297
|
+
|
|
298
|
+
if (isMounted.current && !defaultsCalledInOnSuccessRef.current) {
|
|
299
|
+
baseForm.setData((data: TForm) => {
|
|
300
|
+
setDefaultsState(cloneDeep(data));
|
|
301
|
+
return data;
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return responseData;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
throw new HttpResponseError(
|
|
309
|
+
`Request failed with status ${httpResponse.status}`,
|
|
310
|
+
httpResponse,
|
|
311
|
+
);
|
|
312
|
+
} catch (error: unknown) {
|
|
313
|
+
if (snapshot && isMounted.current) {
|
|
314
|
+
baseForm.setData(snapshot);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (error instanceof HttpResponseError) {
|
|
318
|
+
if (error.response.status === 422) {
|
|
319
|
+
const responseData = JSON.parse(error.response.data);
|
|
320
|
+
const validationErrors = responseData.errors || {};
|
|
321
|
+
const processedErrors = (
|
|
322
|
+
withAllErrors.enabled()
|
|
323
|
+
? validationErrors
|
|
324
|
+
: toSimpleValidationErrors(validationErrors)
|
|
325
|
+
) as FormDataErrors<TForm>;
|
|
326
|
+
|
|
327
|
+
if (isMounted.current) {
|
|
328
|
+
clearErrors();
|
|
329
|
+
setError(processedErrors);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
options.onError?.(processedErrors as Errors);
|
|
333
|
+
|
|
334
|
+
return undefined as TResponse;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
options.onHttpException?.(error.response);
|
|
338
|
+
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (
|
|
343
|
+
error instanceof HttpCancelledError ||
|
|
344
|
+
(error instanceof Error && error.name === 'AbortError')
|
|
345
|
+
) {
|
|
346
|
+
options.onCancel?.();
|
|
347
|
+
throw new HttpCancelledError('Request was cancelled', url);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
options.onNetworkError?.(error instanceof Error ? error : new Error('Unknown error'));
|
|
351
|
+
|
|
352
|
+
throw error;
|
|
353
|
+
} finally {
|
|
354
|
+
if (isMounted.current) {
|
|
355
|
+
finishProcessing();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
abortController.current = null;
|
|
359
|
+
options.onFinish?.();
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
[clearErrors, setError],
|
|
363
|
+
slots.submit,
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
const cancel = useCallback(
|
|
367
|
+
() => {
|
|
368
|
+
if (abortController.current) {
|
|
369
|
+
abortController.current.abort();
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
[],
|
|
373
|
+
slots.cancel,
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
const submitWithArgs = useCallback(
|
|
377
|
+
(...args: UseHttpSubmitArguments<TResponse, TForm>): Promise<TResponse> => {
|
|
378
|
+
const parsed = UseFormUtils.parseSubmitArguments(
|
|
379
|
+
args as any,
|
|
380
|
+
precognitionEndpointRef.current,
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
return submit(
|
|
384
|
+
parsed.method,
|
|
385
|
+
parsed.url,
|
|
386
|
+
parsed.options as UseHttpSubmitOptions<TResponse, TForm>,
|
|
387
|
+
);
|
|
388
|
+
},
|
|
389
|
+
[submit],
|
|
390
|
+
slots.submitWithArgs,
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
const submitMethods = useMemo(
|
|
394
|
+
() => ({
|
|
395
|
+
get: async (
|
|
396
|
+
url: string,
|
|
397
|
+
options: UseHttpSubmitOptions<TResponse, TForm> = {},
|
|
398
|
+
): Promise<TResponse> => submit('get', url, options),
|
|
399
|
+
post: async (
|
|
400
|
+
url: string,
|
|
401
|
+
options: UseHttpSubmitOptions<TResponse, TForm> = {},
|
|
402
|
+
): Promise<TResponse> => submit('post', url, options),
|
|
403
|
+
put: async (
|
|
404
|
+
url: string,
|
|
405
|
+
options: UseHttpSubmitOptions<TResponse, TForm> = {},
|
|
406
|
+
): Promise<TResponse> => submit('put', url, options),
|
|
407
|
+
patch: async (
|
|
408
|
+
url: string,
|
|
409
|
+
options: UseHttpSubmitOptions<TResponse, TForm> = {},
|
|
410
|
+
): Promise<TResponse> => submit('patch', url, options),
|
|
411
|
+
delete: async (
|
|
412
|
+
url: string,
|
|
413
|
+
options: UseHttpSubmitOptions<TResponse, TForm> = {},
|
|
414
|
+
): Promise<TResponse> => submit('delete', url, options),
|
|
415
|
+
}),
|
|
416
|
+
[submit],
|
|
417
|
+
slots.submitMethods,
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
// Add useHttp-specific methods to the form object
|
|
421
|
+
Object.assign(baseForm, {
|
|
422
|
+
response,
|
|
423
|
+
submit: submitWithArgs,
|
|
424
|
+
...submitMethods,
|
|
425
|
+
cancel,
|
|
426
|
+
dontRemember: <K extends FormDataKeys<TForm>>(...keys: K[]) => {
|
|
427
|
+
excludeKeysRef.current = keys;
|
|
428
|
+
return form;
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
optimistic: (callback: (currentData: TForm) => Partial<TForm>) => {
|
|
432
|
+
pendingOptimisticRef.current = callback;
|
|
433
|
+
return form;
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
withAllErrors: () => {
|
|
437
|
+
withAllErrors.enable();
|
|
438
|
+
return form;
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// Cast to the full form type (baseForm now has HTTP methods)
|
|
443
|
+
const form = baseForm as unknown as UseHttpProps<TForm, TResponse>;
|
|
444
|
+
|
|
445
|
+
// Wrap withPrecognition to return the correct type with HTTP methods
|
|
446
|
+
const originalWithPrecognition = baseForm.withPrecognition;
|
|
447
|
+
form.withPrecognition = (
|
|
448
|
+
...args: UseFormWithPrecognitionArguments
|
|
449
|
+
): UseHttpPrecognitiveProps<TForm, TResponse> => {
|
|
450
|
+
originalWithPrecognition(...args);
|
|
451
|
+
return form as UseHttpPrecognitiveProps<TForm, TResponse>;
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
return precognitionEndpointRef.current
|
|
455
|
+
? (form as UseHttpPrecognitiveProps<TForm, TResponse>)
|
|
456
|
+
: form;
|
|
457
|
+
}
|
package/src/usePage.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Page, PageProps, SharedPageProps } from '@inertiajs/core';
|
|
2
|
+
import { useContext } from 'octane';
|
|
3
|
+
import PageContext from './PageContext';
|
|
4
|
+
|
|
5
|
+
export default function usePage<TPageProps extends PageProps = PageProps>(): Page<
|
|
6
|
+
TPageProps & SharedPageProps
|
|
7
|
+
> {
|
|
8
|
+
const page = useContext(PageContext);
|
|
9
|
+
|
|
10
|
+
if (!page) {
|
|
11
|
+
throw new Error('usePage must be used within the Inertia component');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return page as Page<TPageProps & SharedPageProps>;
|
|
15
|
+
}
|
package/src/usePoll.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { PollOptions, ReloadOptions, router } from '@inertiajs/core';
|
|
2
|
+
import { useEffect, useRef } from 'octane';
|
|
3
|
+
|
|
4
|
+
const latestSlot = Symbol('Inertia.usePoll.latest');
|
|
5
|
+
const pollSlot = Symbol('Inertia.usePoll.poll');
|
|
6
|
+
const effectSlot = Symbol('Inertia.usePoll.effect');
|
|
7
|
+
|
|
8
|
+
export default function usePoll(
|
|
9
|
+
interval: number,
|
|
10
|
+
requestOptions: ReloadOptions | (() => ReloadOptions) | symbol = {},
|
|
11
|
+
options: PollOptions | symbol = {
|
|
12
|
+
keepAlive: false,
|
|
13
|
+
autoStart: true,
|
|
14
|
+
},
|
|
15
|
+
) {
|
|
16
|
+
requestOptions = typeof requestOptions === 'symbol' ? {} : requestOptions;
|
|
17
|
+
options =
|
|
18
|
+
typeof options === 'symbol'
|
|
19
|
+
? {
|
|
20
|
+
keepAlive: false,
|
|
21
|
+
autoStart: true,
|
|
22
|
+
}
|
|
23
|
+
: options;
|
|
24
|
+
const latest = useRef(requestOptions, latestSlot);
|
|
25
|
+
latest.current = requestOptions;
|
|
26
|
+
|
|
27
|
+
const pollRef = useRef<ReturnType<typeof router.poll> | null>(null, pollSlot);
|
|
28
|
+
|
|
29
|
+
useEffect(
|
|
30
|
+
() => {
|
|
31
|
+
pollRef.current = router.poll(
|
|
32
|
+
interval,
|
|
33
|
+
typeof requestOptions === 'function'
|
|
34
|
+
? () => (latest.current as () => ReloadOptions)()
|
|
35
|
+
: requestOptions,
|
|
36
|
+
{ ...options, autoStart: options.autoStart ?? true },
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return () => pollRef.current?.destroy();
|
|
40
|
+
},
|
|
41
|
+
[],
|
|
42
|
+
effectSlot,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
stop: () => pollRef.current?.stop(),
|
|
47
|
+
start: () => pollRef.current?.start(),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { router, VisitOptions } from '@inertiajs/core';
|
|
2
|
+
import { useEffect, useState } from 'octane';
|
|
3
|
+
|
|
4
|
+
const lastUpdatedAtSlot = Symbol('Inertia.usePrefetch.lastUpdatedAt');
|
|
5
|
+
const isPrefetchingSlot = Symbol('Inertia.usePrefetch.isPrefetching');
|
|
6
|
+
const isPrefetchedSlot = Symbol('Inertia.usePrefetch.isPrefetched');
|
|
7
|
+
const effectSlot = Symbol('Inertia.usePrefetch.effect');
|
|
8
|
+
|
|
9
|
+
export default function usePrefetch(options: VisitOptions | symbol = {}): {
|
|
10
|
+
lastUpdatedAt: number | null;
|
|
11
|
+
isPrefetching: boolean;
|
|
12
|
+
isPrefetched: boolean;
|
|
13
|
+
flush: () => void;
|
|
14
|
+
} {
|
|
15
|
+
options = typeof options === 'symbol' ? {} : options;
|
|
16
|
+
const cached =
|
|
17
|
+
typeof window === 'undefined' ? null : router.getCached(window.location.pathname, options);
|
|
18
|
+
const inFlight =
|
|
19
|
+
typeof window === 'undefined' ? null : router.getPrefetching(window.location.pathname, options);
|
|
20
|
+
|
|
21
|
+
const [lastUpdatedAt, setLastUpdatedAt] = useState<number | null>(
|
|
22
|
+
cached?.staleTimestamp || null,
|
|
23
|
+
lastUpdatedAtSlot,
|
|
24
|
+
);
|
|
25
|
+
const [isPrefetching, setIsPrefetching] = useState(inFlight !== null, isPrefetchingSlot);
|
|
26
|
+
const [isPrefetched, setIsPrefetched] = useState(cached !== null, isPrefetchedSlot);
|
|
27
|
+
|
|
28
|
+
useEffect(
|
|
29
|
+
() => {
|
|
30
|
+
const onPrefetchingListener = router.on('prefetching', (e) => {
|
|
31
|
+
if (e.detail.visit.url.pathname === window.location.pathname) {
|
|
32
|
+
setIsPrefetching(true);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const onPrefetchedListener = router.on('prefetched', (e) => {
|
|
37
|
+
if (e.detail.visit.url.pathname === window.location.pathname) {
|
|
38
|
+
setIsPrefetching(false);
|
|
39
|
+
setIsPrefetched(true);
|
|
40
|
+
setLastUpdatedAt(e.detail.fetchedAt);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return () => {
|
|
45
|
+
onPrefetchedListener();
|
|
46
|
+
onPrefetchingListener();
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
[],
|
|
50
|
+
effectSlot,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
lastUpdatedAt,
|
|
55
|
+
isPrefetching,
|
|
56
|
+
isPrefetched,
|
|
57
|
+
flush: () => router.flush(window.location.pathname, options),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { router } from '@inertiajs/core';
|
|
2
|
+
import { useEffect, useState } from 'octane';
|
|
3
|
+
|
|
4
|
+
type SetStateAction<State> = State | ((previousState: State) => State);
|
|
5
|
+
type Dispatch<Action> = (action: Action) => void;
|
|
6
|
+
type MutableRefObject<Value> = { current: Value };
|
|
7
|
+
|
|
8
|
+
const stateSlotCache = new Map<string, symbol>();
|
|
9
|
+
const effectSlotCache = new Map<string, symbol>();
|
|
10
|
+
|
|
11
|
+
function slotForRememberKey(
|
|
12
|
+
cache: Map<string, symbol>,
|
|
13
|
+
label: string,
|
|
14
|
+
key: string | undefined,
|
|
15
|
+
): symbol {
|
|
16
|
+
const cacheKey = key ?? '';
|
|
17
|
+
let slot = cache.get(cacheKey);
|
|
18
|
+
if (!slot) {
|
|
19
|
+
slot = Symbol(`Inertia.useRemember.${label}:${cacheKey}`);
|
|
20
|
+
cache.set(cacheKey, slot);
|
|
21
|
+
}
|
|
22
|
+
return slot;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function useRemember<State>(
|
|
26
|
+
initialState: State,
|
|
27
|
+
key?: string | symbol,
|
|
28
|
+
excludeKeysRef?: MutableRefObject<string[]> | symbol,
|
|
29
|
+
): [State, Dispatch<SetStateAction<State>>] {
|
|
30
|
+
key = typeof key === 'symbol' ? undefined : key;
|
|
31
|
+
excludeKeysRef = typeof excludeKeysRef === 'symbol' ? undefined : excludeKeysRef;
|
|
32
|
+
const stateSlot = slotForRememberKey(stateSlotCache, 'state', key);
|
|
33
|
+
const effectSlot = slotForRememberKey(effectSlotCache, 'effect', key);
|
|
34
|
+
const [state, setState] = useState(function () {
|
|
35
|
+
const restored = router.restore(key) as State;
|
|
36
|
+
|
|
37
|
+
return restored !== undefined ? restored : initialState;
|
|
38
|
+
}, stateSlot);
|
|
39
|
+
|
|
40
|
+
useEffect(
|
|
41
|
+
function () {
|
|
42
|
+
const keys = excludeKeysRef?.current;
|
|
43
|
+
if (keys && keys.length > 0 && typeof state === 'object' && state !== null) {
|
|
44
|
+
const filtered = { ...state } as Record<string, unknown>;
|
|
45
|
+
keys.forEach(function (k) {
|
|
46
|
+
delete filtered[k];
|
|
47
|
+
});
|
|
48
|
+
router.remember(filtered, key);
|
|
49
|
+
} else {
|
|
50
|
+
router.remember(state, key);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
[state, key],
|
|
54
|
+
effectSlot,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
return [state, setState];
|
|
58
|
+
}
|