@picked/svelte 0.0.1-dev.1767659629925 → 0.0.1-dev.1767832235641

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.
@@ -1 +1,16 @@
1
- export { getForm } from './form.svelte';
1
+ import { ValidationError } from '@picked/client';
2
+ interface GetFormOptions {
3
+ id: string;
4
+ title: string;
5
+ onSuccess?: () => void | Promise<void>;
6
+ onError?: (errors: ValidationError[]) => void | Promise<void>;
7
+ }
8
+ type SubmitEventWithCurrentTarget = SubmitEvent & {
9
+ currentTarget: EventTarget & HTMLFormElement;
10
+ };
11
+ export declare function getForm({ id: formId, title, onError, onSuccess }: GetFormOptions): {
12
+ form: {
13
+ onsubmit: import('effector').EventCallable<SubmitEventWithCurrentTarget>;
14
+ };
15
+ };
16
+ export {};
@@ -1 +1 @@
1
- import{getForm as e}from"./form.svelte";export{e as getForm};
1
+ import{createEvent as k,createEffect as m,attach as b,sample as g,split as D}from"effector";import{getPicked as F,getExperience as S}from"../hooks/client.js";import{getStep as B}from"../hooks/step.js";import"svelte";import"svelte/store";import"@picked/client";import"@picked/analytics";function z({id:f,title:l,onError:d,onSuccess:h}){const o=F(),x=B(),n=S(),{currentStepStore:u}=n,v=k(),y=m(r=>o.responses.commit(r)),A=b({source:{step:u},effect:m(({step:r})=>{o.eventBus({type:"form_submit_success",form:{id:f,title:l},experience:n,step:r??void 0}),h==null||h()})}),_=b({source:{step:u},mapParams:(r,a)=>({step:a.step,errors:r.errors}),effect:m(({step:r,errors:a})=>{o.eventBus({type:"form_submit_error",form:{id:f,title:l},exception:{reason:a},experience:n,step:r??void 0}),a.forEach(p=>{o.eventBus({type:"field_validation_error",exception:{reason:[p]},experience:n,step:r??void 0})}),d==null||d(a)})});return g({clock:v,source:{step:u},fn:({step:r})=>({formId:f,title:l,step:r}),target:m(({formId:r,title:a,step:p})=>{o.eventBus({type:"form_submit",form:{id:r,title:a},experience:n,step:p??void 0})})}),g({clock:v,source:{fields:o.fields.listStore,stepFields:x.fields,step:u},fn:({stepFields:r},a)=>{a.preventDefault();const p=new FormData(a.currentTarget),E=Array.from(p.entries()),c={data:{},push(t){const s=t.questionId,e=this.data[s];e?Array.isArray(e)&&Array.isArray(t.value)?Array.isArray(e)&&Array.isArray(t.value)&&(typeof e[0]=="string"?this.data[s]=[...e,...t.value].map(String):typeof e[0]=="number"?this.data[s]=[...e,...t.value].map(Number):typeof e[0]=="boolean"?this.data[s]=[...e,...t.value].map(i=>!!i):this.data[s]=t.value):this.data[s]=t.value:this.data[s]=t.value}};return r.forEach(t=>{let s=E.filter(([e])=>e===t.id);s.length===0&&(s=[[t.id,""]]),s.forEach(([,e])=>{const i=t.id;if(e===""){c.push({questionId:i,value:null});return}switch(t.type){case"text":c.push({questionId:i,value:t.multiple?[String(e)]:String(e)});break;case"number":c.push({questionId:i,value:t.multiple?[Number(e)]:Number(e)});break;case"boolean":c.push({questionId:i,value:t.multiple?[e==="true"]:e==="true"});break}})}),c.data},target:y}),D({source:y.doneData,match:{hasErrors:({errors:r})=>r.length>0},cases:{hasErrors:_,__:A}}),{form:{onsubmit:v}}}export{z as getForm};
@@ -1,6 +1,5 @@
1
1
  export { getPicked, getExperience } from './client';
2
- export { getStep, setStepContext, type FieldWithValue } from './step';
3
- export { getField } from './field';
2
+ export { getStep, setStepContext } from './step';
4
3
  export { useUnit, getContextScope, setContextScope } from './stores';
