import { ZodType } from "zod"; import { BlockType, ChainableConditional, ChainableExpr, ChainableGenerator, ChainableIterable, ChainableMatch, ChainableRef, FunctionExpr, IterateExpr, PredicateExpr, StructureType, TransformerFunctionExpr, ValidationExpr } from "@ministryofjustice/hmpps-forge/core/authoring"; //#region forge-core/src/components/types/structures.type.d.ts /** * Props for basic (non-field) block components. * Use this as the base for component Props interfaces. */ interface BasicBlockProps { /** * Conditional visibility - the block is rendered when this evaluates to truthy. * Defaults to true (always visible). * * To also skip validation and clear the value, use `dependentWhen` on field blocks. * * @example false // Always hidden * @example Answer('contactMethod').match(Condition.Equals('email')) // Visible when email selected */ visibleWhen?: ResolvableBoolean; /** * Optional metadata for the field. * Can be used for analytics, debugging, or custom processing. * * @example { section: 'personal-details', priority: 'high' } */ metadata?: { [key: string]: any; }; } /** * Base interface for all block types in forge. * Blocks are the fundamental building units of form UI. */ interface BlockDefinition extends BasicBlockProps { type: StructureType.BLOCK; /** The specific variant/type of block (e.g., 'text', 'number', 'radio', etc.) */ variant: string; /** Discriminator to distinguish field blocks from regular blocks */ blockType: BlockType; } /** * Props for field block components. * Use this as the base for field component Props interfaces. */ interface FieldBlockProps extends BasicBlockProps { /** * Unique identifier for the field within the form. * Used for storing answers and referencing the field value. * * @example 'email' * @example 'date_of_birth' * @example Format('task_%1_status', Item().path('id')) // Dynamic code in iterators */ code: ResolvableString; /** * Initial or computed value for the field. * Can be a static value, a reference to another field, or a computed expression. * * @example 'United Kingdom' // Static default * @example Answer('previousEmail') // Copy from another field * @example Data('user.name') // From loaded data */ defaultValue?: ResolvableString | ResolvableString[] | FunctionExpr; /** * Array of transformers to format/process the field value before rendering/storing. * Transformers only affect submitted values, and run AFTER sanitization. * * @example [Transformer.String.Trim()] // Remove whitespace * @example [Transformer.String.Uppercase(), Transformer.String.SnakeCase()] // Convert to uppercase snake_case */ formatters?: TransformerFunctionExpr[]; /** * Array of parsers to transform stored values back to display form on GET. * Parsers are the inverse of formatters: they run when loading a stored value * for rendering, converting canonical form back to what the component needs. * Parsers do NOT modify the stored answer. */ parsers?: TransformerFunctionExpr[]; /** * Array of validation rules for this field. * The field is valid when all conditions pass. * * @example * validWhen: [ * validation({ * condition: Self().match(Condition.IsRequired()), * message: 'Enter your full name', * }), * validation({ * condition: Self().match(Condition.String.HasMaxLength(200)), * message: 'Full name must be 200 characters or less', * }), * ] */ validWhen?: (ValidationExpr | IterateExpr | ChainableIterable)[] | IterateExpr | ChainableIterable; /** * Marks field as dependent on other fields. * When the predicate evaluates to false, validation is skipped and the answer is cleared. * * **Note:** This does not affect rendering — the field is still visible. * To also control visibility, use `visibleWhen`. * * @example * // Only validate and keep this field's value when appointmentType is 'phone' * dependentWhen: Answer('appointmentType').match(Condition.Equals('phone')) */ dependentWhen?: PredicateExpr; } /** * Block definition for form field blocks. * Represents user input fields with validation and formatting. */ interface FieldBlockDefinition extends BlockDefinition, FieldBlockProps {} /** * The fluent wrappers the authoring DSL returns. * Authors only ever see this side; the finalisation walk unwraps these into the * wire-format expressions (ReferenceExpr, PipelineExpr, ...) the engine consumes. */ type ChainableValue = ChainableRef | ChainableExpr | ChainableConditional | ChainableMatch | ChainableGenerator; type ResolvableString = string | ChainableValue; type ResolvableBoolean = boolean | ChainableValue | PredicateExpr; type ResolvableNumber = number | ChainableValue; type ResolvableArray = T[] | ChainableValue | ChainableIterable; type ResolvableObject = T | ChainableValue; type RenderedBlock = { block: BlockDefinition; } & ([TOutput] extends [string] ? { html: string; } : { output: TOutput; }); type Resolved = Exclude; type EvaluatedBlock = Resolved extends (infer R) ? [R] extends [never] ? never : R extends string ? string : R extends boolean ? boolean : R extends number ? number : R extends (infer U)[] ? EvaluatedBlock[] : R extends FieldBlockDefinition ? IsRoot extends true ? { [K in keyof R]: K extends 'type' | 'variant' ? R[K] : EvaluatedBlock; } & { value?: unknown; errors?: { message: string; details?: Record; }[]; } : TRenderedBlock : R extends BlockDefinition ? IsRoot extends true ? { [K in keyof R]: K extends 'type' | 'variant' ? R[K] : EvaluatedBlock; } & { value?: unknown; } : TRenderedBlock : R extends object ? { [K in keyof R]: K extends 'type' | 'variant' ? R[K] : EvaluatedBlock; } : R : never; //#endregion //#region forge-core/src/components/types/components.type.d.ts type MaybePromise = T | Promise; /** * Component render function * * Components are functions that take an evaluated block and an optional renderer, * returning framework-specific output. The optional `renderer` parameter allows * framework adapters to inject rendering dependencies at render time. * * @param block - The evaluated block with resolved properties * @param renderer - Optional renderer provided by the framework adapter * @returns Rendered component output * * @example * ```typescript * // Simple component (no renderer needed) * const htmlComponent: ComponentRenderer = block => block.content * * // Template-based component (uses renderer) * const textInput: ComponentRenderer = (block, renderer) => { * const nunjucksEnv = renderer as nunjucks.Environment * return nunjucksEnv.render('govuk/components/input/template.njk', { params }) * } * ``` */ type ComponentRenderer = (block: EvaluatedBlock, renderer?: unknown) => MaybePromise; /** * Component registry entry * * All components have the same simple interface - a variant name and a render function. * The render output is intentionally adapter-specific: Nunjucks components return * strings, React components may return React nodes, and test components can return * whichever value the test needs. */ interface ComponentRegistryEntry { variant: string; render(block: EvaluatedBlock, renderer?: unknown): MaybePromise; /** * Type-inference marker only - never set at runtime. Gives generic consumers a bare * `T` position to infer from (`EvaluatedBlock` is a conditional type TS cannot invert). */ readonly __block?: T; /** * The shape of the submitted (post-normalise) value this component can legitimately * produce - a rendered text input can only ever submit a string. Anything failing the * schema did not come from the rendered form. */ inputSchema?: ZodType; /** * Whether the component keeps every submitted value rather than the first non-empty one. * Fixed-shape components such as checkboxes declare it here, so it is a component * property rather than an author decision. */ multiple?: boolean; /** * Derives the document anchor an error summary link should target when this * component's block fails validation. The component owns the ids it renders, * so only it can say where focus should land. Returning `undefined` (or not * declaring this) falls back to the field code. */ errorAnchor?(props: ResolvedPropsOf): string | undefined; } /** * The keys `component()` stamps onto every block it builds. Authors never supply * them, so they are stripped from the props a component accepts. */ type ComponentDiscriminatorKey = 'type' | 'variant' | 'blockType'; /** * The props an author writes for a block - everything on the block definition except * the `type`, `variant` and `blockType` keys that `component()` stamps automatically. * Optionality and JSDoc carry through from the block interface. */ type PropsOf = { [K in keyof TBlock as K extends ComponentDiscriminatorKey ? never : K]: TBlock[K]; }; /** * The framework-declared keys render props keep. Everything else on the base * block definitions is consumed by the engine before render. */ type RenderKeptKey = 'code' | 'metadata'; /** * What a component's render receives - the evaluated block minus the keys the * engine has already consumed. `code`, `metadata`, `value` and `errors` come through. */ type ResolvedPropsOf = { [K in keyof EvaluatedBlock as K extends Exclude ? never : K]: EvaluatedBlock[K]; }; /** * The options every component supplies. * * @typeParam TBlock - The component's block definition interface * @typeParam TOutput - What the component's render produces * @typeParam TRenderer - The renderer the framework adapter supplies at render time */ interface BaseComponentOptions { /** * Turns the block's render props into rendered output. * * The framework adapter supplies its renderer as the second argument - the * Express/Nunjucks adapter passes a `nunjucks.Environment`. * * @example * ```typescript * render: (props, nunjucksEnv) => * nunjucksEnv.render('components/card.njk', { params: { text: props.title } }) * ``` */ render: (props: ResolvedPropsOf, renderer: TRenderer) => TOutput; /** * Adjusts the props an author wrote before the block is built from them. Runs each time * the builder is called. Use it for props the component supplies itself - a date input * prepending its ISO `formatters`, for instance. * * @example * ```typescript * prepare: props => ({ * ...props, * formatters: [Transformer.Object.ToISO(datePaths), ...(props.formatters ?? [])], * }) * ``` */ prepare?: (props: PropsOf) => PropsOf; } /** * The extra options a field component supplies - a block that captures user input. * * @typeParam TBlock - The component's block definition interface * @typeParam TOutput - What the component's render produces * @typeParam TRenderer - The renderer the framework adapter supplies at render time */ interface FieldComponentOptions extends BaseComponentOptions { /** * Marks this as a field component, so the block it builds is stamped * `blockType: BlockType.FIELD` and takes part in answer capture and validation. * * Required when the block interface extends {@link FieldBlockDefinition} and rejected * when it does not - interfaces are erased at runtime, so field-ness has to be declared * somewhere the runtime can see it. */ field: true; /** * The shape of the submitted (post-normalise) value this component can legitimately * produce - a rendered text input can only ever submit a string. Anything failing the * schema did not come from the rendered form. */ inputSchema?: ZodType; /** * Whether the component keeps every submitted value rather than the first non-empty * one. Declare it when the component's shape fixes it - checkboxes, for instance. */ multiple?: boolean; /** * Derives the document anchor an error summary link should target when this * component's block fails validation - the id of the control focus should land * on. Declare it whenever the component renders ids that can differ from the * field code (an `id` or `idPrefix` prop, a suffixed first input). Without it * the error summary links to the field code. * * @example * ```typescript * errorAnchor: props => props.idPrefix ?? props.code * ``` */ errorAnchor?: (props: ResolvedPropsOf) => string | undefined; } /** * The options `component()` accepts: the field options when the block interface is a * field block, the base options otherwise. * * @typeParam TBlock - The component's block definition interface * @typeParam TOutput - What the component's render produces * @typeParam TRenderer - The renderer the framework adapter supplies at render time */ type ComponentOptions = TBlock extends FieldBlockDefinition ? FieldComponentOptions : BaseComponentOptions; /** * What `component()` returns: a builder an author calls with props to create a block, * which is simultaneously the registry entry the framework renders with. * * When every prop is optional the props argument is too, so an all-defaults component * can be built with a bare call - `GovUKSectionBreak()`. * * @typeParam TBlock - The component's block definition interface * @typeParam TOutput - What the component's render produces */ type ForgeComponent = ({} extends PropsOf ? (props?: PropsOf) => TBlock : (props: PropsOf) => TBlock) & ComponentRegistryEntry; //#endregion //#region forge-core/src/components/utils/buildComponent.d.ts interface BuildComponentOptions { inputSchema?: ZodType; multiple?: boolean; } /** * Creates a component for the registry. * * Use this for simple components that render HTML directly, such as * HTML passthrough or collection blocks. * * * @param variant - The block variant identifier (e.g., 'html', 'collection-block') * @param renderer - Function that takes a block and returns HTML string * @param options - Optional input schema and fixed-shape `multiple` flag for the entry * @returns A registerable component * * @example * ```typescript * export const html = buildComponent('html', block => { * return block.content * }) * ``` */ declare const buildComponent: (variant: string, renderer: ComponentRenderer, options?: BuildComponentOptions) => ComponentRegistryEntry; //#endregion //#region forge-core/src/components/component.d.ts /** * Defines a component from a single block interface. * * The returned value is both the authoring builder and the registry entry, so one * declaration covers both roles: * * ```typescript * export interface MyCard extends BlockDefinition { title: string } * export const MyCard = component('myCard', { * render: (props, renderer) => { * const nunjucksEnv = renderer as nunjucks.Environment * * return nunjucksEnv.render('components/card.njk', { params: { text: props.title } }) * }, * }) * ``` * * A component that captures user input declares `field: true` - see * {@link FieldComponentOptions.field}. * * @param variant - The component's variant identifier * @param options - How the component renders, plus the field options where it is one * @returns A callable block builder that doubles as the component registry entry */ declare function component(variant: string, options: ComponentOptions): ForgeComponent; //#endregion //#region forge-core/src/built-ins/components/html.d.ts /** * HTML Block component. * * Use this to render raw HTML content within forms. * * When `tag` is set, content is wrapped in that element with `classes` and `attributes` * applied directly. When `tag` is not set but `classes`/`attributes` are present, falls * back to a wrapper `
`. Content can be a string or an array of rendered blocks * (e.g. from a collection expression), which are concatenated into a single string. * * **WARNING: XSS Risk — Content is rendered as raw HTML without any sanitization.** * * Any dynamic data interpolated into the content (e.g. via `Format()`, `Data()`, `Item()`) * will be rendered as-is. If that data comes from user input or external sources, it **must** * be escaped using `Transformer.String.EscapeHtml()` to prevent injection attacks. * * @example Safe — static developer HTML: * ```typescript * HtmlBlock({ * content: '

Terms of Service

', * }) * ``` * * @example Safe — dynamic data escaped before interpolation: * ```typescript * HtmlBlock({ * content: Format( * '

%1

', * Data('goalTitle').pipe(Transformer.String.EscapeHtml()), * ), * }) * ``` * * @example UNSAFE — dynamic data interpolated without escaping: * ```typescript * // DO NOT do this — vulnerable to XSS if goalTitle contains malicious HTML * HtmlBlock({ * content: Format('

%1

', Data('goalTitle')), * }) * ``` */ interface HtmlBlock extends BlockDefinition { /** * HTML tag to render content within. When set, `classes` and `attributes` * are applied directly to this element instead of a wrapper `
`. */ tag?: string; /** * Content to render. Accepts a string, a dynamic expression, or an array of child blocks. * When `tag` is a void element (e.g. `hr`), content is ignored. * * **WARNING: Not sanitized.** Escape any untrusted data with `Transformer.String.EscapeHtml()`. */ content?: ResolvableString | BlockDefinition | BlockDefinition[]; /** Additional CSS classes to apply to the element (optional) */ classes?: ResolvableString; /** Custom HTML attributes for the element (optional) */ attributes?: Record; } /** * HTML Block component. * * Use this to render raw HTML content within forms. * * When `tag` is set, content is wrapped in that element with `classes` and `attributes` * applied directly. When `tag` is not set but `classes`/`attributes` are present, falls * back to a wrapper `
`. Content can be a string or an array of rendered blocks * (e.g. from a collection expression), which are concatenated into a single string. * * **WARNING: XSS Risk — Content is rendered as raw HTML without any sanitization.** * * Any dynamic data interpolated into the content (e.g. via `Format()`, `Data()`, `Item()`) * will be rendered as-is. If that data comes from user input or external sources, it **must** * be escaped using `Transformer.String.EscapeHtml()` to prevent injection attacks. * * @example Safe — static developer HTML: * ```typescript * HtmlBlock({ * content: '

Terms of Service

', * }) * ``` * * @example Safe — dynamic data escaped before interpolation: * ```typescript * HtmlBlock({ * content: Format( * '

%1

', * Data('goalTitle').pipe(Transformer.String.EscapeHtml()), * ), * }) * ``` * * @example UNSAFE — dynamic data interpolated without escaping: * ```typescript * // DO NOT do this — vulnerable to XSS if goalTitle contains malicious HTML * HtmlBlock({ * content: Format('

%1

', Data('goalTitle')), * }) * ``` */ declare const HtmlBlock: ForgeComponent; //#endregion //#region forge-core/src/built-ins/components/collectionBlock.d.ts /** * Collection Block component. * Renders repeated blocks based on a collection expression. * * The `collection` property accepts any chainable expression that evaluates to an array of blocks. * This works with the Iterator pattern (e.g., `Data('items').each(Iterator.Map(...))`) * * @template T - Type of blocks in the collection array * @template F - Type of blocks in the fallback array (defaults to T) * * @example * ```typescript * CollectionBlock({ * collection: Data('tasks').each(Iterator.Map({ * template: MojCard({ * heading: Item().path('title'), * content: Item().path('description'), * }), * })), * fallback: [GovUKInsetText({ html: 'No tasks available' })], * classes: 'govuk-!-margin-bottom-6', * }) * ``` */ interface CollectionBlock extends BlockDefinition { /** * The blocks to render: an expression that evaluates to an array of blocks, * or a static array of block definitions. * @example Data('items').each(Iterator.Map({ template: GovUKInsetText({ ... }) })) * @example [GovUKInsetText({ html: 'First' }), GovUKInsetText({ html: 'Second' })] */ collection: ResolvableArray; /** * Fallback blocks to render when the collection is empty. * @example [GovUKInsetText({ html: 'No items found' })] */ fallback?: F[]; /** * HTML tag to render content within. When set, `classes` and `attributes` * are applied directly to this element instead of a wrapper `
`. * @example 'ul' */ tag?: string; /** * Additional CSS classes to apply to the wrapper element. * @example 'govuk-!-margin-bottom-6' */ classes?: ResolvableString; /** * Custom HTML attributes for the wrapper element. * @example { 'data-module': 'collection-list' } */ attributes?: Record; } /** * Runtime representation of a collection block after evaluation. * The `collection` property contains the rendered blocks from applying the template. * * Note: This doesn't extend EvaluatedBlock because the * `collection` property transforms from an expression to RenderedBlock[] * during evaluation - a transformation the generic type can't express. */ interface EvaluatedCollectionBlock { type: typeof StructureType.BLOCK; variant: 'collection-block'; /** The rendered blocks from applying the template to each collection item */ collection?: RenderedBlock[]; /** Fallback blocks rendered when the collection is empty */ fallback?: RenderedBlock[]; /** HTML tag for the wrapper element (defaults to div when classes/attributes are present) */ tag?: string; /** Additional CSS classes applied to the wrapper element */ classes?: string; /** Custom HTML attributes for the wrapper element */ attributes?: Record; } /** * Collection Block component. * Renders repeated blocks based on a collection expression. * * The `collection` property accepts any chainable expression that evaluates to an array of blocks. * This works with the Iterator pattern (e.g., `Data('items').each(Iterator.Map(...))`) * * @example * ```typescript * CollectionBlock({ * collection: Data('tasks').each(Iterator.Map({ * template: MojCard({ * heading: Item().path('title'), * content: Item().path('description'), * }), * })), * fallback: [GovUKInsetText({ html: 'No tasks available' })], * classes: 'govuk-!-margin-bottom-6', * }) * ``` */ declare const CollectionBlock: ForgeComponent, string>; //#endregion //#region forge-core/src/built-ins/components/templateWrapper.d.ts /** * TemplateWrapper component. * * Template wrapper allows wrapping child blocks in an HTML template. * Slots in the template use the syntax `{{slot:slotName}}` and will be replaced * with the rendered HTML of the corresponding blocks in the `slots` property. * * Values in the template use the syntax `{{valueName}}` and will be replaced * with the corresponding string value from the `values` property. * * @example * ```typescript * TemplateWrapper({ * template: ` *
*

{{title}}

* {{slot:content}} *

{{footer}}

*
* `, * values: { * title: 'Journey Configuration', * footer: 'See the next page for step configuration.' * }, * slots: { * content: [ * HtmlBlock({ content: '

Explanation...

' }), * GovUKCodeBlock({ code: '...' }), * ] * } * }) * ``` */ interface TemplateWrapper extends BlockDefinition { /** * HTML template with slot markers ({{slot:name}}) and value markers ({{name}}). * * @example '
{{slot:content}}
' * @example '

{{title}}

{{slot:body}}' */ template: ResolvableString; /** * String values to inject into the template at {{name}} markers. * * **WARNING: Not sanitized.** Values are injected directly into the HTML template. * Escape any untrusted data with `Transformer.String.EscapeHtml()`. * * @example { title: 'Section Title', footer: 'Footer text' } */ values?: Record; /** * Named slots containing blocks to render at {{slot:name}} markers. * * @example { content: [HtmlBlock({ content: '

Hello

' })] } */ slots?: Record; /** * HTML tag to render content within. When set, `classes` and `attributes` * are applied directly to this element instead of a wrapper `
`. * * @example 'section' */ tag?: string; /** * Additional CSS classes to apply to the wrapper element (optional). * Only applies when a wrapper element is rendered. * * @example 'govuk-!-margin-bottom-6' */ classes?: ResolvableString; /** * Custom HTML attributes for the wrapper element (optional). * Only applies when a wrapper element is rendered. * * @example { 'data-module': 'template-section' } */ attributes?: Record; } /** * TemplateWrapper component. * * Template wrapper allows wrapping child blocks in an HTML template. * Slots in the template use the syntax `{{slot:slotName}}` and will be replaced * with the rendered HTML of the corresponding blocks in the `slots` property. * * Values in the template use the syntax `{{valueName}}` and will be replaced * with the corresponding string value from the `values` property. * * @example * ```typescript * TemplateWrapper({ * template: ` *
*

{{title}}

* {{slot:content}} *

{{footer}}

*
* `, * values: { * title: 'Journey Configuration', * footer: 'See the next page for step configuration.' * }, * slots: { * content: [ * HtmlBlock({ content: '

Explanation...

' }), * GovUKCodeBlock({ code: '...' }), * ] * } * }) * ``` */ declare const TemplateWrapper: ForgeComponent; //#endregion //#region forge-core/src/built-ins/components/fragment.d.ts /** * Fragment component. * * Groups child blocks without adding a wrapper element - the blocks render * back-to-back exactly as they would as siblings. * * Useful anywhere a single block is expected but you want to output several, * such as the template of an `Iterator.Map()`. * * @example * ```typescript * Data('tasks').each(Iterator.Map( * Fragment({ * blocks: [ * GovUKHeading({ text: Item().path('title'), level: 3 }), * GovUKBody({ text: Item().path('description') }), * ], * }), * )) * ``` */ interface Fragment extends BlockDefinition { /** * The child blocks to render, in order. * * @example [GovUKHeading({ text: 'Title' }), GovUKBody({ text: 'Body' })] */ blocks: ResolvableArray; } /** * Fragment component. * * Groups child blocks without adding a wrapper element - the blocks render * back-to-back exactly as they would as siblings. * * @example * ```typescript * Fragment({ * blocks: [ * GovUKHeading({ text: 'Title', level: 3 }), * GovUKBody({ text: 'Body' }), * ], * }) * ``` */ declare const Fragment: ForgeComponent; //#endregion //#region forge-core/src/built-ins/components/index.d.ts declare const coreComponents: (ForgeComponent | ForgeComponent, string> | ForgeComponent | ForgeComponent)[]; //#endregion export { type BasicBlockProps, type BlockDefinition, type BuildComponentOptions, CollectionBlock, type ComponentOptions, type ComponentRegistryEntry, type ComponentRenderer, type EvaluatedBlock, type EvaluatedCollectionBlock, type FieldBlockDefinition, type FieldBlockProps, type ForgeComponent, Fragment, HtmlBlock, type PropsOf, type RenderedBlock, type ResolvableArray, type ResolvableBoolean, type ResolvableNumber, type ResolvableObject, type ResolvableString, type ResolvedPropsOf, TemplateWrapper, buildComponent, component, coreComponents };