Skip to main content

CHEFS Integration

This document covers how the external-ui embeds and interacts with CHEFS (Common Hosted Enterprise Forms Service) forms via the <chefs-form-viewer> web component.

Overview

CHEFS forms are rendered using a web component (<chefs-form-viewer>) distributed as a script from the CHEFS application. The external-ui wraps this web component in a React component (ChefsFormViewer) that handles script loading, attribute binding, event listening, and lifecycle cleanup.

Component Architecture

ShowFormHandler (action-handlers/show-form-handler.tsx)

│ 1. Fetch CHEFS token + OIDC claims (parallel)
│ 2. Build token/user/prefill objects
│ 3. Render:

└── ChefsFormViewer (components/chefs/chefs-form-viewer.tsx)

│ 1. Load script via useChefsScript hook
│ 2. Inject <chefs-form-viewer> element into DOM
│ 3. Set attributes (form-id, auth-token, token, user, headers, etc.)
│ 4. Listen for formio events
│ 5. Apply prefill data on formio:ready

└── <chefs-form-viewer> (web component from CHEFS)

└── Formio.js form instance (renders the form)

File Locations

FilePurpose
external-ui/src/components/chefs/chefs-form-viewer.tsxReact wrapper component
external-ui/src/components/chefs/use-chefs-script.hook.tsDynamic script loader hook
external-ui/src/components/chefs/types.tsShared types (ChefsFormViewerProps, ScriptStatus)
external-ui/src/components/action-handlers/show-form-handler.tsxWIL ShowForm handler that orchestrates the CHEFS flow
external-ui/src/components/wil/actions-tab.tsxActions list (renders action items that can lead to form display)

Dynamic Script Loading

The useChefsScript hook manages loading the CHEFS web component script as a singleton — it prevents duplicate <script> tags regardless of how many times ChefsFormViewer mounts/unmounts.

How It Works

const SCRIPT_PATH = import.meta.env.VITE_CHEFS_SCRIPT_PATH || '/embed/chefs-form-viewer.min.js';

// Full URL: e.g., "https://submit.digital.gov.bc.ca/app/embed/chefs-form-viewer.min.js"
const scriptUrl = `${baseUrl}${SCRIPT_PATH}`;
  1. On first mount, check if a <script src="..."> already exists in the DOM.
  2. If not, create one, set async = true, append to <head>.
  3. Track status via dataset.status on the script element: loadingready or error.
  4. Subsequent mounts reuse the existing script element (singleton).

Version Pinning

To pin a specific CHEFS version (useful for stability in production), set:

VITE_CHEFS_SCRIPT_PATH=/embed/chefs-form-viewer.2.4.0.min.js

Default loads the latest: /embed/chefs-form-viewer.min.js.

ChefsFormViewer Props

interface ChefsFormViewerProps {
formId: string; // CHEFS form UUID
authToken?: string; // Short-lived JWT from token exchange
baseUrl?: string; // CHEFS app URL (default: https://submit.digital.gov.bc.ca/app)
submissionId?: string; // Existing submission to load (view/edit mode)
prefillData?: Record<string, unknown>; // Data to inject into form fields
token?: Record<string, unknown>; // Auth context for form logic (sub, roles, email, idp)
user?: Record<string, unknown>; // User display info (name, email, username, idp)
headers?: Record<string, string>; // Custom headers (e.g., Authorization: Bearer <appToken>)
readOnly?: boolean; // Render form in read-only mode
language?: string; // Form language (default: 'en')
onFormReady?: (detail: { formio: unknown }) => void;
onSubmissionComplete?: (detail: unknown) => void;
onSubmissionError?: (detail: unknown) => void;
}

Web Component Attributes

The React component injects <chefs-form-viewer> with these HTML attributes:

AttributeSourceFormat
form-idprops.formIdPlain string
base-urlprops.baseUrlURL string
auth-tokenprops.authTokenJWT string
submission-idprops.submissionIdUUID string (optional)
tokenprops.tokenJSON.stringify'd object
userprops.userJSON.stringify'd object
headersprops.headersJSON.stringify'd object
read-onlyprops.readOnly"true" if set
languageprops.language"en", "fr", etc.
isolate-stylesAlways setAttribute presence flag

Note: token, user, and headers use single-quote wrapping ('...') in the HTML because they contain JSON with double quotes.

Event Handling

The component listens for three custom events dispatched by the web component:

formio:ready

