@void/svelte 0.0.0

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.
@@ -0,0 +1,205 @@
1
+ import { getContext } from 'svelte';
2
+ import {
3
+ VoidActionError,
4
+ isAbortError,
5
+ isEqualFormValue,
6
+ submitAction,
7
+ type VisitOptions,
8
+ type VoidRouter,
9
+ } from 'void/pages-client';
10
+ import type { ActionUrl, ResolveActionBody, ActionFormOptions } from 'void/routes';
11
+
12
+ type FormSubmitOptions = Omit<VisitOptions, 'method' | 'data'> & {
13
+ data?: Record<string, unknown>;
14
+ };
15
+ type UntypedForm = {
16
+ readonly data: Record<string, unknown>;
17
+ readonly errors: Record<string, string>;
18
+ readonly error: VoidActionError | null;
19
+ readonly pending: boolean;
20
+ readonly hasChanges: boolean;
21
+ readonly wasSuccessful: boolean;
22
+ readonly recentlySuccessful: boolean;
23
+ post(opts?: FormSubmitOptions): Promise<void>;
24
+ put(opts?: FormSubmitOptions): Promise<void>;
25
+ patch(opts?: FormSubmitOptions): Promise<void>;
26
+ delete(opts?: FormSubmitOptions): Promise<void>;
27
+ reset(...fields: Array<string>): void;
28
+ clearErrors(...fields: Array<string>): void;
29
+ clearError(): void;
30
+ };
31
+
32
+ function getValidationErrors(error: VoidActionError): Record<string, string> | null {
33
+ const body = error.body;
34
+ if (
35
+ body &&
36
+ typeof body === 'object' &&
37
+ 'errors' in body &&
38
+ body.errors &&
39
+ typeof body.errors === 'object' &&
40
+ !Array.isArray(body.errors)
41
+ ) {
42
+ return body.errors as Record<string, string>;
43
+ }
44
+ return null;
45
+ }
46
+
47
+ // Typed overload — full inference from PageActionMap (requires codegen)
48
+ export function useForm<U extends ActionUrl & string>(
49
+ url: U,
50
+ defaults: ResolveActionBody<U>,
51
+ options?: ActionFormOptions<U>,
52
+ ): {
53
+ readonly data: ResolveActionBody<U>;
54
+ readonly errors: Partial<Record<keyof ResolveActionBody<U> & string, string>>;
55
+ readonly error: VoidActionError | null;
56
+ readonly pending: boolean;
57
+ readonly hasChanges: boolean;
58
+ readonly wasSuccessful: boolean;
59
+ readonly recentlySuccessful: boolean;
60
+ post(opts?: FormSubmitOptions): Promise<void>;
61
+ put(opts?: FormSubmitOptions): Promise<void>;
62
+ patch(opts?: FormSubmitOptions): Promise<void>;
63
+ delete(opts?: FormSubmitOptions): Promise<void>;
64
+ reset(...fields: Array<keyof ResolveActionBody<U> & string>): void;
65
+ clearErrors(...fields: Array<keyof ResolveActionBody<U> & string>): void;
66
+ clearError(): void;
67
+ };
68
+
69
+ export function useForm(
70
+ url: string,
71
+ defaults: Record<string, unknown>,
72
+ options?: { params?: Record<string, string> },
73
+ ): UntypedForm {
74
+ let resolvedUrl = url;
75
+
76
+ // Strip ?action query before param resolution, then re-append
77
+ let actionQuery = '';
78
+ const qIdx = resolvedUrl.indexOf('?');
79
+ if (qIdx !== -1) {
80
+ actionQuery = resolvedUrl.slice(qIdx);
81
+ resolvedUrl = resolvedUrl.slice(0, qIdx);
82
+ }
83
+
84
+ if (options?.params) {
85
+ for (const [key, value] of Object.entries(options.params)) {
86
+ resolvedUrl = resolvedUrl.replace(`:${key}`, encodeURIComponent(value));
87
+ }
88
+ }
89
+
90
+ resolvedUrl = resolvedUrl + actionQuery;
91
+
92
+ const router = getContext<VoidRouter>('__void_router');
93
+
94
+ let data = $state({ ...defaults });
95
+ let errors: Record<string, string> = $state({});
96
+ let error: VoidActionError | null = $state(null);
97
+ let pending = $state(false);
98
+ let wasSuccessful = $state(false);
99
+ let recentlySuccessful = $state(false);
100
+
101
+ let defaults_ = { ...defaults };
102
+ let successTimeout: ReturnType<typeof setTimeout> | null = null;
103
+
104
+ const hasChanges = $derived(
105
+ Object.keys(defaults_).some((key) => !isEqualFormValue(data[key], defaults_[key])),
106
+ );
107
+
108
+ function reset(...fields: Array<string>) {
109
+ if (fields.length === 0) {
110
+ data = { ...defaults_ };
111
+ } else {
112
+ for (const field of fields) {
113
+ data[field] = defaults_[field];
114
+ }
115
+ }
116
+ }
117
+
118
+ function clearErrors(...fields: Array<string>) {
119
+ if (fields.length === 0) {
120
+ errors = {};
121
+ } else {
122
+ for (const field of fields) {
123
+ delete errors[field];
124
+ }
125
+ }
126
+ }
127
+
128
+ function clearError() {
129
+ error = null;
130
+ }
131
+
132
+ async function submit(method: string, opts: FormSubmitOptions = {}) {
133
+ pending = true;
134
+ wasSuccessful = false;
135
+ recentlySuccessful = false;
136
+ if (successTimeout) {
137
+ clearTimeout(successTimeout);
138
+ }
139
+ clearErrors();
140
+ clearError();
141
+
142
+ try {
143
+ const result = await submitAction(router, resolvedUrl, {
144
+ method,
145
+ data: { ...data },
146
+ ...opts,
147
+ });
148
+
149
+ if (!result.ok) {
150
+ const validationErrors = getValidationErrors(result.error);
151
+ if (validationErrors) {
152
+ Object.assign(errors, validationErrors);
153
+ } else {
154
+ error = result.error;
155
+ }
156
+ return;
157
+ }
158
+
159
+ wasSuccessful = true;
160
+ recentlySuccessful = true;
161
+ defaults_ = { ...data };
162
+ successTimeout = setTimeout(() => {
163
+ recentlySuccessful = false;
164
+ }, 2000);
165
+ } catch (submitError) {
166
+ if (isAbortError(submitError)) {
167
+ return;
168
+ }
169
+ throw submitError;
170
+ } finally {
171
+ pending = false;
172
+ }
173
+ }
174
+
175
+ return {
176
+ get data() {
177
+ return data;
178
+ },
179
+ get errors() {
180
+ return errors;
181
+ },
182
+ get error() {
183
+ return error;
184
+ },
185
+ get pending() {
186
+ return pending;
187
+ },
188
+ get hasChanges() {
189
+ return hasChanges;
190
+ },
191
+ get wasSuccessful() {
192
+ return wasSuccessful;
193
+ },
194
+ get recentlySuccessful() {
195
+ return recentlySuccessful;
196
+ },
197
+ reset,
198
+ clearErrors,
199
+ clearError,
200
+ post: (opts?: FormSubmitOptions) => submit('POST', opts),
201
+ put: (opts?: FormSubmitOptions) => submit('PUT', opts),
202
+ patch: (opts?: FormSubmitOptions) => submit('PATCH', opts),
203
+ delete: (opts?: FormSubmitOptions) => submit('DELETE', opts),
204
+ };
205
+ }
@@ -0,0 +1,175 @@
1
+ import { VoidActionError, categorizeActionError, isEqualFormValue } from 'void/pages-client';
2
+
3
+ type UntypedIslandForm = {
4
+ readonly data: Record<string, unknown>;
5
+ readonly errors: Record<string, string>;
6
+ readonly error: VoidActionError | null;
7
+ readonly pending: boolean;
8
+ readonly hasChanges: boolean;
9
+ readonly wasSuccessful: boolean;
10
+ readonly recentlySuccessful: boolean;
11
+ submit(url: string, method: string): Promise<void>;
12
+ post(url: string): Promise<void>;
13
+ put(url: string): Promise<void>;
14
+ patch(url: string): Promise<void>;
15
+ delete(url: string): Promise<void>;
16
+ reset(...fields: Array<string>): void;
17
+ clearErrors(...fields: Array<string>): void;
18
+ clearError(): void;
19
+ };
20
+
21
+ function getValidationErrors(body: unknown): Record<string, string> | null {
22
+ if (
23
+ body &&
24
+ typeof body === 'object' &&
25
+ 'errors' in body &&
26
+ body.errors &&
27
+ typeof body.errors === 'object' &&
28
+ !Array.isArray(body.errors)
29
+ ) {
30
+ return body.errors as Record<string, string>;
31
+ }
32
+ return null;
33
+ }
34
+
35
+ async function readActionErrorBody(response: Response): Promise<unknown> {
36
+ const contentType = response.headers.get('content-type') || '';
37
+ try {
38
+ if (contentType.includes('application/json')) {
39
+ return await response.json();
40
+ }
41
+ const text = await response.text();
42
+ return text || null;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Form composable for island pages. Uses fetch + page reload instead of the
50
+ * Void Router (which is not available in island mode).
51
+ */
52
+ export function useIslandForm(defaults: Record<string, unknown>): UntypedIslandForm {
53
+ let data: Record<string, unknown> = $state({ ...defaults });
54
+ let errors: Record<string, string> = $state({});
55
+ let error: VoidActionError | null = $state(null);
56
+ let pending = $state(false);
57
+ let wasSuccessful = $state(false);
58
+ let recentlySuccessful = $state(false);
59
+
60
+ let defaults_ = { ...defaults };
61
+ let successTimeout: ReturnType<typeof setTimeout> | null = null;
62
+
63
+ const hasChanges = $derived(
64
+ Object.keys(defaults_).some((key) => !isEqualFormValue(data[key], defaults_[key])),
65
+ );
66
+
67
+ function reset(...fields: Array<string>) {
68
+ if (fields.length === 0) {
69
+ data = { ...defaults_ };
70
+ } else {
71
+ for (const field of fields) {
72
+ data[field] = defaults_[field];
73
+ }
74
+ }
75
+ }
76
+
77
+ function clearErrors(...fields: Array<string>) {
78
+ if (fields.length === 0) {
79
+ errors = {};
80
+ } else {
81
+ for (const field of fields) {
82
+ delete errors[field];
83
+ }
84
+ }
85
+ }
86
+
87
+ function clearError() {
88
+ error = null;
89
+ }
90
+
91
+ async function submit(url: string, method: string) {
92
+ pending = true;
93
+ wasSuccessful = false;
94
+ recentlySuccessful = false;
95
+ if (successTimeout) {
96
+ clearTimeout(successTimeout);
97
+ }
98
+ clearErrors();
99
+ clearError();
100
+
101
+ try {
102
+ const response = await fetch(url, {
103
+ method,
104
+ headers: { 'Content-Type': 'application/json' },
105
+ body: JSON.stringify(data),
106
+ });
107
+
108
+ if (response.redirected) {
109
+ window.location.href = response.url;
110
+ return;
111
+ }
112
+
113
+ if (response.ok) {
114
+ wasSuccessful = true;
115
+ recentlySuccessful = true;
116
+ defaults_ = { ...data };
117
+ successTimeout = setTimeout(() => {
118
+ recentlySuccessful = false;
119
+ }, 2000);
120
+ window.location.reload();
121
+ } else if (!response.ok) {
122
+ const body = await readActionErrorBody(response);
123
+ const actionError = new VoidActionError({
124
+ body,
125
+ status: response.status,
126
+ statusText: response.statusText,
127
+ url,
128
+ });
129
+ const validationErrors = response.status === 422 ? getValidationErrors(body) : null;
130
+ if (validationErrors) {
131
+ Object.assign(errors, validationErrors);
132
+ return;
133
+ }
134
+ if (categorizeActionError(response.status) === 'boundary') {
135
+ throw actionError;
136
+ }
137
+ error = actionError;
138
+ }
139
+ } finally {
140
+ pending = false;
141
+ }
142
+ }
143
+
144
+ return {
145
+ get data() {
146
+ return data;
147
+ },
148
+ get errors() {
149
+ return errors;
150
+ },
151
+ get error() {
152
+ return error;
153
+ },
154
+ get pending() {
155
+ return pending;
156
+ },
157
+ get hasChanges() {
158
+ return hasChanges;
159
+ },
160
+ get wasSuccessful() {
161
+ return wasSuccessful;
162
+ },
163
+ get recentlySuccessful() {
164
+ return recentlySuccessful;
165
+ },
166
+ submit,
167
+ reset,
168
+ clearErrors,
169
+ clearError,
170
+ post: (url: string) => submit(url, 'POST'),
171
+ put: (url: string) => submit(url, 'PUT'),
172
+ patch: (url: string) => submit(url, 'PATCH'),
173
+ delete: (url: string) => submit(url, 'DELETE'),
174
+ };
175
+ }
@@ -0,0 +1,22 @@
1
+ import { getContext } from 'svelte';
2
+ import { idleNavigationState, type NavigationState } from 'void/pages-client';
3
+
4
+ export type { NavigationState } from 'void/pages-client';
5
+
6
+ export function useNavigation(): NavigationState {
7
+ const ctx = getContext<{ value: NavigationState }>('__void_navigation');
8
+ if (!ctx) {
9
+ return idleNavigationState();
10
+ }
11
+ return {
12
+ get state() {
13
+ return ctx.value.state;
14
+ },
15
+ get location() {
16
+ return ctx.value.location;
17
+ },
18
+ get method() {
19
+ return ctx.value.method;
20
+ },
21
+ };
22
+ }
@@ -0,0 +1,9 @@
1
+ import { getContext } from 'svelte';
2
+ import { ssrProxy } from 'void/pages-client';
3
+ import type { VoidRouter } from 'void/pages-client';
4
+
5
+ export type { VoidRouter } from 'void/pages-client';
6
+
7
+ export function useRouter(): VoidRouter {
8
+ return getContext<VoidRouter>('__void_router') ?? ssrProxy;
9
+ }
@@ -0,0 +1,10 @@
1
+ import { getContext } from 'svelte';
2
+ import type { CloudContextVariables } from 'void';
3
+
4
+ export function useShared<T = CloudContextVariables['shared']>(): T {
5
+ const ctx = getContext<{ value: T }>('__void_shared');
6
+ if (!ctx) {
7
+ throw new Error('useShared(): must be used inside a Void page.');
8
+ }
9
+ return ctx.value;
10
+ }