Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add back conditionally revealed components #1251

Open
jenbutongit opened this issue May 22, 2024 · 3 comments
Open

Add back conditionally revealed components #1251

jenbutongit opened this issue May 22, 2024 · 3 comments

Comments

@jenbutongit
Copy link
Contributor

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

@jenbutongit
Copy link
Contributor Author

jenbutongit commented May 22, 2024

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

  1. In SelectionControlField's constructor, first check if the component has anything defined in options.conditionallyRevealedComponents.
  2. Create these components and store these fields internally, e.g this.conditionallyRevealedComponents
  3. It might also be helpful to create a helper flag, e.g. this.hasConditionallyRevealedComponents
  4. in getViewModel, check if this.hasConditionallyRevealedComponents, if so, render the html and insert it into the viewModel.

@jenbutongit jenbutongit changed the title Add back conditional components Add back conditionally revealed components May 23, 2024
@emilyjevans
Copy link

emilyjevans commented May 29, 2024

@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;
  }
}

@jenbutongit
Copy link
Contributor Author

jenbutongit commented May 29, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants