@void/vue 0.27.0 → 1.0.3
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/dist/index.d.mts +72 -2
- package/dist/index.mjs +431 -1
- package/dist/plugin.d.mts +2 -1
- package/dist/plugin.mjs +2 -38
- package/package.json +8 -8
- package/dist/index-vAbysUNV.d.mts +0 -72
- package/dist/runtime/index.d.mts +0 -2
- package/dist/runtime/index.mjs +0 -2
- package/dist/runtime-lPB1mgy1.mjs +0 -436
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,72 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { ActionClient, NavigationState, VisitOptions, VoidActionError, VoidRouter } from "void/pages-client";
|
|
2
|
+
import { Component } from "vue";
|
|
3
|
+
import { CloudContextVariables, DeferredState as Deferred, InferProps } from "void";
|
|
4
|
+
import { ActionFormOptions, ActionUrl, ResolveActionBody } from "void/routes";
|
|
5
|
+
//#region src/runtime/form.d.ts
|
|
6
|
+
export declare const Form: Component;
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/runtime/link.d.ts
|
|
9
|
+
export declare const Link: Component;
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/runtime/use-router.d.ts
|
|
12
|
+
export declare function useRouter(): VoidRouter;
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/runtime/use-params.d.ts
|
|
15
|
+
export declare function useParams<T extends Record<string, string> = Record<string, string>>(): T;
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/runtime/use-shared.d.ts
|
|
18
|
+
export declare function useShared<T = CloudContextVariables["shared"]>(): T;
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/runtime/use-form.d.ts
|
|
21
|
+
type FormSubmitOptions = Omit<VisitOptions, "method" | "data"> & {
|
|
22
|
+
data?: Record<string, unknown>;
|
|
23
|
+
};
|
|
24
|
+
export declare function useForm<U extends ActionUrl & string>(url: U, defaults: ResolveActionBody<U>, options?: ActionFormOptions<U>): {
|
|
25
|
+
data: ResolveActionBody<U>;
|
|
26
|
+
errors: Partial<Record<keyof ResolveActionBody<U> & string, string>>;
|
|
27
|
+
error: VoidActionError | null;
|
|
28
|
+
pending: boolean;
|
|
29
|
+
hasChanges: boolean;
|
|
30
|
+
wasSuccessful: boolean;
|
|
31
|
+
recentlySuccessful: boolean;
|
|
32
|
+
post(opts?: FormSubmitOptions): Promise<void>;
|
|
33
|
+
put(opts?: FormSubmitOptions): Promise<void>;
|
|
34
|
+
patch(opts?: FormSubmitOptions): Promise<void>;
|
|
35
|
+
delete(opts?: FormSubmitOptions): Promise<void>;
|
|
36
|
+
reset(...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
|
|
37
|
+
clearErrors(...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
|
|
38
|
+
clearError(): void;
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/runtime/use-navigation.d.ts
|
|
42
|
+
export declare function useNavigation(): NavigationState;
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/runtime/use-island-form.d.ts
|
|
45
|
+
type IslandFormReturn<T extends Record<string, unknown>> = Readonly<{
|
|
46
|
+
data: T;
|
|
47
|
+
errors: Record<string, string>;
|
|
48
|
+
error: VoidActionError | null;
|
|
49
|
+
pending: boolean;
|
|
50
|
+
hasChanges: boolean;
|
|
51
|
+
wasSuccessful: boolean;
|
|
52
|
+
recentlySuccessful: boolean;
|
|
53
|
+
submit(url: string, method: string): Promise<void>;
|
|
54
|
+
post(url: string): Promise<void>;
|
|
55
|
+
put(url: string): Promise<void>;
|
|
56
|
+
patch(url: string): Promise<void>;
|
|
57
|
+
delete(url: string): Promise<void>;
|
|
58
|
+
reset(...fields: ReadonlyArray<keyof T & string>): void;
|
|
59
|
+
clearErrors(...fields: ReadonlyArray<string>): void;
|
|
60
|
+
clearError(): void;
|
|
61
|
+
}>;
|
|
62
|
+
/**
|
|
63
|
+
* Form composable for island pages. Uses fetch + page reload instead of the
|
|
64
|
+
* Void Router (which is not available in island mode).
|
|
65
|
+
*/
|
|
66
|
+
export declare function useIslandForm<T extends Record<string, unknown>>(defaults: T): IslandFormReturn<T>;
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/runtime/action.d.ts
|
|
69
|
+
export declare const action: ActionClient["action"];
|
|
70
|
+
export declare const setActionRouter: ActionClient["setActionRouter"];
|
|
71
|
+
//#endregion
|
|
72
|
+
export type { Deferred, InferProps };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,432 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { VoidActionError, categorizeActionError, createActionClient, formErrorMessage, formRetryMessage, formSubmissionBlocked, getValidationErrors, idleNavigationState, interpolateRouteParams, isAbortError, isEqualFormValue, isModifiedEvent, mergeDataIntoHref, scheduleFormRetry, ssrProxy, submitAction } from "void/pages-client";
|
|
2
|
+
import { computed, defineComponent, h, inject, onMounted, onUnmounted, reactive, ref, watch } from "vue";
|
|
3
|
+
//#region src/runtime/form.ts
|
|
4
|
+
const Form = defineComponent({
|
|
5
|
+
name: "VoidForm",
|
|
6
|
+
inheritAttrs: false,
|
|
7
|
+
props: {
|
|
8
|
+
form: {
|
|
9
|
+
type: Object,
|
|
10
|
+
required: true
|
|
11
|
+
},
|
|
12
|
+
method: {
|
|
13
|
+
type: String,
|
|
14
|
+
default: "post"
|
|
15
|
+
},
|
|
16
|
+
renderError: Function
|
|
17
|
+
},
|
|
18
|
+
setup(props, { attrs, slots }) {
|
|
19
|
+
const now = ref(Date.now());
|
|
20
|
+
let cancel = () => {};
|
|
21
|
+
onMounted(() => watch(() => props.form.error, (error) => {
|
|
22
|
+
cancel();
|
|
23
|
+
cancel = scheduleFormRetry(error, () => {
|
|
24
|
+
now.value = Date.now();
|
|
25
|
+
});
|
|
26
|
+
}, { immediate: true }));
|
|
27
|
+
onUnmounted(() => cancel());
|
|
28
|
+
return () => {
|
|
29
|
+
const form = props.form;
|
|
30
|
+
const message = formErrorMessage(form), retry = formRetryMessage(form.error);
|
|
31
|
+
return h("form", {
|
|
32
|
+
...attrs,
|
|
33
|
+
"aria-busy": form.pending,
|
|
34
|
+
onSubmit: async (event) => {
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
if (!formSubmissionBlocked(form, now.value)) await form[props.method]();
|
|
37
|
+
}
|
|
38
|
+
}, [message ? h("div", {
|
|
39
|
+
role: "alert",
|
|
40
|
+
"data-void-form-error": ""
|
|
41
|
+
}, [form.error && props.renderError ? props.renderError(form.error) : message, retry ? h("p", retry) : null]) : null, slots.default?.()]);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/runtime/link.ts
|
|
47
|
+
function callHandler(handler, event) {
|
|
48
|
+
if (Array.isArray(handler)) {
|
|
49
|
+
for (const fn of handler) if (typeof fn === "function") fn(event);
|
|
50
|
+
} else if (typeof handler === "function") handler(event);
|
|
51
|
+
}
|
|
52
|
+
const Link = defineComponent({
|
|
53
|
+
name: "Link",
|
|
54
|
+
props: {
|
|
55
|
+
href: {
|
|
56
|
+
type: String,
|
|
57
|
+
required: true
|
|
58
|
+
},
|
|
59
|
+
method: {
|
|
60
|
+
type: String,
|
|
61
|
+
default: "GET"
|
|
62
|
+
},
|
|
63
|
+
data: {
|
|
64
|
+
type: Object,
|
|
65
|
+
default: void 0
|
|
66
|
+
},
|
|
67
|
+
preserveScroll: {
|
|
68
|
+
type: Boolean,
|
|
69
|
+
default: false
|
|
70
|
+
},
|
|
71
|
+
preserveState: {
|
|
72
|
+
type: Boolean,
|
|
73
|
+
default: false
|
|
74
|
+
},
|
|
75
|
+
replace: {
|
|
76
|
+
type: Boolean,
|
|
77
|
+
default: false
|
|
78
|
+
},
|
|
79
|
+
reloadDocument: {
|
|
80
|
+
type: Boolean,
|
|
81
|
+
default: false
|
|
82
|
+
},
|
|
83
|
+
viewTransition: {
|
|
84
|
+
type: Boolean,
|
|
85
|
+
default: void 0
|
|
86
|
+
},
|
|
87
|
+
onNavigate: {
|
|
88
|
+
type: Function,
|
|
89
|
+
default: void 0
|
|
90
|
+
},
|
|
91
|
+
prefetch: {
|
|
92
|
+
type: [
|
|
93
|
+
Boolean,
|
|
94
|
+
String,
|
|
95
|
+
Array
|
|
96
|
+
],
|
|
97
|
+
default: false
|
|
98
|
+
},
|
|
99
|
+
cacheFor: {
|
|
100
|
+
type: [
|
|
101
|
+
Number,
|
|
102
|
+
String,
|
|
103
|
+
Array
|
|
104
|
+
],
|
|
105
|
+
default: void 0
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
setup(props, { slots, attrs }) {
|
|
109
|
+
const router = inject("__void_router", null);
|
|
110
|
+
const elRef = ref(null);
|
|
111
|
+
const normalizedMethod = computed(() => props.method.toUpperCase());
|
|
112
|
+
const isGet = computed(() => normalizedMethod.value === "GET");
|
|
113
|
+
const hrefWithData = computed(() => isGet.value ? mergeDataIntoHref(props.href, props.data) : props.href);
|
|
114
|
+
const strategies = computed(() => {
|
|
115
|
+
if (props.prefetch === false || props.reloadDocument) return [];
|
|
116
|
+
if (props.prefetch === true) return ["hover"];
|
|
117
|
+
if (typeof props.prefetch === "string") return [props.prefetch];
|
|
118
|
+
return props.prefetch;
|
|
119
|
+
});
|
|
120
|
+
function doPrefetch() {
|
|
121
|
+
if (!router?.prefetch) return;
|
|
122
|
+
const current = window.location.pathname + window.location.search;
|
|
123
|
+
if (hrefWithData.value === current) return;
|
|
124
|
+
const options = { method: normalizedMethod.value };
|
|
125
|
+
if (props.cacheFor !== void 0) options.cacheFor = props.cacheFor;
|
|
126
|
+
router.prefetch(hrefWithData.value, options);
|
|
127
|
+
}
|
|
128
|
+
function onClick(e) {
|
|
129
|
+
callHandler(attrs.onClick, e);
|
|
130
|
+
if (props.reloadDocument || !router || e.defaultPrevented || isModifiedEvent(e)) return;
|
|
131
|
+
if (e.currentTarget.hasAttribute("download")) return;
|
|
132
|
+
if (new URL(hrefWithData.value, window.location.origin).origin !== window.location.origin) return;
|
|
133
|
+
let navigatePrevented = false;
|
|
134
|
+
props.onNavigate?.({ preventDefault() {
|
|
135
|
+
navigatePrevented = true;
|
|
136
|
+
} });
|
|
137
|
+
if (navigatePrevented) {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
router.visit(hrefWithData.value, {
|
|
143
|
+
method: normalizedMethod.value,
|
|
144
|
+
data: isGet.value ? void 0 : props.data,
|
|
145
|
+
preserveScroll: props.preserveScroll,
|
|
146
|
+
preserveState: props.preserveState,
|
|
147
|
+
replace: props.replace,
|
|
148
|
+
viewTransition: props.viewTransition
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
let hoverTimer = null;
|
|
152
|
+
let observer = null;
|
|
153
|
+
function onMouseEnter(e) {
|
|
154
|
+
callHandler(attrs.onMouseenter ?? attrs.onMouseEnter, e);
|
|
155
|
+
if (e.defaultPrevented) return;
|
|
156
|
+
hoverTimer = setTimeout(doPrefetch, router?._hoverDelay ?? 75);
|
|
157
|
+
}
|
|
158
|
+
function onMouseLeave(e) {
|
|
159
|
+
callHandler(attrs.onMouseleave ?? attrs.onMouseLeave, e);
|
|
160
|
+
if (hoverTimer) {
|
|
161
|
+
clearTimeout(hoverTimer);
|
|
162
|
+
hoverTimer = null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function onFocus(e) {
|
|
166
|
+
callHandler(attrs.onFocus, e);
|
|
167
|
+
if (!e.defaultPrevented) doPrefetch();
|
|
168
|
+
}
|
|
169
|
+
function onMouseDown(e) {
|
|
170
|
+
callHandler(attrs.onMousedown ?? attrs.onMouseDown, e);
|
|
171
|
+
if (!e.defaultPrevented) doPrefetch();
|
|
172
|
+
}
|
|
173
|
+
function onTouchStart(e) {
|
|
174
|
+
callHandler(attrs.onTouchstart ?? attrs.onTouchStart, e);
|
|
175
|
+
if (!e.defaultPrevented) doPrefetch();
|
|
176
|
+
}
|
|
177
|
+
onMounted(() => {
|
|
178
|
+
if (strategies.value.includes("mount")) doPrefetch();
|
|
179
|
+
if (strategies.value.includes("visible") && elRef.value) {
|
|
180
|
+
observer = new IntersectionObserver((entries) => {
|
|
181
|
+
if (entries.some((e) => e.isIntersecting)) {
|
|
182
|
+
doPrefetch();
|
|
183
|
+
observer?.disconnect();
|
|
184
|
+
observer = null;
|
|
185
|
+
}
|
|
186
|
+
}, { rootMargin: "0px" });
|
|
187
|
+
observer.observe(elRef.value);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
onUnmounted(() => {
|
|
191
|
+
if (hoverTimer) clearTimeout(hoverTimer);
|
|
192
|
+
if (observer) {
|
|
193
|
+
observer.disconnect();
|
|
194
|
+
observer = null;
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
return () => {
|
|
198
|
+
if (props.prefetch !== false && !isGet.value) throw new Error("Link: prefetch only supports GET links. Remove `prefetch` or use method=\"GET\".");
|
|
199
|
+
if (props.reloadDocument && !isGet.value) throw new Error("Link: reloadDocument only supports GET links.");
|
|
200
|
+
const tag = isGet.value ? "a" : "button";
|
|
201
|
+
const eventHandlers = { onClick };
|
|
202
|
+
if (strategies.value.includes("hover")) {
|
|
203
|
+
eventHandlers.onMouseenter = onMouseEnter;
|
|
204
|
+
eventHandlers.onMouseleave = onMouseLeave;
|
|
205
|
+
eventHandlers.onFocus = onFocus;
|
|
206
|
+
eventHandlers.onTouchstart = onTouchStart;
|
|
207
|
+
}
|
|
208
|
+
if (strategies.value.includes("click")) {
|
|
209
|
+
eventHandlers.onMousedown = onMouseDown;
|
|
210
|
+
eventHandlers.onTouchstart = onTouchStart;
|
|
211
|
+
}
|
|
212
|
+
return h(tag, {
|
|
213
|
+
...attrs,
|
|
214
|
+
ref: elRef,
|
|
215
|
+
...isGet.value ? { href: hrefWithData.value } : { type: "button" },
|
|
216
|
+
style: [{ touchAction: "manipulation" }, attrs.style],
|
|
217
|
+
...eventHandlers
|
|
218
|
+
}, slots.default?.());
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/runtime/use-router.ts
|
|
224
|
+
function useRouter() {
|
|
225
|
+
return inject("__void_router", null) ?? ssrProxy;
|
|
226
|
+
}
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/runtime/use-params.ts
|
|
229
|
+
function useParams() {
|
|
230
|
+
const params = inject("__void_params");
|
|
231
|
+
if (!params) throw new Error("useParams(): must be used inside a Void page.");
|
|
232
|
+
return params;
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region src/runtime/use-shared.ts
|
|
236
|
+
function useShared() {
|
|
237
|
+
const shared = inject("__void_shared");
|
|
238
|
+
if (!shared) throw new Error("useShared(): must be used inside a Void page.");
|
|
239
|
+
return shared;
|
|
240
|
+
}
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region src/runtime/use-form.ts
|
|
243
|
+
function useForm(url, defaults, options) {
|
|
244
|
+
const formDefaults = defaults;
|
|
245
|
+
const router = inject("__void_router", null);
|
|
246
|
+
const data = reactive({ ...formDefaults });
|
|
247
|
+
const errors = reactive({});
|
|
248
|
+
const error = ref(null);
|
|
249
|
+
const pending = ref(false);
|
|
250
|
+
const wasSuccessful = ref(false);
|
|
251
|
+
const recentlySuccessful = ref(false);
|
|
252
|
+
let defaults_ = { ...formDefaults };
|
|
253
|
+
let successTimeout = null;
|
|
254
|
+
const hasChanges = ref(false);
|
|
255
|
+
watch(() => ({ ...data }), (val) => {
|
|
256
|
+
hasChanges.value = Object.keys(defaults_).some((key) => !isEqualFormValue(val[key], defaults_[key]));
|
|
257
|
+
}, { deep: true });
|
|
258
|
+
function reset(...fields) {
|
|
259
|
+
if (fields.length === 0) Object.assign(data, { ...defaults_ });
|
|
260
|
+
else for (const field of fields) data[field] = defaults_[field];
|
|
261
|
+
}
|
|
262
|
+
function clearErrors(...fields) {
|
|
263
|
+
if (fields.length === 0) for (const key of Object.keys(errors)) delete errors[key];
|
|
264
|
+
else for (const field of fields) delete errors[field];
|
|
265
|
+
}
|
|
266
|
+
function clearError() {
|
|
267
|
+
error.value = null;
|
|
268
|
+
}
|
|
269
|
+
async function submit(method, opts = {}) {
|
|
270
|
+
pending.value = true;
|
|
271
|
+
wasSuccessful.value = false;
|
|
272
|
+
recentlySuccessful.value = false;
|
|
273
|
+
if (successTimeout) clearTimeout(successTimeout);
|
|
274
|
+
clearErrors();
|
|
275
|
+
clearError();
|
|
276
|
+
try {
|
|
277
|
+
if (!router) throw new Error("useForm(): requires the Void Router.");
|
|
278
|
+
const result = await submitAction(router, interpolateRouteParams(url, options?.params), {
|
|
279
|
+
method,
|
|
280
|
+
data: { ...data },
|
|
281
|
+
...opts
|
|
282
|
+
});
|
|
283
|
+
if (!result.ok) {
|
|
284
|
+
const validationErrors = getValidationErrors(result.error.body);
|
|
285
|
+
if (validationErrors) Object.assign(errors, validationErrors);
|
|
286
|
+
else error.value = result.error;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
wasSuccessful.value = true;
|
|
290
|
+
recentlySuccessful.value = true;
|
|
291
|
+
defaults_ = { ...data };
|
|
292
|
+
successTimeout = setTimeout(() => {
|
|
293
|
+
recentlySuccessful.value = false;
|
|
294
|
+
}, 2e3);
|
|
295
|
+
} catch (submitError) {
|
|
296
|
+
if (isAbortError(submitError)) return;
|
|
297
|
+
throw submitError;
|
|
298
|
+
} finally {
|
|
299
|
+
pending.value = false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return reactive({
|
|
303
|
+
data,
|
|
304
|
+
errors,
|
|
305
|
+
error,
|
|
306
|
+
pending,
|
|
307
|
+
hasChanges,
|
|
308
|
+
wasSuccessful,
|
|
309
|
+
recentlySuccessful,
|
|
310
|
+
reset,
|
|
311
|
+
clearErrors,
|
|
312
|
+
clearError,
|
|
313
|
+
post: (opts) => submit("POST", opts),
|
|
314
|
+
put: (opts) => submit("PUT", opts),
|
|
315
|
+
patch: (opts) => submit("PATCH", opts),
|
|
316
|
+
delete: (opts) => submit("DELETE", opts)
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/runtime/use-navigation.ts
|
|
321
|
+
function useNavigation() {
|
|
322
|
+
return inject("__void_navigation", null) ?? idleNavigationState();
|
|
323
|
+
}
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region src/runtime/use-island-form.ts
|
|
326
|
+
async function readActionErrorBody(response) {
|
|
327
|
+
const contentType = response.headers.get("content-type") || "";
|
|
328
|
+
try {
|
|
329
|
+
if (contentType.includes("application/json")) return await response.json();
|
|
330
|
+
return await response.text() || null;
|
|
331
|
+
} catch {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Form composable for island pages. Uses fetch + page reload instead of the
|
|
337
|
+
* Void Router (which is not available in island mode).
|
|
338
|
+
*/
|
|
339
|
+
function useIslandForm(defaults) {
|
|
340
|
+
const data = reactive({ ...defaults });
|
|
341
|
+
const errors = reactive({});
|
|
342
|
+
const error = ref(null);
|
|
343
|
+
const pending = ref(false);
|
|
344
|
+
const wasSuccessful = ref(false);
|
|
345
|
+
const recentlySuccessful = ref(false);
|
|
346
|
+
let defaults_ = { ...defaults };
|
|
347
|
+
let successTimeout = null;
|
|
348
|
+
const hasChanges = ref(false);
|
|
349
|
+
watch(() => ({ ...data }), (val) => {
|
|
350
|
+
hasChanges.value = Object.keys(defaults_).some((key) => !isEqualFormValue(val[key], defaults_[key]));
|
|
351
|
+
}, { deep: true });
|
|
352
|
+
function reset(...fields) {
|
|
353
|
+
if (fields.length === 0) Object.assign(data, { ...defaults_ });
|
|
354
|
+
else for (const field of fields) data[field] = defaults_[field];
|
|
355
|
+
}
|
|
356
|
+
function clearErrors(...fields) {
|
|
357
|
+
if (fields.length === 0) for (const key of Object.keys(errors)) delete errors[key];
|
|
358
|
+
else for (const field of fields) delete errors[field];
|
|
359
|
+
}
|
|
360
|
+
function clearError() {
|
|
361
|
+
error.value = null;
|
|
362
|
+
}
|
|
363
|
+
async function submit(url, method) {
|
|
364
|
+
pending.value = true;
|
|
365
|
+
wasSuccessful.value = false;
|
|
366
|
+
recentlySuccessful.value = false;
|
|
367
|
+
if (successTimeout) clearTimeout(successTimeout);
|
|
368
|
+
clearErrors();
|
|
369
|
+
clearError();
|
|
370
|
+
try {
|
|
371
|
+
const response = await fetch(url, {
|
|
372
|
+
method,
|
|
373
|
+
headers: { "Content-Type": "application/json" },
|
|
374
|
+
body: JSON.stringify(data)
|
|
375
|
+
});
|
|
376
|
+
if (response.redirected) {
|
|
377
|
+
window.location.href = response.url;
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (response.ok) {
|
|
381
|
+
wasSuccessful.value = true;
|
|
382
|
+
recentlySuccessful.value = true;
|
|
383
|
+
defaults_ = { ...data };
|
|
384
|
+
successTimeout = setTimeout(() => {
|
|
385
|
+
recentlySuccessful.value = false;
|
|
386
|
+
}, 2e3);
|
|
387
|
+
window.location.reload();
|
|
388
|
+
} else {
|
|
389
|
+
const body = await readActionErrorBody(response);
|
|
390
|
+
const actionError = new VoidActionError({
|
|
391
|
+
body,
|
|
392
|
+
status: response.status,
|
|
393
|
+
statusText: response.statusText,
|
|
394
|
+
url
|
|
395
|
+
});
|
|
396
|
+
const validationErrors = response.status === 422 ? getValidationErrors(body) : null;
|
|
397
|
+
if (validationErrors) {
|
|
398
|
+
Object.assign(errors, validationErrors);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (categorizeActionError(response.status) === "boundary") throw actionError;
|
|
402
|
+
error.value = actionError;
|
|
403
|
+
}
|
|
404
|
+
} finally {
|
|
405
|
+
pending.value = false;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return reactive({
|
|
409
|
+
data,
|
|
410
|
+
errors,
|
|
411
|
+
error,
|
|
412
|
+
pending,
|
|
413
|
+
hasChanges,
|
|
414
|
+
wasSuccessful,
|
|
415
|
+
recentlySuccessful,
|
|
416
|
+
submit,
|
|
417
|
+
reset,
|
|
418
|
+
clearErrors,
|
|
419
|
+
clearError,
|
|
420
|
+
post: (url) => submit(url, "POST"),
|
|
421
|
+
put: (url) => submit(url, "PUT"),
|
|
422
|
+
patch: (url) => submit(url, "PATCH"),
|
|
423
|
+
delete: (url) => submit(url, "DELETE")
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
//#endregion
|
|
427
|
+
//#region src/runtime/action.ts
|
|
428
|
+
const client = createActionClient();
|
|
429
|
+
const action = client.action;
|
|
430
|
+
const setActionRouter = client.setActionRouter;
|
|
431
|
+
//#endregion
|
|
2
432
|
export { Form, Link, action, setActionRouter, useForm, useIslandForm, useNavigation, useParams, useRouter, useShared };
|
package/dist/plugin.d.mts
CHANGED
package/dist/plugin.mjs
CHANGED
|
@@ -51,30 +51,13 @@ const COMPONENT_EXTENSIONS = [
|
|
|
51
51
|
".ts",
|
|
52
52
|
".js"
|
|
53
53
|
];
|
|
54
|
-
function extractCss(filePath) {
|
|
55
|
-
const code = readFileSync(filePath, "utf-8");
|
|
56
|
-
const parts = [];
|
|
57
|
-
const jsRe = /^\s*import\b[^"']*["']([^"']+\.(?:css|scss|sass|less|styl|stylus|pcss|postcss))["']/gm;
|
|
58
|
-
let match;
|
|
59
|
-
while ((match = jsRe.exec(code)) !== null) parts.push(`@import ${JSON.stringify(match[1])};`);
|
|
60
|
-
const styleBlockRe = /<style\b([^>]*)>([\s\S]*?)<\/style>/g;
|
|
61
|
-
while ((match = styleBlockRe.exec(code)) !== null) {
|
|
62
|
-
const attrs = match[1];
|
|
63
|
-
if (/\bscoped\b/.test(attrs)) continue;
|
|
64
|
-
const content = match[2].trim();
|
|
65
|
-
if (content) parts.push(content);
|
|
66
|
-
}
|
|
67
|
-
return parts.join("\n");
|
|
68
|
-
}
|
|
69
54
|
function islandsPlugin(pagesDir, options) {
|
|
70
55
|
let pageScan = null;
|
|
71
56
|
const root = options?.root ?? dirname(pagesDir);
|
|
72
57
|
const islandManifest = /* @__PURE__ */ new Map();
|
|
73
58
|
const islandCssFiles = /* @__PURE__ */ new Set();
|
|
74
59
|
let isDev = false;
|
|
75
|
-
let viteRoot = "";
|
|
76
60
|
let resolvedConfig = null;
|
|
77
|
-
/** Resolve an island import and record it in the client manifest. */
|
|
78
61
|
function registerIsland(spec, importerDir, hydrate) {
|
|
79
62
|
const absPath = resolveSpecifier(spec, importerDir, COMPONENT_EXTENSIONS);
|
|
80
63
|
if (!absPath) return {
|
|
@@ -97,7 +80,6 @@ function islandsPlugin(pagesDir, options) {
|
|
|
97
80
|
enforce: "pre",
|
|
98
81
|
configResolved(config) {
|
|
99
82
|
resolvedConfig = config;
|
|
100
|
-
viteRoot = config.root;
|
|
101
83
|
isDev = config.command === "serve";
|
|
102
84
|
},
|
|
103
85
|
configureServer(server) {
|
|
@@ -138,21 +120,13 @@ function islandsPlugin(pagesDir, options) {
|
|
|
138
120
|
}
|
|
139
121
|
},
|
|
140
122
|
resolveId: {
|
|
141
|
-
filter: { id: /^virtual:void-islands-(renderer|client)
|
|
123
|
+
filter: { id: /^virtual:void-islands-(renderer|client)$/ },
|
|
142
124
|
handler(source) {
|
|
143
125
|
if (source === ISLANDS_RENDERER_ID || source === ISLANDS_CLIENT_ENTRY_ID) return "\0" + source;
|
|
144
|
-
if (source.endsWith(".css")) {
|
|
145
|
-
const srcPath = normalizePath(source.slice(0, -4));
|
|
146
|
-
if (islandCssFiles.has(srcPath)) return srcPath + ".css";
|
|
147
|
-
if (srcPath.startsWith("/")) {
|
|
148
|
-
const absPath = normalizePath(resolve(viteRoot, srcPath.slice(1)));
|
|
149
|
-
if (islandCssFiles.has(absPath)) return absPath + ".css";
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
126
|
}
|
|
153
127
|
},
|
|
154
128
|
load: {
|
|
155
|
-
filter: { id: /^\0virtual:void-islands-(renderer|client)
|
|
129
|
+
filter: { id: /^\0virtual:void-islands-(renderer|client)$/ },
|
|
156
130
|
handler(id) {
|
|
157
131
|
if (id === "\0" + ISLANDS_RENDERER_ID) {
|
|
158
132
|
if (!pageScan) return "export function renderIslandPage() { throw new Error('No pages found'); }";
|
|
@@ -165,10 +139,6 @@ function islandsPlugin(pagesDir, options) {
|
|
|
165
139
|
if (!pageScan) return "";
|
|
166
140
|
return generateIslandClientEntry(islandManifest, islandCssFiles, isDev);
|
|
167
141
|
}
|
|
168
|
-
if (id.endsWith(".css") && !id.startsWith("\0")) {
|
|
169
|
-
const srcPath = id.slice(0, -4);
|
|
170
|
-
if (islandCssFiles.has(srcPath)) return extractCss(srcPath);
|
|
171
|
-
}
|
|
172
142
|
}
|
|
173
143
|
},
|
|
174
144
|
transform: {
|
|
@@ -248,7 +218,6 @@ export async function renderIslandPage(pageObj, assetTags) {
|
|
|
248
218
|
const app = createSSRApp({
|
|
249
219
|
setup() {
|
|
250
220
|
provide("__void_shared", reactive(pageObj.shared || {}));
|
|
251
|
-
provide("__void_errors", reactive(pageObj.errors || {}));
|
|
252
221
|
provide("__void_params", reactive(pageObj.params || {}));
|
|
253
222
|
if (__frontmatter) provide("__void_frontmatter", __frontmatter);
|
|
254
223
|
// No router provided — islands have no Void Router
|
|
@@ -499,7 +468,6 @@ export async function renderPage(pageObj, assetTags) {
|
|
|
499
468
|
const app = createSSRApp({
|
|
500
469
|
setup() {
|
|
501
470
|
provide("__void_shared", reactive(pageObj.shared || {}));
|
|
502
|
-
provide("__void_errors", reactive(pageObj.errors || {}));
|
|
503
471
|
provide("__void_params", reactive(pageObj.params || {}));
|
|
504
472
|
provide("__void_router", createSsrRouter(pageObj.url || "/", pageObj.params || {}));
|
|
505
473
|
provide("__void_navigation", idleNavigationState());
|
|
@@ -544,7 +512,6 @@ import { parseCacheFor } from "void/pages-prefetch";
|
|
|
544
512
|
${generateComponentManifest(scan, pagesDir)}
|
|
545
513
|
|
|
546
514
|
const shared = reactive({});
|
|
547
|
-
const errors = reactive({});
|
|
548
515
|
const params = reactive({});
|
|
549
516
|
const currentComponent = shallowRef(null);
|
|
550
517
|
const currentLayouts = shallowRef([]);
|
|
@@ -574,7 +541,6 @@ const { router, initialPageData, initialComponent, initialLayouts, start, commit
|
|
|
574
541
|
applyUpdate(pageData, comp, layouts, options) {
|
|
575
542
|
if (!options?.preserveState) currentPageKey.value++;
|
|
576
543
|
replaceRecord(shared, pageData.shared || {});
|
|
577
|
-
replaceRecord(errors, pageData.errors || {});
|
|
578
544
|
replaceRecord(params, pageData.params || {});
|
|
579
545
|
currentProps.value = pageData.props;
|
|
580
546
|
currentLayouts.value = layouts;
|
|
@@ -603,7 +569,6 @@ const reactiveRouter = createRouterFacade(router, () => routeUrl.value, () => pa
|
|
|
603
569
|
setActionRouter(reactiveRouter);
|
|
604
570
|
|
|
605
571
|
Object.assign(shared, initialPageData.shared || {});
|
|
606
|
-
replaceRecord(errors, initialPageData.errors || {});
|
|
607
572
|
replaceRecord(params, initialPageData.params || {});
|
|
608
573
|
currentProps.value = initialPageData.props;
|
|
609
574
|
currentComponent.value = initialComponent;
|
|
@@ -615,7 +580,6 @@ const createVoidApp = appEl.getAttribute("data-void-ssr") === "false" ? createAp
|
|
|
615
580
|
const app = createVoidApp({
|
|
616
581
|
setup() {
|
|
617
582
|
provide("__void_shared", shared);
|
|
618
|
-
provide("__void_errors", errors);
|
|
619
583
|
provide("__void_params", params);
|
|
620
584
|
provide("__void_router", reactiveRouter);
|
|
621
585
|
provide("__void_navigation", navigation);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@void/vue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Vue pages, server rendering, forms, and navigation for Void.",
|
|
5
5
|
"homepage": "https://void.cloud",
|
|
6
6
|
"bugs": {
|
|
@@ -34,19 +34,19 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@vitejs/plugin-vue": "^6.0.
|
|
37
|
+
"@vitejs/plugin-vue": "^6.0.10"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@types/node": "^26.6.
|
|
40
|
+
"@types/node": "^26.6.5",
|
|
41
41
|
"pathslash": "^0.1.0",
|
|
42
|
-
"vite": "npm:@voidzero-dev/vite-plus-core@1.
|
|
43
|
-
"vite-plus": "1.
|
|
44
|
-
"void": "0.
|
|
45
|
-
"vue": "^3.5.
|
|
42
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@1.2.0",
|
|
43
|
+
"vite-plus": "1.2.0",
|
|
44
|
+
"void": "1.0.3",
|
|
45
|
+
"vue": "^3.5.44"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"vite": "^8.0.0",
|
|
49
|
-
"void": "0.
|
|
49
|
+
"void": "1.0.3",
|
|
50
50
|
"vue": "^3.5.0"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { ActionClient, NavigationState, VisitOptions, VoidActionError, VoidRouter } from "void/pages-client";
|
|
2
|
-
import { Component } from "vue";
|
|
3
|
-
import { CloudContextVariables, DeferredState as Deferred, InferProps } from "void";
|
|
4
|
-
import { ActionFormOptions, ActionUrl, ResolveActionBody } from "void/routes";
|
|
5
|
-
//#region src/runtime/form.d.ts
|
|
6
|
-
declare const Form: Component;
|
|
7
|
-
//#endregion
|
|
8
|
-
//#region src/runtime/link.d.ts
|
|
9
|
-
declare const Link: Component;
|
|
10
|
-
//#endregion
|
|
11
|
-
//#region src/runtime/use-router.d.ts
|
|
12
|
-
declare function useRouter(): VoidRouter;
|
|
13
|
-
//#endregion
|
|
14
|
-
//#region src/runtime/use-params.d.ts
|
|
15
|
-
declare function useParams<T extends Record<string, string> = Record<string, string>>(): T;
|
|
16
|
-
//#endregion
|
|
17
|
-
//#region src/runtime/use-shared.d.ts
|
|
18
|
-
declare function useShared<T = CloudContextVariables["shared"]>(): T;
|
|
19
|
-
//#endregion
|
|
20
|
-
//#region src/runtime/use-form.d.ts
|
|
21
|
-
type FormSubmitOptions = Omit<VisitOptions, "method" | "data"> & {
|
|
22
|
-
data?: Record<string, unknown>;
|
|
23
|
-
};
|
|
24
|
-
declare function useForm<U extends ActionUrl & string>(url: U, defaults: ResolveActionBody<U>, options?: ActionFormOptions<U>): {
|
|
25
|
-
data: ResolveActionBody<U>;
|
|
26
|
-
errors: Partial<Record<keyof ResolveActionBody<U> & string, string>>;
|
|
27
|
-
error: VoidActionError | null;
|
|
28
|
-
pending: boolean;
|
|
29
|
-
hasChanges: boolean;
|
|
30
|
-
wasSuccessful: boolean;
|
|
31
|
-
recentlySuccessful: boolean;
|
|
32
|
-
post(opts?: FormSubmitOptions): Promise<void>;
|
|
33
|
-
put(opts?: FormSubmitOptions): Promise<void>;
|
|
34
|
-
patch(opts?: FormSubmitOptions): Promise<void>;
|
|
35
|
-
delete(opts?: FormSubmitOptions): Promise<void>;
|
|
36
|
-
reset(...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
|
|
37
|
-
clearErrors(...fields: ReadonlyArray<keyof ResolveActionBody<U> & string>): void;
|
|
38
|
-
clearError(): void;
|
|
39
|
-
};
|
|
40
|
-
//#endregion
|
|
41
|
-
//#region src/runtime/use-navigation.d.ts
|
|
42
|
-
declare function useNavigation(): NavigationState;
|
|
43
|
-
//#endregion
|
|
44
|
-
//#region src/runtime/use-island-form.d.ts
|
|
45
|
-
type IslandFormReturn<T extends Record<string, unknown>> = Readonly<{
|
|
46
|
-
data: T;
|
|
47
|
-
errors: Record<string, string>;
|
|
48
|
-
error: VoidActionError | null;
|
|
49
|
-
pending: boolean;
|
|
50
|
-
hasChanges: boolean;
|
|
51
|
-
wasSuccessful: boolean;
|
|
52
|
-
recentlySuccessful: boolean;
|
|
53
|
-
submit(url: string, method: string): Promise<void>;
|
|
54
|
-
post(url: string): Promise<void>;
|
|
55
|
-
put(url: string): Promise<void>;
|
|
56
|
-
patch(url: string): Promise<void>;
|
|
57
|
-
delete(url: string): Promise<void>;
|
|
58
|
-
reset(...fields: ReadonlyArray<keyof T & string>): void;
|
|
59
|
-
clearErrors(...fields: ReadonlyArray<string>): void;
|
|
60
|
-
clearError(): void;
|
|
61
|
-
}>;
|
|
62
|
-
/**
|
|
63
|
-
* Form composable for island pages. Uses fetch + page reload instead of the
|
|
64
|
-
* Void Router (which is not available in island mode).
|
|
65
|
-
*/
|
|
66
|
-
declare function useIslandForm<T extends Record<string, unknown>>(defaults: T): IslandFormReturn<T>;
|
|
67
|
-
//#endregion
|
|
68
|
-
//#region src/runtime/action.d.ts
|
|
69
|
-
declare const action: ActionClient["action"];
|
|
70
|
-
declare const setActionRouter: ActionClient["setActionRouter"];
|
|
71
|
-
//#endregion
|
|
72
|
-
export { useIslandForm as a, useShared as c, Link as d, Form as f, setActionRouter as i, useParams as l, InferProps as n, useNavigation as o, action as r, useForm as s, Deferred as t, useRouter as u };
|
package/dist/runtime/index.d.mts
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import { a as useIslandForm, c as useShared, d as Link, f as Form, i as setActionRouter, l as useParams, n as InferProps, o as useNavigation, r as action, s as useForm, t as Deferred, u as useRouter } from "../index-vAbysUNV.mjs";
|
|
2
|
-
export { type Deferred, Form, type InferProps, Link, action, setActionRouter, useForm, useIslandForm, useNavigation, useParams, useRouter, useShared };
|
package/dist/runtime/index.mjs
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import { a as useForm, c as useRouter, i as useNavigation, l as Link, n as setActionRouter, o as useShared, r as useIslandForm, s as useParams, t as action, u as Form } from "../runtime-lPB1mgy1.mjs";
|
|
2
|
-
export { Form, Link, action, setActionRouter, useForm, useIslandForm, useNavigation, useParams, useRouter, useShared };
|
|
@@ -1,436 +0,0 @@
|
|
|
1
|
-
import { VoidActionError, categorizeActionError, createActionClient, formErrorMessage, formRetryMessage, formSubmissionBlocked, getValidationErrors, idleNavigationState, interpolateRouteParams, isAbortError, isEqualFormValue, isModifiedEvent, mergeDataIntoHref, scheduleFormRetry, ssrProxy, submitAction } from "void/pages-client";
|
|
2
|
-
import { computed, defineComponent, h, inject, onMounted, onUnmounted, reactive, ref, watch } from "vue";
|
|
3
|
-
//#region src/runtime/form.ts
|
|
4
|
-
const Form = defineComponent({
|
|
5
|
-
name: "VoidForm",
|
|
6
|
-
inheritAttrs: false,
|
|
7
|
-
props: {
|
|
8
|
-
form: {
|
|
9
|
-
type: Object,
|
|
10
|
-
required: true
|
|
11
|
-
},
|
|
12
|
-
method: {
|
|
13
|
-
type: String,
|
|
14
|
-
default: "post"
|
|
15
|
-
},
|
|
16
|
-
renderError: Function
|
|
17
|
-
},
|
|
18
|
-
setup(props, { attrs, slots }) {
|
|
19
|
-
const now = ref(Date.now());
|
|
20
|
-
let cancel = () => {};
|
|
21
|
-
onMounted(() => watch(() => props.form.error, (error) => {
|
|
22
|
-
cancel();
|
|
23
|
-
cancel = scheduleFormRetry(error, () => {
|
|
24
|
-
now.value = Date.now();
|
|
25
|
-
});
|
|
26
|
-
}, { immediate: true }));
|
|
27
|
-
onUnmounted(() => cancel());
|
|
28
|
-
return () => {
|
|
29
|
-
const form = props.form;
|
|
30
|
-
const message = formErrorMessage(form), retry = formRetryMessage(form.error);
|
|
31
|
-
return h("form", {
|
|
32
|
-
...attrs,
|
|
33
|
-
"aria-busy": form.pending,
|
|
34
|
-
onSubmit: async (event) => {
|
|
35
|
-
event.preventDefault();
|
|
36
|
-
if (!formSubmissionBlocked(form, now.value)) await form[props.method]();
|
|
37
|
-
}
|
|
38
|
-
}, [message ? h("div", {
|
|
39
|
-
role: "alert",
|
|
40
|
-
"data-void-form-error": ""
|
|
41
|
-
}, [form.error && props.renderError ? props.renderError(form.error) : message, retry ? h("p", retry) : null]) : null, slots.default?.()]);
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
//#endregion
|
|
46
|
-
//#region src/runtime/link.ts
|
|
47
|
-
function hasPrefetch(prefetch) {
|
|
48
|
-
return prefetch !== false;
|
|
49
|
-
}
|
|
50
|
-
function callHandler(handler, event) {
|
|
51
|
-
if (Array.isArray(handler)) {
|
|
52
|
-
for (const fn of handler) if (typeof fn === "function") fn(event);
|
|
53
|
-
} else if (typeof handler === "function") handler(event);
|
|
54
|
-
}
|
|
55
|
-
const Link = defineComponent({
|
|
56
|
-
name: "Link",
|
|
57
|
-
props: {
|
|
58
|
-
href: {
|
|
59
|
-
type: String,
|
|
60
|
-
required: true
|
|
61
|
-
},
|
|
62
|
-
method: {
|
|
63
|
-
type: String,
|
|
64
|
-
default: "GET"
|
|
65
|
-
},
|
|
66
|
-
data: {
|
|
67
|
-
type: Object,
|
|
68
|
-
default: void 0
|
|
69
|
-
},
|
|
70
|
-
preserveScroll: {
|
|
71
|
-
type: Boolean,
|
|
72
|
-
default: false
|
|
73
|
-
},
|
|
74
|
-
preserveState: {
|
|
75
|
-
type: Boolean,
|
|
76
|
-
default: false
|
|
77
|
-
},
|
|
78
|
-
replace: {
|
|
79
|
-
type: Boolean,
|
|
80
|
-
default: false
|
|
81
|
-
},
|
|
82
|
-
reloadDocument: {
|
|
83
|
-
type: Boolean,
|
|
84
|
-
default: false
|
|
85
|
-
},
|
|
86
|
-
viewTransition: {
|
|
87
|
-
type: Boolean,
|
|
88
|
-
default: void 0
|
|
89
|
-
},
|
|
90
|
-
onNavigate: {
|
|
91
|
-
type: Function,
|
|
92
|
-
default: void 0
|
|
93
|
-
},
|
|
94
|
-
prefetch: {
|
|
95
|
-
type: [
|
|
96
|
-
Boolean,
|
|
97
|
-
String,
|
|
98
|
-
Array
|
|
99
|
-
],
|
|
100
|
-
default: false
|
|
101
|
-
},
|
|
102
|
-
cacheFor: {
|
|
103
|
-
type: [
|
|
104
|
-
Number,
|
|
105
|
-
String,
|
|
106
|
-
Array
|
|
107
|
-
],
|
|
108
|
-
default: void 0
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
setup(props, { slots, attrs }) {
|
|
112
|
-
const router = inject("__void_router", null);
|
|
113
|
-
const elRef = ref(null);
|
|
114
|
-
const normalizedMethod = computed(() => props.method.toUpperCase());
|
|
115
|
-
const isGet = computed(() => normalizedMethod.value === "GET");
|
|
116
|
-
const hrefWithData = computed(() => isGet.value ? mergeDataIntoHref(props.href, props.data) : props.href);
|
|
117
|
-
const strategies = computed(() => {
|
|
118
|
-
if (props.prefetch === false || props.reloadDocument) return [];
|
|
119
|
-
if (props.prefetch === true) return ["hover"];
|
|
120
|
-
if (typeof props.prefetch === "string") return [props.prefetch];
|
|
121
|
-
return props.prefetch;
|
|
122
|
-
});
|
|
123
|
-
function doPrefetch() {
|
|
124
|
-
if (!router?.prefetch) return;
|
|
125
|
-
const current = window.location.pathname + window.location.search;
|
|
126
|
-
if (hrefWithData.value === current) return;
|
|
127
|
-
const options = {};
|
|
128
|
-
if (props.cacheFor !== void 0) options.cacheFor = props.cacheFor;
|
|
129
|
-
if (normalizedMethod.value) options.method = normalizedMethod.value;
|
|
130
|
-
router.prefetch(hrefWithData.value, options);
|
|
131
|
-
}
|
|
132
|
-
function onClick(e) {
|
|
133
|
-
callHandler(attrs.onClick, e);
|
|
134
|
-
if (props.reloadDocument || !router || e.defaultPrevented || isModifiedEvent(e)) return;
|
|
135
|
-
if (e.currentTarget.hasAttribute("download")) return;
|
|
136
|
-
if (new URL(hrefWithData.value, window.location.origin).origin !== window.location.origin) return;
|
|
137
|
-
let navigatePrevented = false;
|
|
138
|
-
props.onNavigate?.({ preventDefault() {
|
|
139
|
-
navigatePrevented = true;
|
|
140
|
-
} });
|
|
141
|
-
if (navigatePrevented) {
|
|
142
|
-
e.preventDefault();
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
e.preventDefault();
|
|
146
|
-
router.visit(hrefWithData.value, {
|
|
147
|
-
method: normalizedMethod.value,
|
|
148
|
-
data: isGet.value ? void 0 : props.data,
|
|
149
|
-
preserveScroll: props.preserveScroll,
|
|
150
|
-
preserveState: props.preserveState,
|
|
151
|
-
replace: props.replace,
|
|
152
|
-
viewTransition: props.viewTransition
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
let hoverTimer = null;
|
|
156
|
-
let observer = null;
|
|
157
|
-
function onMouseEnter(e) {
|
|
158
|
-
callHandler(attrs.onMouseenter ?? attrs.onMouseEnter, e);
|
|
159
|
-
if (e.defaultPrevented) return;
|
|
160
|
-
hoverTimer = setTimeout(doPrefetch, router?._hoverDelay ?? 75);
|
|
161
|
-
}
|
|
162
|
-
function onMouseLeave(e) {
|
|
163
|
-
callHandler(attrs.onMouseleave ?? attrs.onMouseLeave, e);
|
|
164
|
-
if (hoverTimer) {
|
|
165
|
-
clearTimeout(hoverTimer);
|
|
166
|
-
hoverTimer = null;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
function onFocus(e) {
|
|
170
|
-
callHandler(attrs.onFocus, e);
|
|
171
|
-
if (!e.defaultPrevented) doPrefetch();
|
|
172
|
-
}
|
|
173
|
-
function onMouseDown(e) {
|
|
174
|
-
callHandler(attrs.onMousedown ?? attrs.onMouseDown, e);
|
|
175
|
-
if (!e.defaultPrevented) doPrefetch();
|
|
176
|
-
}
|
|
177
|
-
function onTouchStart(e) {
|
|
178
|
-
callHandler(attrs.onTouchstart ?? attrs.onTouchStart, e);
|
|
179
|
-
if (!e.defaultPrevented) doPrefetch();
|
|
180
|
-
}
|
|
181
|
-
onMounted(() => {
|
|
182
|
-
if (strategies.value.includes("mount")) doPrefetch();
|
|
183
|
-
if (strategies.value.includes("visible") && elRef.value) {
|
|
184
|
-
observer = new IntersectionObserver((entries) => {
|
|
185
|
-
if (entries.some((e) => e.isIntersecting)) {
|
|
186
|
-
doPrefetch();
|
|
187
|
-
observer?.disconnect();
|
|
188
|
-
observer = null;
|
|
189
|
-
}
|
|
190
|
-
}, { rootMargin: "0px" });
|
|
191
|
-
observer.observe(elRef.value);
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
onUnmounted(() => {
|
|
195
|
-
if (hoverTimer) clearTimeout(hoverTimer);
|
|
196
|
-
if (observer) {
|
|
197
|
-
observer.disconnect();
|
|
198
|
-
observer = null;
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
return () => {
|
|
202
|
-
if (hasPrefetch(props.prefetch) && !isGet.value) throw new Error("Link: prefetch only supports GET links. Remove `prefetch` or use method=\"GET\".");
|
|
203
|
-
if (props.reloadDocument && !isGet.value) throw new Error("Link: reloadDocument only supports GET links.");
|
|
204
|
-
const tag = isGet.value ? "a" : "button";
|
|
205
|
-
const eventHandlers = { onClick };
|
|
206
|
-
if (strategies.value.includes("hover")) {
|
|
207
|
-
eventHandlers.onMouseenter = onMouseEnter;
|
|
208
|
-
eventHandlers.onMouseleave = onMouseLeave;
|
|
209
|
-
eventHandlers.onFocus = onFocus;
|
|
210
|
-
eventHandlers.onTouchstart = onTouchStart;
|
|
211
|
-
}
|
|
212
|
-
if (strategies.value.includes("click")) {
|
|
213
|
-
eventHandlers.onMousedown = onMouseDown;
|
|
214
|
-
eventHandlers.onTouchstart = onTouchStart;
|
|
215
|
-
}
|
|
216
|
-
return h(tag, {
|
|
217
|
-
...attrs,
|
|
218
|
-
ref: elRef,
|
|
219
|
-
...isGet.value ? { href: hrefWithData.value } : { type: "button" },
|
|
220
|
-
style: [{ touchAction: "manipulation" }, attrs.style],
|
|
221
|
-
...eventHandlers
|
|
222
|
-
}, slots.default?.());
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
//#endregion
|
|
227
|
-
//#region src/runtime/use-router.ts
|
|
228
|
-
function useRouter() {
|
|
229
|
-
return inject("__void_router", null) ?? ssrProxy;
|
|
230
|
-
}
|
|
231
|
-
//#endregion
|
|
232
|
-
//#region src/runtime/use-params.ts
|
|
233
|
-
function useParams() {
|
|
234
|
-
const params = inject("__void_params");
|
|
235
|
-
if (!params) throw new Error("useParams(): must be used inside a Void page.");
|
|
236
|
-
return params;
|
|
237
|
-
}
|
|
238
|
-
//#endregion
|
|
239
|
-
//#region src/runtime/use-shared.ts
|
|
240
|
-
function useShared() {
|
|
241
|
-
const shared = inject("__void_shared");
|
|
242
|
-
if (!shared) throw new Error("useShared(): must be used inside a Void page.");
|
|
243
|
-
return shared;
|
|
244
|
-
}
|
|
245
|
-
//#endregion
|
|
246
|
-
//#region src/runtime/use-form.ts
|
|
247
|
-
function useForm(url, defaults, options) {
|
|
248
|
-
const formDefaults = defaults;
|
|
249
|
-
const router = inject("__void_router", null);
|
|
250
|
-
const data = reactive({ ...formDefaults });
|
|
251
|
-
const errors = reactive({});
|
|
252
|
-
const error = ref(null);
|
|
253
|
-
const pending = ref(false);
|
|
254
|
-
const wasSuccessful = ref(false);
|
|
255
|
-
const recentlySuccessful = ref(false);
|
|
256
|
-
let defaults_ = { ...formDefaults };
|
|
257
|
-
let successTimeout = null;
|
|
258
|
-
const hasChanges = ref(false);
|
|
259
|
-
watch(() => ({ ...data }), (val) => {
|
|
260
|
-
hasChanges.value = Object.keys(defaults_).some((key) => !isEqualFormValue(val[key], defaults_[key]));
|
|
261
|
-
}, { deep: true });
|
|
262
|
-
function reset(...fields) {
|
|
263
|
-
if (fields.length === 0) Object.assign(data, { ...defaults_ });
|
|
264
|
-
else for (const field of fields) data[field] = defaults_[field];
|
|
265
|
-
}
|
|
266
|
-
function clearErrors(...fields) {
|
|
267
|
-
if (fields.length === 0) for (const key of Object.keys(errors)) delete errors[key];
|
|
268
|
-
else for (const field of fields) delete errors[field];
|
|
269
|
-
}
|
|
270
|
-
function clearError() {
|
|
271
|
-
error.value = null;
|
|
272
|
-
}
|
|
273
|
-
async function submit(method, opts = {}) {
|
|
274
|
-
pending.value = true;
|
|
275
|
-
wasSuccessful.value = false;
|
|
276
|
-
recentlySuccessful.value = false;
|
|
277
|
-
if (successTimeout) clearTimeout(successTimeout);
|
|
278
|
-
clearErrors();
|
|
279
|
-
clearError();
|
|
280
|
-
try {
|
|
281
|
-
if (!router) throw new Error("useForm(): requires the Void Router.");
|
|
282
|
-
const result = await submitAction(router, interpolateRouteParams(url, options?.params), {
|
|
283
|
-
method,
|
|
284
|
-
data: { ...data },
|
|
285
|
-
...opts
|
|
286
|
-
});
|
|
287
|
-
if (!result.ok) {
|
|
288
|
-
const validationErrors = getValidationErrors(result.error.body);
|
|
289
|
-
if (validationErrors) Object.assign(errors, validationErrors);
|
|
290
|
-
else error.value = result.error;
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
wasSuccessful.value = true;
|
|
294
|
-
recentlySuccessful.value = true;
|
|
295
|
-
defaults_ = { ...data };
|
|
296
|
-
successTimeout = setTimeout(() => {
|
|
297
|
-
recentlySuccessful.value = false;
|
|
298
|
-
}, 2e3);
|
|
299
|
-
} catch (submitError) {
|
|
300
|
-
if (isAbortError(submitError)) return;
|
|
301
|
-
throw submitError;
|
|
302
|
-
} finally {
|
|
303
|
-
pending.value = false;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
return reactive({
|
|
307
|
-
data,
|
|
308
|
-
errors,
|
|
309
|
-
error,
|
|
310
|
-
pending,
|
|
311
|
-
hasChanges,
|
|
312
|
-
wasSuccessful,
|
|
313
|
-
recentlySuccessful,
|
|
314
|
-
reset,
|
|
315
|
-
clearErrors,
|
|
316
|
-
clearError,
|
|
317
|
-
post: (opts) => submit("POST", opts),
|
|
318
|
-
put: (opts) => submit("PUT", opts),
|
|
319
|
-
patch: (opts) => submit("PATCH", opts),
|
|
320
|
-
delete: (opts) => submit("DELETE", opts)
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
//#endregion
|
|
324
|
-
//#region src/runtime/use-navigation.ts
|
|
325
|
-
function useNavigation() {
|
|
326
|
-
return inject("__void_navigation", null) ?? idleNavigationState();
|
|
327
|
-
}
|
|
328
|
-
//#endregion
|
|
329
|
-
//#region src/runtime/use-island-form.ts
|
|
330
|
-
async function readActionErrorBody(response) {
|
|
331
|
-
const contentType = response.headers.get("content-type") || "";
|
|
332
|
-
try {
|
|
333
|
-
if (contentType.includes("application/json")) return await response.json();
|
|
334
|
-
return await response.text() || null;
|
|
335
|
-
} catch {
|
|
336
|
-
return null;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* Form composable for island pages. Uses fetch + page reload instead of the
|
|
341
|
-
* Void Router (which is not available in island mode).
|
|
342
|
-
*/
|
|
343
|
-
function useIslandForm(defaults) {
|
|
344
|
-
const data = reactive({ ...defaults });
|
|
345
|
-
const errors = reactive({});
|
|
346
|
-
const error = ref(null);
|
|
347
|
-
const pending = ref(false);
|
|
348
|
-
const wasSuccessful = ref(false);
|
|
349
|
-
const recentlySuccessful = ref(false);
|
|
350
|
-
let defaults_ = { ...defaults };
|
|
351
|
-
let successTimeout = null;
|
|
352
|
-
const hasChanges = ref(false);
|
|
353
|
-
watch(() => ({ ...data }), (val) => {
|
|
354
|
-
hasChanges.value = Object.keys(defaults_).some((key) => !isEqualFormValue(val[key], defaults_[key]));
|
|
355
|
-
}, { deep: true });
|
|
356
|
-
function reset(...fields) {
|
|
357
|
-
if (fields.length === 0) Object.assign(data, { ...defaults_ });
|
|
358
|
-
else for (const field of fields) data[field] = defaults_[field];
|
|
359
|
-
}
|
|
360
|
-
function clearErrors(...fields) {
|
|
361
|
-
if (fields.length === 0) for (const key of Object.keys(errors)) delete errors[key];
|
|
362
|
-
else for (const field of fields) delete errors[field];
|
|
363
|
-
}
|
|
364
|
-
function clearError() {
|
|
365
|
-
error.value = null;
|
|
366
|
-
}
|
|
367
|
-
async function submit(url, method) {
|
|
368
|
-
pending.value = true;
|
|
369
|
-
wasSuccessful.value = false;
|
|
370
|
-
recentlySuccessful.value = false;
|
|
371
|
-
if (successTimeout) clearTimeout(successTimeout);
|
|
372
|
-
clearErrors();
|
|
373
|
-
clearError();
|
|
374
|
-
try {
|
|
375
|
-
const response = await fetch(url, {
|
|
376
|
-
method,
|
|
377
|
-
headers: { "Content-Type": "application/json" },
|
|
378
|
-
body: JSON.stringify(data)
|
|
379
|
-
});
|
|
380
|
-
if (response.redirected) {
|
|
381
|
-
window.location.href = response.url;
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
if (response.ok) {
|
|
385
|
-
wasSuccessful.value = true;
|
|
386
|
-
recentlySuccessful.value = true;
|
|
387
|
-
defaults_ = { ...data };
|
|
388
|
-
successTimeout = setTimeout(() => {
|
|
389
|
-
recentlySuccessful.value = false;
|
|
390
|
-
}, 2e3);
|
|
391
|
-
window.location.reload();
|
|
392
|
-
} else {
|
|
393
|
-
const body = await readActionErrorBody(response);
|
|
394
|
-
const actionError = new VoidActionError({
|
|
395
|
-
body,
|
|
396
|
-
status: response.status,
|
|
397
|
-
statusText: response.statusText,
|
|
398
|
-
url
|
|
399
|
-
});
|
|
400
|
-
const validationErrors = response.status === 422 ? getValidationErrors(body) : null;
|
|
401
|
-
if (validationErrors) {
|
|
402
|
-
Object.assign(errors, validationErrors);
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
if (categorizeActionError(response.status) === "boundary") throw actionError;
|
|
406
|
-
error.value = actionError;
|
|
407
|
-
}
|
|
408
|
-
} finally {
|
|
409
|
-
pending.value = false;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
return reactive({
|
|
413
|
-
data,
|
|
414
|
-
errors,
|
|
415
|
-
error,
|
|
416
|
-
pending,
|
|
417
|
-
hasChanges,
|
|
418
|
-
wasSuccessful,
|
|
419
|
-
recentlySuccessful,
|
|
420
|
-
submit,
|
|
421
|
-
reset,
|
|
422
|
-
clearErrors,
|
|
423
|
-
clearError,
|
|
424
|
-
post: (url) => submit(url, "POST"),
|
|
425
|
-
put: (url) => submit(url, "PUT"),
|
|
426
|
-
patch: (url) => submit(url, "PATCH"),
|
|
427
|
-
delete: (url) => submit(url, "DELETE")
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
//#endregion
|
|
431
|
-
//#region src/runtime/action.ts
|
|
432
|
-
const client = createActionClient();
|
|
433
|
-
const action = client.action;
|
|
434
|
-
const setActionRouter = client.setActionRouter;
|
|
435
|
-
//#endregion
|
|
436
|
-
export { useForm as a, useRouter as c, useNavigation as i, Link as l, setActionRouter as n, useShared as o, useIslandForm as r, useParams as s, action as t, Form as u };
|