5
4
  export { createSessionStorageStore, createLocalStorageStore } from './storage.js';
6
5
  export { getTrackRecommendationPresentEvent } from './events';
@@ -1 +1 @@
1
- import{getExperience as r,getPicked as o}from"./client.js";import{getStep as c,setStepContext as x}from"./step.js";import{getField as a}from"./field.js";import{getContextScope as S,setContextScope as g,useUnit as s}from"./stores.js";import{createLocalStorageStore as i,createSessionStorageStore as u}from"./storage.js";import{getTrackRecommendationPresentEvent as d}from"./events.js";import{createQuery as P,queryCache as k,queryPresets as l}from"./create-query/index.js";export{i as createLocalStorageStore,P as createQuery,u as createSessionStorageStore,S as getContextScope,r as getExperience,a as getField,o as getPicked,c as getStep,d as getTrackRecommendationPresentEvent,k as queryCache,l as queryPresets,g as setContextScope,x as setStepContext,s as useUnit};
1
+ import{getExperience as r,getPicked as o}from"./client.js";import{getStep as p,setStepContext as n}from"./step.js";import{getContextScope as a,setContextScope as S,useUnit as m}from"./stores.js";import{createLocalStorageStore as g,createSessionStorageStore as f}from"./storage.js";import{getTrackRecommendationPresentEvent as u}from"./events.js";import{createQuery as y,queryCache as P,queryPresets as d}from"./create-query/index.js";export{g as createLocalStorageStore,y as createQuery,f as createSessionStorageStore,a as getContextScope,r as getExperience,o as getPicked,p as getStep,u as getTrackRecommendationPresentEvent,P as queryCache,d as queryPresets,S as setContextScope,n as setStepContext,m as useUnit};
@@ -1,30 +1,13 @@
1
1
  import { Store } from 'effector';
2
2
  import { ExperienceStep, Field, Response } from '@picked/client';
