You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Following on from the discussion in #35, this issue is to clarify our intent for this.
We would like to expose a FormGroup component so that it can be used by library consumers, so that all the boiler plate and logic for error styles, hint text etc can be encapsulated and easily reused. This will help us in Forms so that we can focus on our custom components and not repeat all the boiler plate.
There is some complexity here as the existing FormGroup component was not designed to be exposed and it accepts a function instead of child components in the usual way.
The goal is to have syntax like this:
import React from "react";
import { FormGroup, Input } from "nhsuk-react-components";
const ExampleUsage = () => (
<FormGroup>
<Input hint="Test Hint" label="Test Label" />
<a href="/somewhere-else">Don't have a number?</a>
</FormGroup>
);
Emit something like this:
<div class="nhsuk-form-group">
<label class="nhsuk-label">Test Label</label>
<span class="nhsuk-hint">Test Hint</span>
<input />
<a href="/somewhere-else">Don't have a number?</a>
</div>
The new / exposed FormGroup should support at least the following props (and associated functionality):
hint
label
error
Questions
Should we also support disableErrorLine?
What else is included with labelProps, hintProps, errorProps and formGroupProps, and should we support them?
Fyi. Forms team, we're tracking this in Jira as WBFCD-399.
If the FormGroup wants to be exposed a bit more naturally, some context would need to be used and implemented in each Input component to ensure that all relevant IDs are also passed along with it.
In practice, this might look something like:
constFormGroup=()=>{const[inputID,setInputID]=useState();return(<FormGroupContext.Providervalue={{ setInputID }}><LabelhtmlFor={inputID}>{label}</Label>{children}</FormGroupContext.Provider>)}constInput=({ id })=>{const{ setInputID }=useContext(FormGroupContext);useEffect(()=>{setInputID(id);return()=>setInputID(undefined);},[id])return<Inputid={id}/>}
Though I can see this approach breaking down if multiple Inputs are being used inside a single FormGroup, in which case the render function might work better. Both approaches could be used by doing a check on the children prop:
Sorry if I'm missing some context! Instead of render props, I wonder if we do checks on the children instead? The below is a "prescriptive" layout, although you could just {children} if you don't care much for the order.
//FormGroup.tsimport{Input,Label,Hint,Link}from'~components'functionFormGroup({ hasErrors =false, children }){constchildrenArray=React.children.toArray(children);constInputChild=childrenArray.find(child=>child.type===Input)??null;// or typescript guard. You can use child.type === Input if Input is a function constLabelChild=childrenArray.find(child=>child.type===Label)??null;constHintChild=childrenArray.find(child=>child.type===Hint)??null;constLinkChild=childrenArray.find(child=>child.type===Link)??null;return(<divclassName={classNames('nhsuk-form-group',{'nhsuk-input--error': hasError})}>{LabelChild}{InputChild}{HintChild}{LinkChild}</div>);}
n.b. I forget if you are able to use child.type === Input if Input is an anonymous function (const Input = (..) => { }). It should work if it's defined function Input(..)
constExampleForm=({ children })=>{const[firstName,setFirstName]=useState('');const[hasErrors,setHasErrors]=useState(false);constonFirstNameChange=event=>{// validation, or whatever elsesetFirstName(event.target.value);};functiononSubmit(e){e.preventDefault();// post request}return(<formonSubmit={onSubmit}><FormGrouphasErrors={hasErrors}><LabelhtmlFor="first-name">{i18n('firstName.title')}</Label><Hint>{i18n('firstName.hint')}</Hint><Inputid="first-name"value={firstName}onChange={onFirstNameChange}/></FormGroup></form>);};
Of course, in ExampleForm you can hoist the id / htmlFor into a variable. id={inputId}. ExampleForm is how is more likely to be used. The logic for validation is most likely going to be at this level, as well as submitting, so it would make sense to define these here, instead of at FormGroup level.
Using a Context would be overkill for just an id, there's no prop drilling involved really. It would also involve refactoring the input and label to be consumers (adding useContext(..)). Contexts can make testing a pain as well. Providers need to wrap the component under test (whether or not it's shallow or a full render) or it will throw an error
The approach you suggested was actually the original way the forms worked (pre-1.0) - however what we found with it in practice is it's quite restrictive with what you can use inside that element. For example, the following would break:
The workaround would be to scan all potential child elements of all the children, which ends up being a lot more code and a much more expensive operation on every render than using context.
@Tomdango would/is there be a case where you would wrap the input, label, link etc within FromGroup, but not use the FormGroup wrapper + additional classes itself? It's already a wrapping component
Activity
Tomdango commentedon Feb 3, 2021
The current
FormGrouptakes a function as it's children, and passes through all relevant props, i.e.If the
FormGroupwants to be exposed a bit more naturally, some context would need to be used and implemented in each Input component to ensure that all relevant IDs are also passed along with it.In practice, this might look something like:
Though I can see this approach breaking down if multiple Inputs are being used inside a single FormGroup, in which case the render function might work better. Both approaches could be used by doing a check on the
childrenprop:jenbutongit commentedon Feb 9, 2021
Sorry if I'm missing some context! Instead of render props, I wonder if we do checks on the children instead? The below is a "prescriptive" layout, although you could just {children} if you don't care much for the order.
n.b. I forget if you are able to use
child.type === Inputif Input is an anonymous function (const Input = (..) => { }). It should work if it's definedfunction Input(..)Of course, in
ExampleFormyou can hoist the id / htmlFor into a variable.id={inputId}. ExampleForm is how is more likely to be used. The logic for validation is most likely going to be at this level, as well as submitting, so it would make sense to define these here, instead of at FormGroup level.Using a Context would be overkill for just an id, there's no prop drilling involved really. It would also involve refactoring the input and label to be consumers (adding
useContext(..)). Contexts can make testing a pain as well. Providers need to wrap the component under test (whether or not it's shallow or a full render) or it will throw an errorTomdango commentedon Feb 10, 2021
Hey @jenbutongit
The approach you suggested was actually the original way the forms worked (pre-1.0) - however what we found with it in practice is it's quite restrictive with what you can use inside that element. For example, the following would break:
The workaround would be to scan all potential child elements of all the children, which ends up being a lot more code and a much more expensive operation on every render than using context.
jenbutongit commentedon Feb 10, 2021
@Tomdango would/is there be a case where you would wrap the input, label, link etc within FromGroup, but not use the FormGroup wrapper + additional classes itself? It's already a wrapping component
#71 exposing form group component to the consumers, (error, hint, lab…
NHSDigital#71 exposing form group component to the consumers, (error,…
colinrotherham commentedon Sep 9, 2025
Looks like this was added in
<SingleInputFormGroup>in the current releaseI've extended it to support components with
<fieldset>and<legend>HTML elements in:This aligns the date input, radios and checkboxes components with NHS.UK frontend
I've also renamed it to
<SingleInputFormGroup><FormGroup>ready to use