Initial QuestionGraph library and documentation
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
QuestionnaireRenderer, type QuestionnaireDefinition,
|
||||
type QuestionnaireSaveHandler, type UploadedFileRef, type Answers,
|
||||
} from "../../lib";
|
||||
import { customControls } from "./CustomControls";
|
||||
|
||||
// Your application's API client implements these three operations.
|
||||
type ClaimApi = {
|
||||
saveDraft: QuestionnaireSaveHandler;
|
||||
uploadFiles: (questionId: string, files: File[]) => Promise<UploadedFileRef[]>;
|
||||
submit: (answers: Answers, outcomes: Record<string, boolean>) => Promise<void>;
|
||||
};
|
||||
|
||||
// Pass the questionnaire JSON loaded by your route or data-fetching layer.
|
||||
export function ClaimForm({ definition, api }: {
|
||||
definition: QuestionnaireDefinition;
|
||||
api: ClaimApi;
|
||||
}) {
|
||||
return <div className="questionnaire-pane">
|
||||
<QuestionnaireRenderer
|
||||
definition={definition}
|
||||
components={customControls}
|
||||
onSave={request => api.saveDraft(request)}
|
||||
onUpload={(questionId, files) => api.uploadFiles(questionId, files)}
|
||||
onSubmit={(answers, outcomes) => api.submit(answers, outcomes)}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { QuestionnaireComponents, QuestionnaireFieldProps } from "../../lib";
|
||||
|
||||
function TextControl({ id, value, question, accessibility, onChange }: QuestionnaireFieldProps) {
|
||||
const props = {
|
||||
id,
|
||||
...accessibility,
|
||||
className: "app-input",
|
||||
value: String(value ?? ""),
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
|
||||
onChange(event.target.value),
|
||||
};
|
||||
|
||||
return question.ui?.widget === "textarea"
|
||||
? <textarea {...props} rows={4} />
|
||||
: <input {...props} type="text" />;
|
||||
}
|
||||
|
||||
// Other field types keep their default controls.
|
||||
// Labels, errors, and focus/blur are handled by the renderer.
|
||||
export const customControls: QuestionnaireComponents = { text: TextControl };
|
||||
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
QuestionnaireRenderer, type QuestionnaireDefinition,
|
||||
type QuestionnaireFieldProps,
|
||||
} from "../../lib";
|
||||
|
||||
const definition: QuestionnaireDefinition = {
|
||||
schemaVersion: 1, id: "contact", version: "1", title: "Contact",
|
||||
pages: [{ id: "about", title: "About you", fields: [
|
||||
{ id: "name", type: "text", label: "Your name", required: true,
|
||||
constraints: { minLength: 2 } },
|
||||
] }],
|
||||
};
|
||||
|
||||
// This example handles text fields only. A general renderer must handle every
|
||||
// field type in its definition; renderField has no automatic default fallback.
|
||||
function FullTextField(props: QuestionnaireFieldProps) {
|
||||
return <div id={props.id + "-question"} className="app-field"
|
||||
onFocus={props.onFocus}
|
||||
onBlur={event => {
|
||||
if (!event.currentTarget.contains(event.relatedTarget)) props.onBlur();
|
||||
}}>
|
||||
<label htmlFor={props.id}>{props.question.label}{props.required ? " *" : ""}</label>
|
||||
<p id={props.id + "-description"}>{props.question.description}</p>
|
||||
<input id={props.id} {...props.accessibility} type="text"
|
||||
value={String(props.value ?? "")}
|
||||
onChange={event => props.onChange(event.target.value)} />
|
||||
<div id={props.id + "-errors"} aria-live="polite">
|
||||
{props.validation.map((issue, index) =>
|
||||
<p key={index} className={issue.severity === "ERROR" ? "error" : "warning"}>
|
||||
{issue.message}
|
||||
</p>)}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
// In-memory example: no save handler, uploads, or backend submission.
|
||||
export function CustomFieldExample() {
|
||||
return <div className="questionnaire-pane">
|
||||
<QuestionnaireRenderer definition={definition} renderField={FullTextField}
|
||||
onSubmit={answers => { window.alert("Completed for " + answers.name); }} />
|
||||
</div>;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { QuestionnaireSaveHandler } from "../../lib";
|
||||
|
||||
export const saveDraft: QuestionnaireSaveHandler = async request => {
|
||||
const response = await fetch("/api/claims/current/draft", {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"If-Match": JSON.stringify(request.revision),
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
|
||||
// Reject failures so the renderer stays on the page and allows a retry.
|
||||
if (!response.ok) throw new Error("Could not save your answers.");
|
||||
|
||||
// The server returns { revision: number }.
|
||||
return response.json();
|
||||
};
|
||||
Reference in New Issue
Block a user