Fired when the form has finished loading and is ready for interaction.

What happens:

  1. Spinner is hidden (setIsFormMounted(true))
  2. Prefill data is applied (if provided and no submissionId)
  3. onFormReady callback is invoked with the formio instance

formio:submitDone

Fired when a form submission completes successfully.

What happens:

  1. onSubmissionComplete callback is invoked with the submission detail
  2. In WIL context: triggers the callback proxy POST with { formId, submission_id }

formio:submitError

Fired when a form submission fails.

What happens:

  1. onSubmissionError callback is invoked with the error detail

Prefill Data Injection

Prefill data is applied only for new forms (no submissionId). It is applied on formio:ready through two strategies:

// Strategy 1: Use the web component's setSubmission method
const viewer = formViewer as { setSubmission?: (data: Record<string, unknown>) => void };
if (typeof viewer.setSubmission === 'function') {
viewer.setSubmission(prefillData);
}
// Strategy 2: Fallback — set submission directly on the formio instance
else if (formioInstance && 'submission' in formioInstance) {
formioInstance.submission = { data: { ...prefillData } };
}

Prefill Data Composition

In the WIL ShowForm handler, prefill data is assembled from two sources:

const prefillData = {
...formPreFillData, // Action payload field — workflow-designer defined values
...userProfile, // OIDC claims mapped to CHEFS-expected field names
};

User profile fields (from OIDC claims):

Prefill KeyOIDC Claim Source
idpUserIdidir_user_guid
usernameidir_username
firstNamegiven_name
lastNamefamily_name
fullNamedisplay_name
emailemail
idpidentity_provider

User profile values override formPreFillData if there are key collisions (spread order).

Submission Handling

Current Behavior: Always New Submission

Currently, every form interaction in WIL creates a new submission. Even if submissionId is provided in the action payload, the form is rendered in submission mode (not edit mode) unless explicitly configured otherwise.

After formio:submitDone:

  1. Extract submission_id from the event detail:

    const submissionId = submission?.id ?? submission?._id ?? detail?.id ?? detail?._id ?? '';
  2. POST to callback proxy:

    postWilCallback({
    tenantId,
    actionId: action.id,
    body: { formId, submission_id: submissionId },
    });
  3. The upstream n8n webhook receives the form ID and submission ID, which it can use to fetch the full submission data from CHEFS if needed.

Limitation: No Submission Updates (Yet)

The current implementation does not support editing existing submissions. The submissionId prop on ChefsFormViewer is wired but the WIL flow always treats interactions as new form fills. See Future Work for planned submission update support.

Lifecycle and Cleanup

The component performs thorough cleanup on unmount:

  1. Remove event listeners — prevents memory leaks from orphaned handlers
  2. Destroy web component — calls viewer.destroy() if available, which clears internal timers (e.g., CHEFS auth token refresh intervals)
  3. Reset statesetIsFormMounted(false) ensures spinner shows on next mount
return () => {
formViewer.removeEventListener('formio:ready', handleFormReady);
formViewer.removeEventListener('formio:submitDone', handleSubmit);
formViewer.removeEventListener('formio:submitError', handleSubmitError);

const viewerInstance = formViewer as HTMLElement & { destroy?: () => void };
if (typeof viewerInstance.destroy === 'function') {
viewerInstance.destroy();
}

setIsFormMounted(false);
};

Stability Considerations

Re-render Prevention

The component uses several techniques to avoid unnecessary re-renders that would destroy and recreate the form:

  • Memoized JSON serializationtoken, user, headers are stringified via useMemo and only trigger re-renders when their content actually changes.
  • Ref-based callbacksonFormReady, onSubmissionComplete, onSubmissionError are stored in refs so the main effect's dependency array stays stable.
  • Ref-based prefillData — stored in a ref and read at event time, not in the effect deps.

Effect Dependencies

The main effect re-runs (destroys and recreates the form) only when these change:

[scriptStatus, formId, authToken, submissionId, baseUrl, tokenJson, userJson, headersJson, readOnly, language];

This means changing the action (different formId or authToken) correctly re-renders a fresh form.

Error States

ConditionBehavior
Script fails to loadShows "Failed to load form. Please try refreshing the page."
Script loadingShows spinner with "Loading form…"
Form loading (after script)Shows spinner until formio:ready fires
Form submission erroronSubmissionError callback invoked — handler shows alert
Token fetch failureShowFormHandler shows error, form never renders