@usefillo/core 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +83 -7
- package/dist/index.js +184 -17
- package/package.json +3 -2
package/dist/index.d.ts
CHANGED
|
@@ -212,6 +212,11 @@ interface UploadSession {
|
|
|
212
212
|
status: UploadStatus;
|
|
213
213
|
/** Defaults to the Fillo protocol when omitted. */
|
|
214
214
|
transport?: UploadTransport;
|
|
215
|
+
/**
|
|
216
|
+
* Per-session bearer returned at creation; sent back (X-Fillo-Upload-Token)
|
|
217
|
+
* on chunk/complete/status so only the creator can drive this upload.
|
|
218
|
+
*/
|
|
219
|
+
token?: string;
|
|
215
220
|
/** Set when status is "complete". */
|
|
216
221
|
file?: FileValue;
|
|
217
222
|
}
|
|
@@ -316,8 +321,12 @@ interface FormDraftSpec {
|
|
|
316
321
|
declare function assembleForm(spec: FormDraftSpec): FormSchema;
|
|
317
322
|
|
|
318
323
|
interface FilloClientOptions {
|
|
319
|
-
/**
|
|
320
|
-
|
|
324
|
+
/**
|
|
325
|
+
* Where the SDK sends requests. Defaults to the hosted API (https://fillo.so).
|
|
326
|
+
* Only set this if you reverse-proxy Fillo through your own domain — e.g. to
|
|
327
|
+
* keep traffic first-party or dodge ad-blockers, like PostHog's api_host.
|
|
328
|
+
*/
|
|
329
|
+
baseUrl?: string;
|
|
321
330
|
/**
|
|
322
331
|
* Publishable workspace key (pk_…) — safe to ship in client code. Required
|
|
323
332
|
* only for syncing code-defined forms into the workspace.
|
|
@@ -358,6 +367,8 @@ interface UploadFileOptions {
|
|
|
358
367
|
signal?: AbortSignal;
|
|
359
368
|
/** Resume an interrupted session instead of starting fresh. */
|
|
360
369
|
sessionId?: string;
|
|
370
|
+
/** Ownership token for the resumed session (from the original create call). */
|
|
371
|
+
uploadToken?: string;
|
|
361
372
|
}
|
|
362
373
|
declare class FilloError extends Error {
|
|
363
374
|
status?: number | undefined;
|
|
@@ -369,7 +380,7 @@ declare class FilloClient {
|
|
|
369
380
|
private fetch;
|
|
370
381
|
/** Publishable workspace key, when configured. */
|
|
371
382
|
readonly key?: string;
|
|
372
|
-
constructor(options
|
|
383
|
+
constructor(options?: FilloClientOptions);
|
|
373
384
|
private url;
|
|
374
385
|
private json;
|
|
375
386
|
/** Fetch a published form definition by id or slug. */
|
|
@@ -395,7 +406,7 @@ declare class FilloClient {
|
|
|
395
406
|
completed?: boolean;
|
|
396
407
|
}): void;
|
|
397
408
|
/** Current state of an upload session — used to resume after interruption. */
|
|
398
|
-
getUploadSession(sessionId: string): Promise<UploadSession>;
|
|
409
|
+
getUploadSession(sessionId: string, token?: string): Promise<UploadSession>;
|
|
399
410
|
/**
|
|
400
411
|
* Resumable chunked upload. Creates (or resumes) a session, streams the file
|
|
401
412
|
* chunk by chunk with progress callbacks, and finalizes into a FileValue that
|
|
@@ -423,7 +434,72 @@ declare class FilloClient {
|
|
|
423
434
|
*/
|
|
424
435
|
private s3PutUpload;
|
|
425
436
|
}
|
|
426
|
-
declare function createClient(options
|
|
437
|
+
declare function createClient(options?: FilloClientOptions): FilloClient;
|
|
438
|
+
|
|
439
|
+
type FormStatus = "idle" | "submitting" | "submitted" | "error";
|
|
440
|
+
interface FormControllerOptions {
|
|
441
|
+
form: FormSchema;
|
|
442
|
+
/** Target form id — required for client submission and funnel tracking. */
|
|
443
|
+
formId?: string;
|
|
444
|
+
/** Collect responses into a Fillo workspace. Omit and pass `onSubmit` to keep them yourself. */
|
|
445
|
+
client?: FilloClient;
|
|
446
|
+
initialData?: ResponseData;
|
|
447
|
+
onChange?: (data: ResponseData) => void;
|
|
448
|
+
onSubmitted?: (responseId: string | undefined, data: ResponseData) => void;
|
|
449
|
+
/** Handle submission yourself instead of the client. */
|
|
450
|
+
onSubmit?: (data: ResponseData) => Promise<void> | void;
|
|
451
|
+
/** Honeypot value, supplied by your renderer; must be empty for a human. */
|
|
452
|
+
getHoneypot?: () => string;
|
|
453
|
+
}
|
|
454
|
+
interface FormControllerState {
|
|
455
|
+
data: ResponseData;
|
|
456
|
+
errors: Record<string, string>;
|
|
457
|
+
status: FormStatus;
|
|
458
|
+
pageIndex: number;
|
|
459
|
+
pageCount: number;
|
|
460
|
+
/** The current page (after clamping). */
|
|
461
|
+
page: FormPage;
|
|
462
|
+
/** Blocks visible on the current page, after conditional logic. */
|
|
463
|
+
blocks: Block[];
|
|
464
|
+
isFirstPage: boolean;
|
|
465
|
+
isLastPage: boolean;
|
|
466
|
+
/** True while any file field is uploading. */
|
|
467
|
+
uploading: boolean;
|
|
468
|
+
}
|
|
469
|
+
interface FormController {
|
|
470
|
+
/** Stable snapshot — same reference until something changes (safe for useSyncExternalStore). */
|
|
471
|
+
getState(): FormControllerState;
|
|
472
|
+
/** Subscribe to state changes; returns an unsubscribe function. */
|
|
473
|
+
subscribe(listener: () => void): () => void;
|
|
474
|
+
setValue(fieldId: string, value: FieldValue): void;
|
|
475
|
+
/** Mark a file field busy/idle so `state.uploading` reflects it. */
|
|
476
|
+
setUploading(fieldId: string, busy: boolean): void;
|
|
477
|
+
/** Validate the current page, then advance. No-op on the last page or with errors. */
|
|
478
|
+
next(): void;
|
|
479
|
+
back(): void;
|
|
480
|
+
submit(): Promise<void>;
|
|
481
|
+
/**
|
|
482
|
+
* Late-bind inputs after creation, keeping current answers/page/status:
|
|
483
|
+
* - `form` updates the rendered schema in place (a builder live preview being
|
|
484
|
+
* edited), recomputing visible blocks and re-notifying.
|
|
485
|
+
* - `formId`/`client` set the submission target (e.g. a code-defined form id
|
|
486
|
+
* that resolves asynchronously).
|
|
487
|
+
*/
|
|
488
|
+
setContext(ctx: {
|
|
489
|
+
form?: FormSchema;
|
|
490
|
+
formId?: string;
|
|
491
|
+
client?: FilloClient;
|
|
492
|
+
}): void;
|
|
493
|
+
/** Drop all listeners. */
|
|
494
|
+
destroy(): void;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Framework-agnostic form engine: validation, conditional logic, pages, spam
|
|
498
|
+
* signals, and submission as a subscribable store — with no rendering. Drive it
|
|
499
|
+
* from vanilla JS, Vue, Svelte, or any framework and lay the fields out
|
|
500
|
+
* yourself. (In React, `<FilloProvider>` / `useFillo()` wrap this same engine.)
|
|
501
|
+
*/
|
|
502
|
+
declare function createFormController(options: FormControllerOptions): FormController;
|
|
427
503
|
|
|
428
504
|
/**
|
|
429
505
|
* A form whose structure lives in user code. Framework renderers can show it
|
|
@@ -472,7 +548,7 @@ declare function resolveText(text: string, data: ResponseData, form: FormSchema)
|
|
|
472
548
|
/** Return a copy of a block with its visible text fields piped, or the block unchanged. */
|
|
473
549
|
declare function pipeBlock(block: Block, data: ResponseData, form: FormSchema): Block;
|
|
474
550
|
|
|
475
|
-
/** Dependency-free nanoid-style id. Crypto-random
|
|
551
|
+
/** Dependency-free nanoid-style id with an unbiased alphabet. Crypto-random. */
|
|
476
552
|
declare function createId(size?: number): string;
|
|
477
553
|
|
|
478
|
-
export { BLOCK_KIND_META, type BaseField, type Block, type BlockKind, CONTENT_KINDS, type CheckboxField, type ChoiceField, type CodeForm, type Condition, type ConditionOp, type ContentBlock, type ContentKind, type CustomField, DRAFT_KINDS, type DateField, type DividerBlock, type Field, type FieldKind, type FieldSpec, type FieldValue, type FileUploadField, type FileValue, FilloClient, type FilloClientOptions, FilloError, type FormDraftSpec, type FormPage, type FormSchema, type FormSettings, type FormTheme, type HeadingBlock, type HiddenField, type JsonValue, type LinearScaleField, type MatrixField, type NumberField, type ParagraphBlock, type PublishedForm, type RankingField, type RatingField, type ResponseData, type SchemaValidationResult, type SelectOption, type SignatureField, type SubmitMeta, type SubmitResult, type TextField, type UploadFileOptions, type UploadProgress, type UploadSession, type UploadStatus, type UploadTransport, type ValidationResult, allFields, assembleForm, createBlock, createClient, createEmptyForm, createId, defineForm, formatAnswer, isBlockVisible, isCodeForm, isField, pipeBlock, prefillFromParams, resolveText, syncCodeForm, validateField, validateFormSchema, validateResponse, visibleBlocks, visibleFields };
|
|
554
|
+
export { BLOCK_KIND_META, type BaseField, type Block, type BlockKind, CONTENT_KINDS, type CheckboxField, type ChoiceField, type CodeForm, type Condition, type ConditionOp, type ContentBlock, type ContentKind, type CustomField, DRAFT_KINDS, type DateField, type DividerBlock, type Field, type FieldKind, type FieldSpec, type FieldValue, type FileUploadField, type FileValue, FilloClient, type FilloClientOptions, FilloError, type FormController, type FormControllerOptions, type FormControllerState, type FormDraftSpec, type FormPage, type FormSchema, type FormSettings, type FormStatus, type FormTheme, type HeadingBlock, type HiddenField, type JsonValue, type LinearScaleField, type MatrixField, type NumberField, type ParagraphBlock, type PublishedForm, type RankingField, type RatingField, type ResponseData, type SchemaValidationResult, type SelectOption, type SignatureField, type SubmitMeta, type SubmitResult, type TextField, type UploadFileOptions, type UploadProgress, type UploadSession, type UploadStatus, type UploadTransport, type ValidationResult, allFields, assembleForm, createBlock, createClient, createEmptyForm, createFormController, createId, defineForm, formatAnswer, isBlockVisible, isCodeForm, isField, pipeBlock, prefillFromParams, resolveText, syncCodeForm, validateField, validateFormSchema, validateResponse, visibleBlocks, visibleFields };
|
package/dist/index.js
CHANGED
|
@@ -335,15 +335,21 @@ function formatAnswer(field, value) {
|
|
|
335
335
|
|
|
336
336
|
// src/ids.ts
|
|
337
337
|
var ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
338
|
+
var MASK = 256 - 256 % ALPHABET.length;
|
|
338
339
|
function createId(size = 12) {
|
|
339
|
-
const bytes = new Uint8Array(size);
|
|
340
|
-
if (typeof globalThis.crypto !== "undefined") {
|
|
341
|
-
globalThis.crypto.getRandomValues(bytes);
|
|
342
|
-
} else {
|
|
343
|
-
for (let i = 0; i < size; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
344
|
-
}
|
|
345
340
|
let id = "";
|
|
346
|
-
|
|
341
|
+
while (id.length < size) {
|
|
342
|
+
const bytes = new Uint8Array(size);
|
|
343
|
+
if (typeof globalThis.crypto !== "undefined") {
|
|
344
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
345
|
+
} else {
|
|
346
|
+
for (let i = 0; i < size; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
347
|
+
}
|
|
348
|
+
for (let i = 0; i < size && id.length < size; i++) {
|
|
349
|
+
const b = bytes[i];
|
|
350
|
+
if (b < MASK) id += ALPHABET[b % ALPHABET.length];
|
|
351
|
+
}
|
|
352
|
+
}
|
|
347
353
|
return id;
|
|
348
354
|
}
|
|
349
355
|
|
|
@@ -564,14 +570,15 @@ function makeReporter(totalBytes, options) {
|
|
|
564
570
|
fraction: totalBytes === 0 ? 1 : uploadedBytes / totalBytes
|
|
565
571
|
});
|
|
566
572
|
}
|
|
573
|
+
var DEFAULT_BASE_URL = "https://fillo.so";
|
|
567
574
|
var FilloClient = class {
|
|
568
575
|
/** Server origin this client targets, normalized (no trailing slash). */
|
|
569
576
|
baseUrl;
|
|
570
577
|
fetch;
|
|
571
578
|
/** Publishable workspace key, when configured. */
|
|
572
579
|
key;
|
|
573
|
-
constructor(options) {
|
|
574
|
-
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
580
|
+
constructor(options = {}) {
|
|
581
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
575
582
|
this.key = options.key;
|
|
576
583
|
this.fetch = options.fetch ?? ((...args) => globalThis.fetch(...args));
|
|
577
584
|
}
|
|
@@ -657,8 +664,11 @@ var FilloClient = class {
|
|
|
657
664
|
}
|
|
658
665
|
}
|
|
659
666
|
/** Current state of an upload session — used to resume after interruption. */
|
|
660
|
-
getUploadSession(sessionId) {
|
|
661
|
-
return this.json(
|
|
667
|
+
getUploadSession(sessionId, token) {
|
|
668
|
+
return this.json(
|
|
669
|
+
`/uploads/${encodeURIComponent(sessionId)}`,
|
|
670
|
+
token ? { headers: { "X-Fillo-Upload-Token": token } } : void 0
|
|
671
|
+
);
|
|
662
672
|
}
|
|
663
673
|
/**
|
|
664
674
|
* Resumable chunked upload. Creates (or resumes) a session, streams the file
|
|
@@ -677,7 +687,7 @@ var FilloClient = class {
|
|
|
677
687
|
const mime = file.type || "application/octet-stream";
|
|
678
688
|
let session;
|
|
679
689
|
if (options.sessionId) {
|
|
680
|
-
session = await this.getUploadSession(options.sessionId);
|
|
690
|
+
session = await this.getUploadSession(options.sessionId, options.uploadToken);
|
|
681
691
|
} else {
|
|
682
692
|
session = await this.json(
|
|
683
693
|
`/forms/${encodeURIComponent(formId)}/uploads`,
|
|
@@ -701,7 +711,7 @@ var FilloClient = class {
|
|
|
701
711
|
}
|
|
702
712
|
const done = await this.json(
|
|
703
713
|
`/uploads/${encodeURIComponent(session.id)}/complete`,
|
|
704
|
-
{ method: "POST" }
|
|
714
|
+
{ method: "POST", headers: session.token ? { "X-Fillo-Upload-Token": session.token } : void 0 }
|
|
705
715
|
);
|
|
706
716
|
if (!done.file) throw new FilloError("Upload completed but no file was returned");
|
|
707
717
|
return done.file;
|
|
@@ -722,7 +732,8 @@ var FilloClient = class {
|
|
|
722
732
|
method: "PUT",
|
|
723
733
|
headers: {
|
|
724
734
|
"Content-Type": "application/octet-stream",
|
|
725
|
-
"X-Upload-Offset": String(offset)
|
|
735
|
+
"X-Upload-Offset": String(offset),
|
|
736
|
+
...session.token ? { "X-Fillo-Upload-Token": session.token } : {}
|
|
726
737
|
},
|
|
727
738
|
body: chunk,
|
|
728
739
|
signal: options.signal
|
|
@@ -742,7 +753,7 @@ var FilloClient = class {
|
|
|
742
753
|
attempt += 1;
|
|
743
754
|
if (attempt >= MAX_CHUNK_RETRIES) throw err;
|
|
744
755
|
await new Promise((r) => setTimeout(r, 500 * 2 ** attempt));
|
|
745
|
-
const fresh = await this.getUploadSession(session.id);
|
|
756
|
+
const fresh = await this.getUploadSession(session.id, session.token);
|
|
746
757
|
offset = fresh.uploadedBytes;
|
|
747
758
|
}
|
|
748
759
|
}
|
|
@@ -790,7 +801,7 @@ var FilloClient = class {
|
|
|
790
801
|
attempt += 1;
|
|
791
802
|
if (attempt >= MAX_CHUNK_RETRIES) throw err;
|
|
792
803
|
await new Promise((r) => setTimeout(r, 500 * 2 ** attempt));
|
|
793
|
-
const fresh = await this.getUploadSession(session.id);
|
|
804
|
+
const fresh = await this.getUploadSession(session.id, session.token);
|
|
794
805
|
offset = fresh.uploadedBytes;
|
|
795
806
|
}
|
|
796
807
|
}
|
|
@@ -825,10 +836,165 @@ var FilloClient = class {
|
|
|
825
836
|
}
|
|
826
837
|
}
|
|
827
838
|
};
|
|
828
|
-
function createClient(options) {
|
|
839
|
+
function createClient(options = {}) {
|
|
829
840
|
return new FilloClient(options);
|
|
830
841
|
}
|
|
831
842
|
|
|
843
|
+
// src/controller.ts
|
|
844
|
+
var EMPTY_PAGE = { id: "empty", blocks: [] };
|
|
845
|
+
function createFormController(options) {
|
|
846
|
+
const { onChange, onSubmitted, onSubmit } = options;
|
|
847
|
+
let form = options.form;
|
|
848
|
+
let formId = options.formId;
|
|
849
|
+
let client = options.client;
|
|
850
|
+
let data = { ...options.initialData ?? {} };
|
|
851
|
+
let errors = {};
|
|
852
|
+
let pageIndex = 0;
|
|
853
|
+
let status = "idle";
|
|
854
|
+
const uploadingFields = /* @__PURE__ */ new Set();
|
|
855
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
856
|
+
const mountedAt = Date.now();
|
|
857
|
+
let sessionId = null;
|
|
858
|
+
let sessionStarted = false;
|
|
859
|
+
function computeSnapshot() {
|
|
860
|
+
const pageCount = form.pages.length;
|
|
861
|
+
const clamped = Math.min(pageIndex, Math.max(pageCount - 1, 0));
|
|
862
|
+
const page = form.pages[clamped] ?? EMPTY_PAGE;
|
|
863
|
+
return {
|
|
864
|
+
data,
|
|
865
|
+
errors,
|
|
866
|
+
status,
|
|
867
|
+
pageIndex: clamped,
|
|
868
|
+
pageCount,
|
|
869
|
+
page,
|
|
870
|
+
blocks: visibleBlocks(page, data),
|
|
871
|
+
isFirstPage: clamped === 0,
|
|
872
|
+
isLastPage: clamped === pageCount - 1,
|
|
873
|
+
uploading: uploadingFields.size > 0
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
let snapshot = computeSnapshot();
|
|
877
|
+
function notify() {
|
|
878
|
+
snapshot = computeSnapshot();
|
|
879
|
+
for (const listener of listeners) listener();
|
|
880
|
+
}
|
|
881
|
+
function validatePage(index) {
|
|
882
|
+
const target = form.pages[index];
|
|
883
|
+
if (!target) return {};
|
|
884
|
+
const pageErrors = {};
|
|
885
|
+
for (const block of visibleBlocks(target, data)) {
|
|
886
|
+
if (!isField(block)) continue;
|
|
887
|
+
const error = validateField(block, data[block.id]);
|
|
888
|
+
if (error) pageErrors[block.id] = error;
|
|
889
|
+
}
|
|
890
|
+
return pageErrors;
|
|
891
|
+
}
|
|
892
|
+
function setValue(fieldId, value) {
|
|
893
|
+
if (!sessionStarted && client && formId) {
|
|
894
|
+
sessionStarted = true;
|
|
895
|
+
void client.startSession(formId, form.pages.length).then((id) => sessionId = id);
|
|
896
|
+
}
|
|
897
|
+
data = { ...data, [fieldId]: value };
|
|
898
|
+
onChange?.(data);
|
|
899
|
+
if (fieldId in errors) {
|
|
900
|
+
errors = Object.fromEntries(Object.entries(errors).filter(([k]) => k !== fieldId));
|
|
901
|
+
}
|
|
902
|
+
notify();
|
|
903
|
+
}
|
|
904
|
+
function setUploading(fieldId, busy) {
|
|
905
|
+
if (busy) uploadingFields.add(fieldId);
|
|
906
|
+
else uploadingFields.delete(fieldId);
|
|
907
|
+
notify();
|
|
908
|
+
}
|
|
909
|
+
function next() {
|
|
910
|
+
const pageCount = form.pages.length;
|
|
911
|
+
const clamped = Math.min(pageIndex, Math.max(pageCount - 1, 0));
|
|
912
|
+
const pageErrors = validatePage(clamped);
|
|
913
|
+
errors = pageErrors;
|
|
914
|
+
if (Object.keys(pageErrors).length > 0) {
|
|
915
|
+
notify();
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
pageIndex = Math.min(clamped + 1, pageCount - 1);
|
|
919
|
+
if (sessionId && client) client.reportProgress(sessionId, { furthestPage: pageIndex });
|
|
920
|
+
notify();
|
|
921
|
+
}
|
|
922
|
+
function back() {
|
|
923
|
+
errors = {};
|
|
924
|
+
pageIndex = Math.max(Math.min(pageIndex, form.pages.length - 1) - 1, 0);
|
|
925
|
+
notify();
|
|
926
|
+
}
|
|
927
|
+
async function submit() {
|
|
928
|
+
if (status === "submitting" || status === "submitted") return;
|
|
929
|
+
const result = validateResponse(form, data);
|
|
930
|
+
if (!result.ok) {
|
|
931
|
+
errors = result.errors;
|
|
932
|
+
const firstBad = form.pages.findIndex((p) => p.blocks.some((b) => b.id in result.errors));
|
|
933
|
+
if (firstBad >= 0) pageIndex = firstBad;
|
|
934
|
+
notify();
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
status = "submitting";
|
|
938
|
+
notify();
|
|
939
|
+
try {
|
|
940
|
+
if (onSubmit) {
|
|
941
|
+
await onSubmit(result.data);
|
|
942
|
+
status = "submitted";
|
|
943
|
+
notify();
|
|
944
|
+
onSubmitted?.(void 0, result.data);
|
|
945
|
+
} else if (client && formId) {
|
|
946
|
+
const res = await client.submit(formId, result.data, {
|
|
947
|
+
hp: options.getHoneypot?.() ?? "",
|
|
948
|
+
elapsedMs: Date.now() - mountedAt
|
|
949
|
+
});
|
|
950
|
+
if (!res.ok) {
|
|
951
|
+
errors = res.errors ?? {};
|
|
952
|
+
status = "idle";
|
|
953
|
+
notify();
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
if (sessionId) client.reportProgress(sessionId, { completed: true });
|
|
957
|
+
status = "submitted";
|
|
958
|
+
notify();
|
|
959
|
+
onSubmitted?.(res.responseId, result.data);
|
|
960
|
+
} else {
|
|
961
|
+
status = "submitted";
|
|
962
|
+
notify();
|
|
963
|
+
onSubmitted?.(void 0, result.data);
|
|
964
|
+
}
|
|
965
|
+
} catch (err) {
|
|
966
|
+
status = "idle";
|
|
967
|
+
notify();
|
|
968
|
+
throw err;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return {
|
|
972
|
+
getState: () => snapshot,
|
|
973
|
+
subscribe(listener) {
|
|
974
|
+
listeners.add(listener);
|
|
975
|
+
return () => {
|
|
976
|
+
listeners.delete(listener);
|
|
977
|
+
};
|
|
978
|
+
},
|
|
979
|
+
setValue,
|
|
980
|
+
setUploading,
|
|
981
|
+
next,
|
|
982
|
+
back,
|
|
983
|
+
submit,
|
|
984
|
+
setContext(ctx) {
|
|
985
|
+
if ("formId" in ctx) formId = ctx.formId;
|
|
986
|
+
if ("client" in ctx) client = ctx.client;
|
|
987
|
+
if (ctx.form && ctx.form !== form) {
|
|
988
|
+
form = ctx.form;
|
|
989
|
+
notify();
|
|
990
|
+
}
|
|
991
|
+
},
|
|
992
|
+
destroy() {
|
|
993
|
+
listeners.clear();
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
|
|
832
998
|
// src/define.ts
|
|
833
999
|
function defineForm(def) {
|
|
834
1000
|
return {
|
|
@@ -958,6 +1124,7 @@ export {
|
|
|
958
1124
|
createBlock,
|
|
959
1125
|
createClient,
|
|
960
1126
|
createEmptyForm,
|
|
1127
|
+
createFormController,
|
|
961
1128
|
createId,
|
|
962
1129
|
defineForm,
|
|
963
1130
|
formatAnswer,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usefillo/core",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Form schema, validation, logic engine and JS client for Fillo. Framework-agnostic.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
44
44
|
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
45
|
-
"typecheck": "tsc --noEmit"
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"test": "node --test test/*.test.mjs"
|
|
46
47
|
}
|
|
47
48
|
}
|