Open jenbutongit opened 1 month ago
IIRC, the conditional components were loaded in via the list property which was confusing. It also meant that lists couldn't be reused.
It might be a better idea to make this a part of the component definition.
{
"startPage": "/start",
"pages": [
{
"path": "/start",
"title": "Start",
"components": [
{
"type": "RadiosField",
"title": "How would you prefer to be contacted?",
"list": "contactTypes"
"options": {
"conditionallyRevealedComponents": {
"email": {
"type": "EmailAddressField",
"name": "email",
"title": "Your email address",
"options": {},
"schema": {}
},
"phone": {
"type": "TelephoneNumberField",
"name": "phoneNumber",
"title": "Your phone number",
"options": {},
"schema": {}
}
}
},
"schema": {}
}
],
"next": []
}
],
"lists": [
{
"name": "contactTypes",
"title": "Contact Types",
"type": "string",
"items": [
{
"text": "Email",
"value": "email"
},
{
"text": "Phone",
"value": "phone"
}
]
}
],
"sections": [
],
"phaseBanner": {},
"fees": [],
"payApiKey": "",
"outputs": [
],
"declaration": "",
"version": 2,
"conditions": []
}
where conditionallyRevealedComponents
is an object. The key, or property name, must match the list
value.
In the above example, the list values are email
and phone
, so those must be the keys.
"conditionallyRevealedComponents": {
"email": {}
"phone": {}
}
The values must be component definitions, but the only allowed components should be Input text fields, specifically TextField
, NumberField
, EmailAddressField
, TelephoneNumberField
. Date fields are also allowed, but that might be somewhat complex to implement. Worth a try, if not it can be added later down the line.
Functionality was removed in this PR: https://github.com/XGovFormBuilder/digital-form-builder/pull/549/files#diff-3c71b7c94d2f3869938661b678fe09f906e8b8cca086b1000ab46abc82480541
It might be more "proper" (according to Joi functionality) to use joi's any.alter, and joi's any.tailor. It may also be useful to use joi's reference/relative selectors rather than iterating through the schema keys. https://joi.dev/api/?v=17.13.0#refkey-options
The general gist for implementation would be
SelectionControlField
's constructor, first check if the component has anything defined in options.conditionallyRevealedComponents
.this.hasConditionallyRevealedComponents
this.hasConditionallyRevealedComponents
, if so, render the html and insert it into the viewModel.@jenbutongit Here's what I have so far on the SelectionControlField. The joi validation doesn't seem to work for email and text field here but I can't understand why they'd be different. Unfortunately getting slightly different behaviour on our forked repo which is out of sync, where the validation does work 🤔
I can't find many examples online of using joi.alter
and joi.tailor
online so having trouble envisaging what that would look like - do you have any more examples or docs on that?
import joi from "joi";
import nunjucks from "nunjucks";
import { ListFormComponent } from "server/plugins/engine/components/ListFormComponent";
import { FormData, FormSubmissionErrors } from "server/plugins/engine/types";
import { ListItem, ViewModel } from "server/plugins/engine/components/types";
import { ComponentCollection } from "./ComponentCollection";
/**
* "Selection controls" are checkboxes and radios (and switches), as per Material UI nomenclature.
*/
const getSchemaKeys = Symbol("getSchemaKeys");
export class SelectionControlField extends ListFormComponent {
conditionallyRevealedComponents?: any;
hasConditionallyRevealedComponents: boolean = false;
constructor(def, model) {
super(def, model);
const { options } = def;
const { items } = this;
if (options.conditionallyRevealedComponents) {
this.conditionallyRevealedComponents =
options.conditionallyRevealedComponents;
items.map((item: any) => {
if (this.conditionallyRevealedComponents![item.value]) {
item.hasConditionallyRevealedComponents = true;
item.conditionallyRevealedComponents = new ComponentCollection(
[this.conditionallyRevealedComponents![item.value]],
item.model
);
}
});
}
}
getViewModel(formData: FormData, errors: FormSubmissionErrors) {
const { name, items } = this;
const options: any = this.options;
const viewModel: ViewModel = super.getViewModel(formData, errors);
viewModel.fieldset = {
legend: viewModel.label,
};
viewModel.items = items.map((item: any) => {
const itemModel: ListItem = {
text: item.text,
value: item.value,
checked: `${item.value}` === `${formData[name]}`,
};
if (options.bold) {
itemModel.label = {
classes: "govuk-label--s",
};
}
if (item.description) {
itemModel.hint = {
html: this.localisedString(item.description),
};
}
if (options.conditionallyRevealedComponents[item.value]) {
// The gov.uk design system Nunjucks examples for conditional reveal reference variables from macros. There does not appear to
// to be a way to do this in JavaScript. As such, render the conditional components with Nunjucks before the main view is rendered.
// The conditional html tag used by the gov.uk design system macro will reference HTML rarther than one or more additional
// gov.uk design system macros.
itemModel.conditional = {
html: nunjucks.render(
"../views/partials/conditional-components.html",
{
components: item.conditionallyRevealedComponents.getViewModel(
formData,
errors
),
}
),
};
}
return itemModel;
});
return viewModel;
}
getStateSchemaKeys() {
return this[getSchemaKeys]("state");
}
getFormSchemaKeys() {
return this[getSchemaKeys]("form");
}
[getSchemaKeys](schemaType) {
const schemaName = `${schemaType}Schema`;
const schemaKeysFunctionName = `get${schemaType
.substring(0, 1)
.toUpperCase()}${schemaType.substring(1)}SchemaKeys`;
const filteredItems = this.items.filter(
(item: any) => item.hasConditionallyRevealedComponents
);
const conditionalName = this.name;
const schemaKeys = { [conditionalName]: this[schemaName] };
// const schema = this[schemaName];
// All conditional component values are submitted regardless of their visibilty.
// As such create Joi validation rules such that:
// a) When a conditional component is visible it is required.
// b) When a conditional component is not visible it is optional.
filteredItems?.forEach((item: any) => {
const conditionalSchemaKeys = item.conditionallyRevealedComponents[
schemaKeysFunctionName
]();
// Iterate through the set of components handled by conditional reveal adding Joi validation rules
// based on whether or not the component controlling the conditional reveal is selected.
Object.keys(conditionalSchemaKeys).forEach((key) => {
Object.assign(schemaKeys, {
[key]: joi.alternatives().conditional(joi.ref(conditionalName), {
is: key,
then: conditionalSchemaKeys[key],
otherwise: joi.optional(),
// TODO: modify for checkboxes
}),
});
});
});
return schemaKeys;
}
}
Sorry! I was misremembering. It should be https://joi.dev/api/?v=17.13.0#anyforkpaths-adjuster. After initialising the component's schema, you'd then want to alter it to accept the new fields. I've done this in RepeatingFieldPageController
Is your feature request related to a problem? Please describe.
Conditional components were previously removed because it was reported to be an accessibility issue. GDS and W3C have since confirmed that conditional components are OK as long as the revealed field is "simple" and there is only one field revealed.
Describe the solution you'd like Add support for conditional components
Describe alternatives you've considered
Additional context