@marktiderman/genesis-forms-web 0.4.0 → 0.6.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.
- package/README.md +57 -1
- package/dist/index.d.ts +91 -2
- package/dist/index.js +342 -20
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -82,6 +82,21 @@ import { Form } from "@marktiderman/genesis-forms-web";
|
|
|
82
82
|
`dropdown` (a native select), `number` (min, max, step; the decimal keypad on phones), `date` (the
|
|
83
83
|
browser's date picker) and `consent` (one checkbox). A rating can show end labels (`lowLabel`,
|
|
84
84
|
`highLabel`) and `display: "stars"`; NPS takes the same labels.
|
|
85
|
+
- **Option lists:** an option's `description` is a muted line under its label and its `image` a
|
|
86
|
+
small picture beside it. A choice question with more than 10 options it shows (or with
|
|
87
|
+
`searchable: true`) gets a search box over an in-page list instead of radios, checkboxes or a
|
|
88
|
+
select: typing narrows it (`searchOptions`), arrow keys move, Enter or a tap picks, and it
|
|
89
|
+
works the same in classic and focus layouts and on a phone. A list filtered by another answer
|
|
90
|
+
(`optionsFrom.filter`) shows only the matching options and drops a pick that stops matching.
|
|
91
|
+
`linkParams` (such as `new URLSearchParams(location.search)`) preselects questions with
|
|
92
|
+
`prefill: { param }`; with `hideWhenPrefilled` a question the link answered isn't shown or
|
|
93
|
+
counted. Which options show, when to search and what a link picks are all decided in
|
|
94
|
+
genesis-forms (`visibleOptions`, `isSearchable`, `prefillFromLink`, `respondentPath`), so
|
|
95
|
+
every renderer agrees with the engine and the server:
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
<Form form={def} hostOptions={lists} linkParams={new URLSearchParams(location.search)} />
|
|
99
|
+
```
|
|
85
100
|
- **Submitting:** the ending shows once `onSubmit` resolves. If it rejects, the completed response
|
|
86
101
|
stays on screen read-only (a completed response is final), Back is hidden, and "Try again"
|
|
87
102
|
(`retryLabel`) resends the same answers.
|
|
@@ -91,7 +106,9 @@ import { Form } from "@marktiderman/genesis-forms-web";
|
|
|
91
106
|
over.
|
|
92
107
|
- **Extension ladder:** `slots` replaces a part (`header`, `brand`, `progress`, `navigation`,
|
|
93
108
|
`ending`, `errorSummary`), `null` removes it, and `slotProps` feeds it (and wins over the defaults). A custom
|
|
94
|
-
ending can mark an element `data-gf-focus` to receive focus;
|
|
109
|
+
ending can mark an element `data-gf-focus` to receive focus; `slotProps={{ ending: { actions } }}`
|
|
110
|
+
puts buttons or links (Send another response, Change your answers) in a centred row under the
|
|
111
|
+
default ending, and `DefaultEnding` is exported for a custom ending to wrap; a custom error summary can mark itself
|
|
95
112
|
`data-gf-error-summary` (with `tabIndex={-1}`) to be focused after a failed submit.
|
|
96
113
|
- **Renderers** come from a registry. `defaultRegistry` is shared by every `<Form>` without a
|
|
97
114
|
`registry` prop, so prefer `extend()`, which returns a new registry and changes nothing else:
|
|
@@ -142,10 +159,49 @@ sends (it says "Wait for the photo to finish uploading."). Without `onUpload`, o
|
|
|
142
159
|
`UploadContext` around the form, a photo field says photos can't be added here. `photoFileProblem`,
|
|
143
160
|
`photoFileType` and `photoAccept` are the renderer's own checks, for a custom one.
|
|
144
161
|
|
|
162
|
+
`signedUploadHost` is an upload host for genesis-forms/server's `createUploadHandler`: for each file
|
|
163
|
+
it asks your endpoint for a signed upload, sends the bytes with `send`, and resolves to the
|
|
164
|
+
server's reference. A refusal rejects with the server's words. `url(ref)` reads a stored file
|
|
165
|
+
through your `readUrl`, null when it can't (for thumbnails in a staff view):
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
const host = signedUploadHost({
|
|
169
|
+
endpoint: `/api/upload/${slug}`,
|
|
170
|
+
claim: () => (editToken ? { edit: editToken } : { attemptId }), // read at each upload
|
|
171
|
+
// Supabase Storage; without `send`, the file is PUT to `upload.url`.
|
|
172
|
+
send: async (file, upload, contentType) => {
|
|
173
|
+
const { error } = await supabase.storage
|
|
174
|
+
.from(upload.bucket!)
|
|
175
|
+
.uploadToSignedUrl(upload.path, upload.token, file, { contentType });
|
|
176
|
+
if (error) throw error;
|
|
177
|
+
},
|
|
178
|
+
readUrl: async (ref) => signedReadUrl(ref), // your storage, or null
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
<Form form={feedback} onUpload={host.upload} onSubmit={send} />;
|
|
182
|
+
```
|
|
183
|
+
|
|
145
184
|
A **`calculated`** field shows the value the engine worked out from other answers, read-only, in an
|
|
146
185
|
`<output>` named by the question that announces changes politely. It follows the field's `format`
|
|
147
186
|
and `decimals`, and shows a dash until there is a value.
|
|
148
187
|
|
|
188
|
+
## Clean form links
|
|
189
|
+
|
|
190
|
+
A link's query (`?edit=`, prefilled answers, `utm_…`) can be taken out of the address bar once the
|
|
191
|
+
page has read it, so it stays out of screenshots and shared addresses. Cosmetic, not security: the
|
|
192
|
+
server still checks every value. Call it once, after reading the query:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import { hideQueryOnLoad, wasReload } from "@marktiderman/genesis-forms-web";
|
|
196
|
+
|
|
197
|
+
useEffect(() => void hideQueryOnLoad(window, wasReload()), []);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
It uses the browser's own `History.prototype.replaceState` (a router that patches the instance's
|
|
201
|
+
would otherwise re-read the address), and keeps the query in sessionStorage (`form-query:<path>`) so
|
|
202
|
+
a reload in the same tab within `HIDDEN_QUERY_KEEP_MS` (30 minutes) brings it back. Blocked storage
|
|
203
|
+
leaves the query visible.
|
|
204
|
+
|
|
149
205
|
## License
|
|
150
206
|
|
|
151
207
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode, ComponentType } from 'react';
|
|
3
|
-
import { FormDefinition, FormEngine, CreateFormOptions, HostOptions, FormState, FormProgress, FieldBlock, PhotoRef } from '@marktiderman/genesis-forms';
|
|
3
|
+
import { FormDefinition, FormEngine, CreateFormOptions, HostOptions, LinkParams, FormState, FormProgress, FieldBlock, PhotoRef, SignedUpload } from '@marktiderman/genesis-forms';
|
|
4
4
|
import * as _marktiderman_genesis_forms_react from '@marktiderman/genesis-forms-react';
|
|
5
5
|
import { RendererRegistry, FieldRendererProps, RendererEntry } from '@marktiderman/genesis-forms-react';
|
|
6
6
|
|
|
@@ -107,6 +107,12 @@ interface NavigationSlotProps {
|
|
|
107
107
|
interface EndingSlotProps {
|
|
108
108
|
title: string;
|
|
109
109
|
description?: string;
|
|
110
|
+
/**
|
|
111
|
+
* Buttons or links under the ending, such as "Send another response" or "Change your
|
|
112
|
+
* answers": pass them through `slotProps.ending`. The default ending shows them in a
|
|
113
|
+
* centred row.
|
|
114
|
+
*/
|
|
115
|
+
actions?: ReactNode;
|
|
110
116
|
className?: string;
|
|
111
117
|
}
|
|
112
118
|
interface ErrorSummaryItem {
|
|
@@ -133,6 +139,11 @@ interface FormSlotMap {
|
|
|
133
139
|
ending: EndingSlotProps;
|
|
134
140
|
errorSummary: ErrorSummarySlotProps;
|
|
135
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* The built-in ending (a check, the title and description, then any `actions`). Exported
|
|
144
|
+
* so a custom ending slot can wrap it rather than copy it.
|
|
145
|
+
*/
|
|
146
|
+
declare function DefaultEnding({ title, description, actions, className }: EndingSlotProps): react.JSX.Element;
|
|
136
147
|
interface FormBrand {
|
|
137
148
|
name: string;
|
|
138
149
|
/** An image URL or your own element (decorative: the name is the text). Without one, the name's initials are shown. */
|
|
@@ -156,6 +167,15 @@ interface FormProps {
|
|
|
156
167
|
* `hostOptions`).
|
|
157
168
|
*/
|
|
158
169
|
hostOptions?: HostOptions | undefined;
|
|
170
|
+
/**
|
|
171
|
+
* The query parameters of the link the form was opened from, such as
|
|
172
|
+
* `new URLSearchParams(location.search)`. A choice question with
|
|
173
|
+
* `props.prefill: { param }` starts with that parameter's value picked when
|
|
174
|
+
* it is one of its options (`prefillFromLink`), and with
|
|
175
|
+
* `hideWhenPrefilled` it isn't shown at all. Read when the response starts;
|
|
176
|
+
* unused with `engine` (pass `linkParams` to `useForm` then).
|
|
177
|
+
*/
|
|
178
|
+
linkParams?: LinkParams | undefined;
|
|
159
179
|
/** Overrides the definition's layout. */
|
|
160
180
|
layout?: "focus" | "classic" | undefined;
|
|
161
181
|
/**
|
|
@@ -278,6 +298,8 @@ declare const defaultRegistry: _marktiderman_genesis_forms_react.RendererRegistr
|
|
|
278
298
|
*/
|
|
279
299
|
interface UploadHost {
|
|
280
300
|
upload(file: File, field: FieldBlock): Promise<PhotoRef>;
|
|
301
|
+
/** A link to read a stored file (the host's storage signs it), or null when it can't be read. */
|
|
302
|
+
url?(ref: PhotoRef, field?: FieldBlock): Promise<string | null>;
|
|
281
303
|
}
|
|
282
304
|
/** Provide it around `<Form>` (or pass `onUpload`) so `photo` fields can store files. */
|
|
283
305
|
declare const UploadContext: react.Context<UploadHost | null>;
|
|
@@ -300,6 +322,48 @@ declare function photoFileProblem(field: FieldBlock, file: {
|
|
|
300
322
|
/** The `accept` attribute for a field: its MIME types, plus HEIC's file names for browsers that give it no type. */
|
|
301
323
|
declare function photoAccept(field: FieldBlock): string;
|
|
302
324
|
|
|
325
|
+
/**
|
|
326
|
+
* Who is uploading: a new response's visit (`attemptId`, a UUID the page keeps for the
|
|
327
|
+
* visit), or an edit link's token while changing a sent response.
|
|
328
|
+
*/
|
|
329
|
+
type UploadClaim = {
|
|
330
|
+
attemptId: string;
|
|
331
|
+
} | {
|
|
332
|
+
edit: string;
|
|
333
|
+
};
|
|
334
|
+
interface SignedUploadHostOptions {
|
|
335
|
+
/** The app's upload endpoint (`createUploadHandler`'s route), or one per field. */
|
|
336
|
+
endpoint: string | ((field: FieldBlock) => string);
|
|
337
|
+
/** Who is uploading, read at each upload. */
|
|
338
|
+
claim: UploadClaim | (() => UploadClaim);
|
|
339
|
+
/**
|
|
340
|
+
* Sends the bytes where the server signed. Default: PUT the file to `upload.url`
|
|
341
|
+
* (with its type), which a store that signs plain URLs sets. Supabase Storage:
|
|
342
|
+
* `supabase.storage.from(upload.bucket).uploadToSignedUrl(upload.path, upload.token, file, { contentType })`.
|
|
343
|
+
* A throw (or rejection) is the upload failing.
|
|
344
|
+
*/
|
|
345
|
+
send?: ((file: File, upload: SignedUpload, contentType: string) => Promise<void>) | undefined;
|
|
346
|
+
/**
|
|
347
|
+
* A link to read a stored file, for thumbnails and staff views: the app's storage signs
|
|
348
|
+
* it. Null when the reader can't open it. Without it, `url` resolves null.
|
|
349
|
+
*/
|
|
350
|
+
readUrl?: ((ref: PhotoRef, field?: FieldBlock) => Promise<string | null>) | undefined;
|
|
351
|
+
/** Default the global `fetch`. */
|
|
352
|
+
fetch?: typeof fetch | undefined;
|
|
353
|
+
}
|
|
354
|
+
/** An `UploadHost` that always has `url`. */
|
|
355
|
+
interface SignedUploadHost extends UploadHost {
|
|
356
|
+
url(ref: PhotoRef, field?: FieldBlock): Promise<string | null>;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* The browser half of the upload handshake (genesis-forms/server's `createUploadHandler`):
|
|
360
|
+
* for each file it asks `endpoint` for a signed upload (`UploadRequest` as JSON), sends
|
|
361
|
+
* the bytes with `send`, and resolves to the reference the server handed back, which is
|
|
362
|
+
* all the answer keeps. A refusal rejects with the server's words. Pass it as `<Form
|
|
363
|
+
* onUpload={host.upload}>` or in an `UploadContext`.
|
|
364
|
+
*/
|
|
365
|
+
declare function signedUploadHost(options: SignedUploadHostOptions): SignedUploadHost;
|
|
366
|
+
|
|
303
367
|
/**
|
|
304
368
|
* The video or audio a form is about, as the host app plays it. `media-notes`
|
|
305
369
|
* fields read it: with `currentTime` the add row starts at the current moment
|
|
@@ -325,4 +389,29 @@ declare const SourceTextContext: react.Context<string | null>;
|
|
|
325
389
|
/** The enclosing `SourceTextContext` value, or null. */
|
|
326
390
|
declare function useSourceText(): string | null;
|
|
327
391
|
|
|
328
|
-
|
|
392
|
+
/** How long a reload brings the query back: 30 minutes. */
|
|
393
|
+
declare const HIDDEN_QUERY_KEEP_MS: number;
|
|
394
|
+
/** The parts of `window` `hideQueryOnLoad` uses (pass `window`; tests pass a fake). */
|
|
395
|
+
interface HiddenQueryWindow {
|
|
396
|
+
location: {
|
|
397
|
+
pathname: string;
|
|
398
|
+
search: string;
|
|
399
|
+
hash: string;
|
|
400
|
+
replace(url: string): void;
|
|
401
|
+
};
|
|
402
|
+
history: {
|
|
403
|
+
state: unknown;
|
|
404
|
+
replaceState(data: unknown, unused: string, url: string): void;
|
|
405
|
+
};
|
|
406
|
+
sessionStorage: Pick<Storage, "getItem" | "setItem" | "removeItem">;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Run once when the page has read its query. Returns "hidden" when it took the query out
|
|
410
|
+
* of the address bar, "restoring" when a reload is bringing a hidden query back (the page
|
|
411
|
+
* is about to load again), else "none".
|
|
412
|
+
*/
|
|
413
|
+
declare function hideQueryOnLoad(win: HiddenQueryWindow, isReload: boolean, now?: number): "hidden" | "restoring" | "none";
|
|
414
|
+
/** Whether this page load was a reload (Navigation Timing), in the browser; false elsewhere. */
|
|
415
|
+
declare const wasReload: () => boolean;
|
|
416
|
+
|
|
417
|
+
export { type BrandSlotProps, CLASSIC_PROGRESS_DELAY_MS, DefaultEnding, type EndingSlotProps, type ErrorSummaryItem, type ErrorSummarySlotProps, FieldChrome, type FieldChromeProps, Form, type FormBrand, type FormPresentation, type FormPresentationValue, type FormProps, type FormSlotMap, type FormTheme, HIDDEN_QUERY_KEEP_MS, type HeaderSlotProps, type HiddenQueryWindow, type MediaTime, MediaTimeContext, type NavigationSlotProps, PROGRESS_DELAY_MS, type ProgressSlotProps, type SignedUploadHost, type SignedUploadHostOptions, SourceTextContext, type ThemeColors, UPLOAD_WAIT_MESSAGE, type UploadClaim, UploadContext, type UploadHost, builtInRenderers, defaultRegistry, describedBy, hideQueryOnLoad, photoAccept, photoFileProblem, photoFileType, signedUploadHost, themeColors, themeCss, useFormPresentation, useMediaTime, useSourceText, useUpload, wasReload };
|