3
- import { getForm } from '../form';
4
- export type FieldWithValue = Field & {
3
+ type FieldWithValue = Field & {
5
4
  value: Response | undefined;
6
5
  };
7
6
  export interface StepContext {
8
7
  step: Store<ExperienceStep>;
9
8
  fields: Store<FieldWithValue[]>;
10
9
  isActiveStep: Store<boolean>;
11
- form: ReturnType<typeof getForm>;
12
10
  }
13
- export declare function getStep(): {
14
- form: {
15
- onsubmit: import('effector').EventCallable<SubmitEvent & {
16
- currentTarget: EventTarget & HTMLFormElement;
17
- }>;
18
- };
19
- onFormSubmit: (callback: (data: import('@picked/client').CommitResponses) => import('@picked/client').CommitResponses | void | Promise<import('@picked/client').CommitResponses | void>) => void;
20
- onFormSuccess: (callback: (options: {
21
- data: import('@picked/client').CommitResponses;
22
- stopNavigation: import('effector').EventCallable<void>;
23
- }) => void | Promise<void>) => void;
24
- onFormError: (callback: (errors: import('@picked/client').ValidationError[]) => void | Promise<void>) => void;
25
- isFormSubmitting: import('effector').StoreWritable<boolean>;
26
- step: Store<ExperienceStep>;
27
- fields: Store<FieldWithValue[]>;
28
- isActiveStep: Store<boolean>;
29
- };
11
+ export declare function getStep(): StepContext;
30
12
  export declare function setStepContext(stepId: string): StepContext;
13
+ export {};
@@ -1 +1 @@
1
- import{getContext as h,setContext as w}from"svelte";import{createStore as C,combine as l,attach as F,createEffect as A,sample as m}from"effector";import{getStepFields as T}from"@picked/client";import{STEP_CONTEXT_KEY as d}from"../context.js";import{getStepRouter as b}from"../router/context.js";import{getForm as P}from"../form/form.svelte";import{getExperience as _,getPicked as $}from"./client.js";function V(){const t=h(d),{form:i}=t;return{...t,...i}}function W(t){const i=_(),n=$(),{activeStep:u}=b(),f=u.map(e=>(e==null?void 0:e.id)===t),s=i.treeStore.map(e=>{const r=e.steps.find(o=>o.id===t);if(!r)throw new Error(`Step with id "${t}" not found in the experience tree.`);return r}),c=C([]),S=l({fields:c,responses:n.responses.listStore},({fields:e,responses:r})=>e.map(o=>{var p;const v=(p=r.find(E=>E.fieldId===o.id))==null?void 0:p.value;return{...o,value:v}})),a=F({source:l({step:s,fields:n.fields.listStore}),effect:A(async({step:{fieldIds:e},fields:r})=>await T(n,r.filter(o=>e.includes(o.id))))}),{label:g}=s.getState(),x=P({id:`${t}`,title:g||t,fields:c});return m({source:{isActiveStep:f},filter:({isActiveStep:e})=>e,target:a}),m({source:a.doneData,target:c}),w(d,{step:s,fields:S,isActiveStep:f,form:x})}export{V as getStep,W as setStepContext};
1
+ import{getContext as v,setContext as E}from"svelte";import{createStore as h,combine as a,attach as w,createEffect as C,sample as l}from"effector";import{getStepFields as A}from"@picked/client";import{STEP_CONTEXT_KEY as d}from"../context.js";import{getStepRouter as F}from"../router/context.js";import{getExperience as T,getPicked as P}from"./client.js";function N(){return v(d)}function O(o){const u=T(),i=P(),{activeStep:m}=F(),n=m.map(e=>(e==null?void 0:e.id)===o),s=u.treeStore.map(e=>{const t=e.steps.find(r=>r.id===o);if(!t)throw new Error(`Step with id "${o}" not found in the experience tree.`);return t}),c=h([]),S=a({fields:c,responses:i.responses.listStore},({fields:e,responses:t})=>e.map(r=>{var f;const g=(f=t.find(x=>x.fieldId===r.id))==null?void 0:f.value;return{...r,value:g}})),p=w({source:a({step:s,fields:i.fields.listStore}),effect:C(async({step:{fieldIds:e},fields:t})=>await A(i,t.filter(r=>e.includes(r.id))))});return l({source:{isActiveStep:n},filter:({isActiveStep:e})=>e,target:p}),l({source:p.doneData,target:c}),E(d,{step:s,fields:S,isActiveStep:n})}export{N as getStep,O as setStepContext};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@picked/svelte",
3
- "version": "0.0.1-dev.1767659629925",
3
+ "version": "0.0.1-dev.1767832235641",
4
4
  "dependencies": {
5
5
  "d3": "^7.9.0"
6
6
  },
@@ -75,7 +75,7 @@
75
75
  ],
76
76
  "module": "./dist/index.js",
77
77
  "peerDependencies": {
78
- "@picked/client": "0.0.1-dev.1767659629925",
78
+ "@picked/client": "0.0.1-dev.1767832235641",
79
79
  "effector": "^23.3.0",
80
80
  "svelte": "^5.34.3"
81
81
  },
