@getmicdrop/svelte-components 5.3.13 → 5.3.15

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,410 @@
1
+ /**
2
+ * @deprecated Use `@getmicdrop/svelte-components/forms` instead.
3
+ * This Svelte 4 store-based implementation will be removed in the next major version.
4
+ *
5
+ * Migration:
6
+ * ```typescript
7
+ * // Old (Svelte 4 stores):
8
+ * import { createFormStore } from '@getmicdrop/svelte-components/stores/createFormStore';
9
+ * const form = createFormStore({ initialData: {...}, endpoint: '...' });
10
+ *
11
+ * // New (Svelte 5 runes):
12
+ * import { createFormStore } from '@getmicdrop/svelte-components/forms';
13
+ * import { z } from 'zod';
14
+ * const schema = z.object({ name: z.string(), email: z.string().email() });
15
+ * const form = createFormStore(schema, { name: '', email: '' });
16
+ * ```
17
+ */
18
+ import { writable, derived, get } from "svelte/store";
19
+ import { showToast } from "./toaster";
20
+
21
+ /**
22
+ * @deprecated Use createFormStore from '@getmicdrop/svelte-components/forms' instead.
23
+ * @typedef {Object} FormStoreOptions
24
+ * @property {Object} initialData - Initial form data
25
+ * @property {string} [endpoint] - API endpoint for saving
26
+ * @property {string} [successMessage] - Message to show on success
27
+ * @property {string} [errorMessage] - Message to show on error
28
+ * @property {Function} [validate] - Validation function returning { isValid, errors }
29
+ * @property {Function} [transformData] - Transform data before saving
30
+ * @property {Function} [onSuccess] - Callback after successful save
31
+ * @property {Function} [onError] - Callback after error
32
+ * @property {boolean} [showToasts=true] - Show toast notifications
33
+ */
34
+
35
+ /**
36
+ * @deprecated Use createFormStore from '@getmicdrop/svelte-components/forms' instead.
37
+ *
38
+ * Creates a comprehensive form store with state management, dirty tracking,
39
+ * validation, and save handling.
40
+ *
41
+ * @param {FormStoreOptions} options - Configuration options
42
+ * @returns {Object} Form store with all state and handlers
43
+ *
44
+ * @example
45
+ * const form = createFormStore({
46
+ * initialData: { name: '', email: '' },
47
+ * endpoint: '/api/profile',
48
+ * validate: (data) => {
49
+ * const errors = {};
50
+ * if (!data.name) errors.name = 'Name is required';
51
+ * return { isValid: Object.keys(errors).length === 0, errors };
52
+ * }
53
+ * });
54
+ *
55
+ * // In Svelte component:
56
+ * <input bind:value={$form.data.name} on:input={form.checkDirty} />
57
+ * <button on:click={form.submit} disabled={$form.isLoading}>Save</button>
58
+ */
59
+ export function createFormStore(options = {}) {
60
+ console.warn(
61
+ '[DEPRECATED] createFormStore from stores/createFormStore is deprecated. ' +
62
+ 'Use createFormStore from @getmicdrop/svelte-components/forms instead.'
63
+ );
64
+ const {
65
+ initialData = {},
66
+ endpoint = "",
67
+ successMessage = "Changes saved successfully",
68
+ errorMessage = "Failed to save changes",
69
+ validate,
70
+ transformData,
71
+ onSuccess,
72
+ onError,
73
+ showToasts = true,
74
+ } = options;
75
+
76
+ // Deep clone initial data to prevent mutations
77
+ const cloneData = (data) => JSON.parse(JSON.stringify(data));
78
+
79
+ // Core state stores
80
+ const data = writable(cloneData(initialData));
81
+ const initial = writable(cloneData(initialData));
82
+ const errors = writable({});
83
+ const isLoading = writable(false);
84
+ const isSuccess = writable(false);
85
+ const saveError = writable(null);
86
+ const showErrors = writable(false);
87
+
88
+ // Derived states
89
+ const isDirty = derived(
90
+ [data, initial],
91
+ ([$data, $initial]) => JSON.stringify($data) !== JSON.stringify($initial)
92
+ );
93
+
94
+ const isValid = derived(
95
+ errors,
96
+ ($errors) => countErrors($errors) === 0
97
+ );
98
+
99
+ const errorCount = derived(
100
+ errors,
101
+ ($errors) => countErrors($errors)
102
+ );
103
+
104
+ const errorList = derived(
105
+ errors,
106
+ ($errors) => flattenErrors($errors)
107
+ );
108
+
109
+ /**
110
+ * Count total errors in a nested error object
111
+ */
112
+ function countErrors(errorObj) {
113
+ let count = 0;
114
+ const traverse = (obj) => {
115
+ if (!obj || typeof obj !== 'object') return;
116
+ Object.values(obj).forEach(value => {
117
+ if (typeof value === 'string' && value) {
118
+ count++;
119
+ } else if (typeof value === 'object') {
120
+ traverse(value);
121
+ }
122
+ });
123
+ };
124
+ traverse(errorObj);
125
+ return count;
126
+ }
127
+
128
+ /**
129
+ * Flatten nested errors into array of { field, message, section }
130
+ */
131
+ function flattenErrors(errorObj) {
132
+ const list = [];
133
+ const traverse = (obj, section = '') => {
134
+ if (!obj || typeof obj !== 'object') return;
135
+ Object.entries(obj).forEach(([key, value]) => {
136
+ if (typeof value === 'string' && value) {
137
+ list.push({ field: key, message: value, section });
138
+ } else if (typeof value === 'object') {
139
+ traverse(value, key);
140
+ }
141
+ });
142
+ };
143
+ traverse(errorObj);
144
+ return list;
145
+ }
146
+
147
+ /**
148
+ * Update form data (partial update)
149
+ */
150
+ function updateData(updates) {
151
+ data.update(current => ({ ...current, ...updates }));
152
+ }
153
+
154
+ /**
155
+ * Update a specific section of form data
156
+ */
157
+ function updateSection(section, updates) {
158
+ data.update(current => ({
159
+ ...current,
160
+ [section]: { ...current[section], ...updates }
161
+ }));
162
+ }
163
+
164
+ /**
165
+ * Update a specific field
166
+ */
167
+ function updateField(section, field, value) {
168
+ data.update(current => ({
169
+ ...current,
170
+ [section]: { ...current[section], [field]: value }
171
+ }));
172
+ }
173
+
174
+ /**
175
+ * Check if form is dirty (call after data changes)
176
+ */
177
+ function checkDirty() {
178
+ // Dirty state is computed automatically via derived store
179
+ // Reset success state when dirty
180
+ const dirty = get(isDirty);
181
+ if (dirty) {
182
+ isSuccess.set(false);
183
+ }
184
+ return dirty;
185
+ }
186
+
187
+ /**
188
+ * Run validation and update errors
189
+ * @returns {boolean} Whether form is valid
190
+ */
191
+ function runValidation() {
192
+ if (!validate) {
193
+ errors.set({});
194
+ return true;
195
+ }
196
+
197
+ const currentData = get(data);
198
+ const result = validate(currentData);
199
+
200
+ if (typeof result === 'boolean') {
201
+ // Simple boolean validation
202
+ return result;
203
+ }
204
+
205
+ // Object-based validation { isValid, errors }
206
+ errors.set(result.errors || {});
207
+ return result.isValid;
208
+ }
209
+
210
+ /**
211
+ * Clear all errors
212
+ */
213
+ function clearErrors() {
214
+ errors.set({});
215
+ showErrors.set(false);
216
+ }
217
+
218
+ /**
219
+ * Clear error for specific field
220
+ */
221
+ function clearFieldError(section, field) {
222
+ errors.update(current => {
223
+ if (current[section]) {
224
+ const updated = { ...current };
225
+ updated[section] = { ...current[section] };
226
+ delete updated[section][field];
227
+ return updated;
228
+ }
229
+ return current;
230
+ });
231
+ }
232
+
233
+ /**
234
+ * Set error for specific field
235
+ */
236
+ function setFieldError(section, field, message) {
237
+ errors.update(current => ({
238
+ ...current,
239
+ [section]: { ...current[section], [field]: message }
240
+ }));
241
+ }
242
+
243
+ /**
244
+ * Submit the form
245
+ * @param {string} [customEndpoint] - Override default endpoint
246
+ * @returns {Promise<boolean>} Success status
247
+ */
248
+ async function submit(customEndpoint = null) {
249
+ showErrors.set(true);
250
+
251
+ // Run validation
252
+ const valid = runValidation();
253
+ if (!valid) {
254
+ return false;
255
+ }
256
+
257
+ const targetEndpoint = customEndpoint || endpoint;
258
+ if (!targetEndpoint) {
259
+ console.error("No endpoint specified for form save");
260
+ return false;
261
+ }
262
+
263
+ isLoading.set(true);
264
+ isSuccess.set(false);
265
+ saveError.set(null);
266
+
267
+ try {
268
+ let submitData = get(data);
269
+
270
+ // Transform data if transformer provided
271
+ if (transformData) {
272
+ submitData = transformData(submitData);
273
+ }
274
+
275
+ const res = await fetch(targetEndpoint, {
276
+ method: "POST",
277
+ headers: { "Content-Type": "application/json" },
278
+ body: JSON.stringify(submitData),
279
+ });
280
+
281
+ if (res.ok) {
282
+ isSuccess.set(true);
283
+ // Update initial data to current (form is now clean)
284
+ initial.set(cloneData(get(data)));
285
+
286
+ if (showToasts && successMessage) {
287
+ showToast(successMessage, "success");
288
+ }
289
+ onSuccess?.();
290
+ return true;
291
+ } else {
292
+ const errorData = await res.json().catch(() => ({}));
293
+ const errorMsg = errorData.message || errorMessage;
294
+ saveError.set(errorMsg);
295
+
296
+ if (showToasts) {
297
+ showToast(errorMsg, "error");
298
+ }
299
+ onError?.(errorMsg);
300
+ return false;
301
+ }
302
+ } catch (err) {
303
+ const errorMsg = err.message || "Something went wrong";
304
+ saveError.set(errorMsg);
305
+
306
+ if (showToasts) {
307
+ showToast(errorMsg, "error");
308
+ }
309
+ onError?.(errorMsg);
310
+ return false;
311
+ } finally {
312
+ isLoading.set(false);
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Reset form to initial data
318
+ */
319
+ function reset() {
320
+ data.set(cloneData(get(initial)));
321
+ errors.set({});
322
+ showErrors.set(false);
323
+ isSuccess.set(false);
324
+ saveError.set(null);
325
+ }
326
+
327
+ /**
328
+ * Reset form to new initial data
329
+ */
330
+ function resetTo(newInitialData) {
331
+ const cloned = cloneData(newInitialData);
332
+ initial.set(cloned);
333
+ data.set(cloned);
334
+ errors.set({});
335
+ showErrors.set(false);
336
+ isSuccess.set(false);
337
+ saveError.set(null);
338
+ }
339
+
340
+ /**
341
+ * Reset only the success state
342
+ */
343
+ function resetSuccess() {
344
+ isSuccess.set(false);
345
+ }
346
+
347
+ /**
348
+ * Mark form as saved (update initial to match current)
349
+ */
350
+ function markSaved() {
351
+ initial.set(cloneData(get(data)));
352
+ isSuccess.set(true);
353
+ }
354
+
355
+ // Create a subscribable store that combines all state
356
+ const combinedStore = derived(
357
+ [data, errors, isLoading, isSuccess, saveError, isDirty, isValid, errorCount, showErrors],
358
+ ([$data, $errors, $isLoading, $isSuccess, $saveError, $isDirty, $isValid, $errorCount, $showErrors]) => ({
359
+ data: $data,
360
+ errors: $errors,
361
+ isLoading: $isLoading,
362
+ isSuccess: $isSuccess,
363
+ saveError: $saveError,
364
+ isDirty: $isDirty,
365
+ isValid: $isValid,
366
+ errorCount: $errorCount,
367
+ showErrors: $showErrors,
368
+ })
369
+ );
370
+
371
+ return {
372
+ // Subscribable combined store
373
+ subscribe: combinedStore.subscribe,
374
+
375
+ // Individual stores (for direct subscription)
376
+ data,
377
+ errors,
378
+ isLoading,
379
+ isSuccess,
380
+ saveError,
381
+ isDirty,
382
+ isValid,
383
+ errorCount,
384
+ errorList,
385
+ showErrors,
386
+
387
+ // Actions
388
+ updateData,
389
+ updateSection,
390
+ updateField,
391
+ checkDirty,
392
+ runValidation,
393
+ clearErrors,
394
+ clearFieldError,
395
+ setFieldError,
396
+ submit,
397
+ reset,
398
+ resetTo,
399
+ resetSuccess,
400
+ markSaved,
401
+
402
+ // Convenience getters
403
+ get currentData() { return get(data); },
404
+ get currentErrors() { return get(errors); },
405
+ get loading() { return get(isLoading); },
406
+ get success() { return get(isSuccess); },
407
+ get dirty() { return get(isDirty); },
408
+ get valid() { return get(isValid); },
409
+ };
410
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=createFormStore.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createFormStore.spec.d.ts","sourceRoot":"","sources":["../../src/lib/stores/createFormStore.spec.js"],"names":[],"mappings":""}