Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
GOVUK_INPUT_WIDTH_3,
GOVUK_LABEL__M
} from '~/src/models/forms/editor-v2/common.js'
import { mapComposableSettings } from '~/src/models/forms/editor-v2/composable-settings-fields.js'

const MIN_FILES_ERROR_MESSAGE =
'Minimum file count must be a whole number between 1 and 25'
Expand Down Expand Up @@ -458,6 +459,7 @@ export function mapBaseQuestionDetails(payload) {
const additionalOptions = getAdditionalOptions(payload)
const additionalSchema = getAdditionalSchema(payload)
const extraRootFields = mapExtraRootFields(payload)
const composableSettings = mapComposableSettings(payload)

return /** @type {Partial<ComponentDef>} */ ({
type: payload.questionType,
Expand All @@ -466,6 +468,7 @@ export function mapBaseQuestionDetails(payload) {
shortDescription: payload.shortDescription,
hint: payload.hintText,
...extraRootFields,
...composableSettings, // Add composable settings (before/after)
options: {
required: !isCheckboxSelected(payload.questionOptional),
...additionalOptions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import { ComponentType } from '@defra/forms-model'

import { GOVUK_LABEL__M } from '~/src/models/forms/editor-v2/common.js'

/**
* Fields for configuring composable component relationships
*/

export const composableSettingsFields = {
/**
* Checkbox to enable adding a component after the main component
*/
addComponentAfter: {
name: 'addComponentAfter',
id: 'addComponentAfter',
classes: 'govuk-checkboxes--small',
items: [
{
value: 'true',
text: 'Add additional component after this field',
checked: false,
hint: {
text: 'Add instructions, guidance, or other content after this field'
}
}
]
},

/**
* Select the type of component to add after
*/
afterComponentType: {
name: 'afterComponentType',
id: 'afterComponentType',
label: {
text: 'Component type',
classes: 'govuk-label--s'
},
items: [
{ value: '', text: 'Select component type' },
{ value: ComponentType.Details, text: 'Details (collapsible content)' },
{ value: ComponentType.InsetText, text: 'Inset text' },
{ value: ComponentType.Html, text: 'HTML content' },
{ value: ComponentType.Markdown, text: 'Markdown content' }
]
},

/**
* Content for Details component
*/
afterComponentDetailsContent: {
name: 'afterComponentDetailsContent',
id: 'afterComponentDetailsContent',
label: {
text: 'Details content',
classes: GOVUK_LABEL__M
},
hint: {
text: 'Enter the content to display in the collapsible section. You can use markdown for formatting.'
},
rows: 5
},

/**
* Summary text for Details component
*/
afterComponentDetailsSummary: {
name: 'afterComponentDetailsSummary',
id: 'afterComponentDetailsSummary',
label: {
text: 'Summary text',
classes: GOVUK_LABEL__M
},
hint: {
text: 'Text shown on the collapsible button (e.g., "How to find location details")'
}
},

/**
* Content for InsetText component
*/
afterComponentInsetContent: {
name: 'afterComponentInsetContent',
id: 'afterComponentInsetContent',
label: {
text: 'Inset text content',
classes: GOVUK_LABEL__M
},
hint: {
text: 'Important information to highlight to users'
},
rows: 3
},

/**
* Content for HTML component
*/
afterComponentHtmlContent: {
name: 'afterComponentHtmlContent',
id: 'afterComponentHtmlContent',
label: {
text: 'HTML content',
classes: GOVUK_LABEL__M
},
hint: {
text: 'Custom HTML content (use with caution)'
},
rows: 5
},

/**
* Content for Markdown component
*/
afterComponentMarkdownContent: {
name: 'afterComponentMarkdownContent',
id: 'afterComponentMarkdownContent',
label: {
text: 'Markdown content',
classes: GOVUK_LABEL__M
},
hint: {
text: 'Content with markdown formatting support'
},
rows: 5
}
}

/**
* Maps form payload to component definition with composable support
* @param {any} payload - Form payload
* @returns {any} Component definition with after property if configured
*/
export function mapComposableSettings(payload) {
/** @type {any} */
const result = {}

// Check if adding component after is enabled
if (payload.addComponentAfter === 'true' && payload.afterComponentType) {
/** @type {any} */
const afterComponent = {
type: payload.afterComponentType,
name: `${payload.name}_after`,
options: {}
}

// Configure based on component type
switch (payload.afterComponentType) {
case ComponentType.Details:
afterComponent.title =
payload.afterComponentDetailsSummary ?? 'More information'
afterComponent.content = payload.afterComponentDetailsContent ?? ''
break

case ComponentType.InsetText:
afterComponent.content = payload.afterComponentInsetContent ?? ''
break

case ComponentType.Html:
afterComponent.content = payload.afterComponentHtmlContent ?? ''
break

case ComponentType.Markdown:
afterComponent.content = payload.afterComponentMarkdownContent ?? ''
break
}

result.after = afterComponent
}

return result
}

/**
* Extract composable settings from existing component
* @param {any} component - Component definition
* @returns {any} Form values for composable settings
*/
export function extractComposableSettings(component) {
/** @type {any} */
const values = {}

if (component.after) {
values.addComponentAfter = 'true'
values.afterComponentType = component.after.type

switch (component.after.type) {
case ComponentType.Details:
values.afterComponentDetailsSummary = component.after.title ?? ''
values.afterComponentDetailsContent = component.after.content ?? ''
break

case ComponentType.InsetText:
values.afterComponentInsetContent = component.after.content ?? ''
break

case ComponentType.Html:
values.afterComponentHtmlContent = component.after.content ?? ''
break

case ComponentType.Markdown:
values.afterComponentMarkdownContent = component.after.content ?? ''
break
}
}

return values
}
52 changes: 52 additions & 0 deletions examples/composable-components-example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "composable components 2",
"engine": "V2",
"schema": 2,
"startPage": "/summary",
"pages": [
{
"title": "",
"path": "/whats-your-email",
"components": [
{
"type": "EmailAddressField",
"title": "whats your email?",
"name": "UzHJfU",
"shortDescription": "xcvbxcvb",
"hint": "sdfsdfsdf",
"options": {
"required": true
},
"schema": {},
"id": "40c77003-aafe-42c0-9bd8-635623875b7a",
"before": {
"type": "InsetText",
"name": "email_notice",
"title": "Important",
"content": "We will only use your email address to send you a confirmation of your submission.",
"options": {}
},
"after": {
"type": "Details",
"name": "email_help",
"title": "Why we need your email",
"content": "Your email address helps us: Send you confirmation of receipt. Contact you if we need more information. Keep you updated on the progress of your application.",
"options": {}
}
}
],
"next": [],
"id": "bd251184-230f-459f-864a-f07b7f0236fc"
},
{
"id": "449a45f6-4541-4a46-91bd-8b8931b07b50",
"title": "Summary",
"path": "/summary",
"controller": "SummaryPageWithConfirmationEmailController",
"next": []
}
],
"conditions": [],
"sections": [],
"lists": []
}
8 changes: 7 additions & 1 deletion model/src/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,13 @@ export interface SelectFieldComponent extends ListFieldBase {
}
}

export type ComponentDef = FormComponentsDef | ContentComponentsDef
export interface ComposableComponent {
before?: ComponentDef | ComponentDef[]
after?: ComponentDef | ComponentDef[]
}

export type ComponentDef = (FormComponentsDef | ContentComponentsDef) &
Partial<ComposableComponent>

// Components that render form fields
export type FormComponentsDef =
Expand Down
9 changes: 8 additions & 1 deletion model/src/form/form-definition/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,14 @@ export const componentSchema = Joi.object<ComponentDef>()
.optional()
.description(
'Reference to a predefined list of options for select components'
)
),
// Just for the spike: Composable components - allow before/after
before: Joi.any()
.optional()
.description('Component(s) to render before this component'),
after: Joi.any()
.optional()
.description('Component(s) to render after this component')
})
.unknown(true)

Expand Down
Loading