@@ -1,27 +0,0 @@
1
- import { CommitResponses, Field, ValidationError } from '@picked/client';
2
- import { EventCallable, Store } from 'effector';
3
- import { FieldWithValue } from '../hooks';
4
- interface GetFormOptions {
5
- id: string;
6
- title: string;
7
- fields: Store<Field[] | FieldWithValue[]>;
8
- }
9
- type SuccessCallback = (options: {
10
- data: CommitResponses;
11
- stopNavigation: EventCallable<void>;
12
- }) => void | Promise<void>;
13
- type ErrorCallback = (errors: ValidationError[]) => void | Promise<void>;
14
- type SubmitCallback = (data: CommitResponses) => CommitResponses | void | Promise<CommitResponses | void>;
15
- type SubmitEventWithCurrentTarget = SubmitEvent & {
16
- currentTarget: EventTarget & HTMLFormElement;
17
- };
18
- export declare function getForm({ id: formId, title, fields }: GetFormOptions): {
19
- form: {
20
- onsubmit: EventCallable<SubmitEventWithCurrentTarget>;
21
- };
22
- onFormSubmit: (callback: SubmitCallback) => void;
23
- onFormSuccess: (callback: SuccessCallback) => void;
24
- onFormError: (callback: ErrorCallback) => void;
25
- isFormSubmitting: import('effector').StoreWritable<boolean>;
26
- };
27
- export {};
@@ -1,379 +0,0 @@
1
- import type {
2
- CommitResponses,
3
- ExperienceStep,
4
- Field,
5
- FieldValue,
6
- ValidationError,
7
- ExperienceInstance,
8
- } from '@picked/client'
9
- import {
10
- attach,
11
- createEffect,
12
- createEvent,
13
- createStore,
14
- sample,
15
- split,
16
- type EventCallable,
17
- type Store,
18
- } from 'effector'
19
- import { getPicked, getExperience, type FieldWithValue } from '../hooks'
20
-
21
- interface Response {
22
- questionId: Field['id']
23
- value: FieldValue
24
- }
25
-
26
- interface GetFormOptions {
27
- id: string
28
- title: string
29
- fields: Store<Field[] | FieldWithValue[]>
30
- }
31
-
32
- interface CommitResult {
33
- data: CommitResponses
34
- errors: ValidationError[]
35
- }
36
-
37
- type SuccessCallback = (options: {
38
- data: CommitResponses
39
- stopNavigation: EventCallable<void>
40
- }) => void | Promise<void>
41
- type ErrorCallback = (errors: ValidationError[]) => void | Promise<void>
42
- type SubmitCallback = (
43
- data: CommitResponses,
44
- ) => CommitResponses | void | Promise<CommitResponses | void>
45
-
46
- type SubmitEventWithCurrentTarget = SubmitEvent & { currentTarget: EventTarget & HTMLFormElement }
47
-
48
- /**
49
- * Factory function to create a callback observer with automatic cleanup
50
- * Reduces duplication across success/error/submit callback patterns
51
- */
52
- function createObserver<TCallback extends (...args: any[]) => any, TReturn = void>(
53
- executor: (callbacks: TCallback[], ...params: Parameters<TCallback>) => Promise<TReturn>,
54
- ) {
55
- let callbacks: TCallback[] = []
56
-
57
- const executeFx = createEffect(async (...params: Parameters<TCallback>): Promise<TReturn> => {
58
- return await executor(callbacks, ...params)
59
- })
60
-
61
- const register = (callback: TCallback) => {
62
- $effect(() => {
63
- callbacks.push(callback)
64
- return () => {
65
- callbacks = callbacks.filter((cb) => cb !== callback)
66
- }
67
- })
68
- }
69
-
70
- return { executeFx, register }
71
- }
72
-
73
- export function getForm({ id: formId, title, fields }: GetFormOptions) {
74
- const client = getPicked()
75
- const experienceInstance: ExperienceInstance = getExperience()
76
- const { currentStepStore: stepStore, goToNextStep } = experienceInstance
77
-
78
- const submitEvent = createEvent<SubmitEventWithCurrentTarget>()
79
- const formSuccessEvent = createEvent<CommitResponses>()
80
- const formErrorEvent = createEvent<ValidationError[]>()
81
- const stopNavigationEvent = createEvent<void>()
82
- const allCallbacksCompleteEvent = createEvent<void>()
83
-
84
- const isFormSubmitting = createStore<boolean>(false)
85
- const stopNavigationStore = createStore(false)
86
- stopNavigationStore
87
- .on(stopNavigationEvent, () => true) // Set to true when stopNavigation is used in onFormSuccess
88
- .on(submitEvent, () => false) // Reset to false on new submit
89
-
90
- // Create observers for each callback type
91
- const successObserver = createObserver<SuccessCallback>(async (callbacks, options) => {
92
- await Promise.all(callbacks.map((callback) => Promise.resolve(callback(options))))
93
- })
94
-
95
- const errorObserver = createObserver<ErrorCallback>(async (callbacks, errors) => {
96
- await Promise.all(callbacks.map((callback) => Promise.resolve(callback(errors))))
97
- })
98
-
99
- const submitObserver = createObserver<SubmitCallback, CommitResponses>(
100
- async (callbacks, data) => {
101
- let transformedData = data
102
-
103
- // Apply each callback's transformation sequentially
104
- for (const callback of callbacks) {
105
- const result = await Promise.resolve(callback(transformedData))
106
- // If callback returns data, use it; otherwise keep current
107
- if (result !== undefined) {
108
- transformedData = result
109
- }
110
- }
111
-
112
- return transformedData
113
- },
114
- )
115
-
116
- const commitFx = createEffect(async (responses: CommitResponses) => {
117
- const result = await client.responses.commit(responses)
118
- // Merge submitted data with validation result
119
- return {
120
- data: responses,
121
- errors: result.errors,
122
- }
123
- })
124
-
125
- const onSuccessFx = attach({
126
- source: { step: stepStore },
127
- mapParams: (commitResult: CommitResult, source) => ({
128
- step: source.step,
129
- data: commitResult.data,
130
- }),
131
- effect: createEffect(
132
- async ({ step, data }: { step: ExperienceStep | null; data: CommitResponses }) => {
133
- client.eventBus({
134
- type: 'form_submit_success',
135
- form: { id: formId, title },
136
- experience: experienceInstance,
137
- step: step ?? undefined,
138
- })
139
- return data
140
- },
141
- ),
142
- })
143
-
144
- const onErrorFx = attach({
145
- source: { step: stepStore },
146
- mapParams: (commitResult: CommitResult, source) => ({
147
- step: source.step,
148
- errors: commitResult.errors,
149
- }),
150
- effect: createEffect(
151
- async ({ step, errors }: { step: ExperienceStep | null; errors: ValidationError[] }) => {
152
- client.eventBus({
153
- type: 'form_submit_error',
154
- form: { id: formId, title },
155
- exception: { reason: errors },
156
- experience: experienceInstance,
157
- step: step ?? undefined,
158
- })
159
-
160
- errors.forEach((exception) => {
161
- client.eventBus({
162
- type: 'field_validation_error',
163
- exception: { reason: [exception] },
164
- experience: experienceInstance,
165
- step: step ?? undefined,
166
- })
167
- })
168
-
169
- return errors
170
- },
171
- ),
172
- })
173
-
174
- // set isFormSubmitting to true when the form is submitted
175
- sample({
176
- clock: submitEvent,
177
- fn: () => true,
178
- target: isFormSubmitting,
179
- })
180
-
181
- sample({
182
- clock: submitEvent,
183
- source: { step: stepStore },
184
- fn: ({ step }) => ({ formId, title, step }),
185
- target: createEffect(
186
- ({
187
- formId: formIdParam,
188
- title: titleParam,
189
- step,
190
- }: {
191
- formId: string
192
- title: string
193
- step: ExperienceStep | null
194
- }) => {
195
- client.eventBus({
196
- type: 'form_submit',
197
- form: { id: formIdParam, title: titleParam },
198
- experience: experienceInstance,
199
- step: step ?? undefined,
200
- })
201
- },
202
- ),
203
- })
204
-
205
- sample({
206
- clock: submitEvent,
207
- source: {
208
- fields: client.fields.listStore,
209
- stepFields: fields,
210
- },
211
- fn: ({ stepFields }, event) => {
212
- event.preventDefault()
213
-
214
- const formData = new FormData(event.currentTarget)
215
- const formDataEntries = Array.from(formData.entries())
216
-
217
- const responses: { data: Record<Field['id'], FieldValue>; push: (item: Response) => void } = {
218
- data: {},
219
- push(item: Response) {
220
- const questionId = item.questionId
221
- const existing = this.data[questionId]
222
-
223
- if (!existing) {
224
- // No matching questionId, just add the new item
225
- this.data[questionId] = item.value
226
- // If both values are arrays, merge them
227
- } else if (Array.isArray(existing) && Array.isArray(item.value)) {
228
- if (Array.isArray(existing) && Array.isArray(item.value)) {
229
- // Check what type FieldValue expects and convert accordingly
230
- if (typeof existing[0] === 'string') {
231
- this.data[questionId] = [...existing, ...item.value].map(String)
232
- } else if (typeof existing[0] === 'number') {
233
- this.data[questionId] = [...existing, ...item.value].map(Number)
234
- } else if (typeof existing[0] === 'boolean') {
235
- this.data[questionId] = [...existing, ...item.value].map((v) => Boolean(v))
236
- } else {
237
- this.data[questionId] = item.value
238
- }
239
- }
240
- } else {
241
- // Otherwise, override the existing value
242
- this.data[questionId] = item.value
243
- }
244
- },
245
- }
246
-
247
- stepFields.forEach((field) => {
248
- let fieldEntries = formDataEntries.filter(([key]) => key === field.id)
249
-
250
- if (fieldEntries.length === 0) {
251
- fieldEntries = [[field.id, '']]
252
- }
253
-
254
- fieldEntries.forEach(([, value]) => {
255
- const questionId = field.id
256
-
257
- if (value === '') {
258
- responses.push({
259
- questionId,
260
- value: null,
261
- })
262
-
263
- return
264
- }
265
-
266
- switch (field.type) {
267
- case 'text':
268
- responses.push({
269
- questionId,
270
- value: field.multiple ? [String(value)] : String(value),
271
- })
272
- break
273
- case 'number':
274
- responses.push({
275
- questionId,
276
- value: field.multiple ? [Number(value)] : Number(value),
277
- })
278
- break
279
- case 'boolean':
280
- responses.push({
281
- questionId,
282
- value: field.multiple ? [value === 'true'] : value === 'true',
283
- })
284
- break
285
- // case 'object':
286
- // try {
287
- // responses.push({
288
- // questionId,
289
- // value: field.multiple ? [JSON.parse(String(value))] : JSON.parse(String(value)),
290
- // })
291
- // } catch {
292
- // responses.push({
293
- // questionId,
294
- // value: field.multiple ? [null] : null,
295
- // })
296
- // }
297
-
298
- // break
299
- }
300
- })
301
- })
302
-
303
- return responses.data
304
- },
305
- target: submitObserver.executeFx,
306
- })
307
-
308
- // Pass transformed data from submit callbacks to commit
309
- sample({
310
- clock: submitObserver.executeFx.doneData,
311
- target: commitFx,
312
- })
313
-
314
- // Errors are returned from the commit even if it is a success, split to handle errors array
315
- split({
316
- source: commitFx.doneData,
317
- match: {
318
- hasErrors: ({ errors }) => errors.length > 0,
319
- },
320
- cases: {
321
- hasErrors: onErrorFx,
322
- __: onSuccessFx,
323
- },
324
- })
325
-
326
- sample({
327
- clock: onSuccessFx.doneData,
328
- target: formSuccessEvent,
329
- })
330
-
331
- // When form succeeds, execute all callbacks
332
- sample({
333
- clock: formSuccessEvent,
334
- fn: (data) => ({ data, stopNavigation: stopNavigationEvent }), // Pass form data and event reference to callbacks
335
- target: successObserver.executeFx,
336
- })
337
-
338
- // When all callbacks complete, trigger navigation check
339
- sample({
340
- clock: successObserver.executeFx.done,
341
- target: allCallbacksCompleteEvent,
342
- })
343
-
344
- // Check navigation after all callbacks complete
345
- sample({
346
- clock: allCallbacksCompleteEvent,
347
- source: stopNavigationStore,
348
- filter: (stop) => !stop,
349
- target: goToNextStep,
350
- })
351
-
352
- // Declarative event wiring for error flow
353
- sample({
354
- clock: onErrorFx.doneData,
355
- target: formErrorEvent,
356
- })
357
-
358
- // When form errors, execute all error callbacks
359
- sample({
360
- clock: formErrorEvent,
361
- target: errorObserver.executeFx,
362
- })
363
-
364
- sample({
365
- clock: [allCallbacksCompleteEvent, errorObserver.executeFx.done],
366
- fn: () => false,
367
- target: isFormSubmitting,
368
- })
369
-
370
- return {
371
- form: {
372
- onsubmit: submitEvent,
373
- },
374
- onFormSubmit: submitObserver.register,
375
- onFormSuccess: successObserver.register,
376
- onFormError: errorObserver.register,
377
- isFormSubmitting,
378
- }
379
- }