@defra/forms-engine-plugin 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -49,6 +49,18 @@ export class UkAddressField extends FormComponent {
49
49
  required: isRequired,
50
50
  optionalText: !isRequired && (hideOptional || !hideTitle)
51
51
  }
52
+ }, {
53
+ type: ComponentType.TextField,
54
+ name: `${name}__county`,
55
+ title: 'County',
56
+ schema: {
57
+ max: 100
58
+ },
59
+ options: {
60
+ autocomplete: 'county',
61
+ required: false,
62
+ optionalText: !isRequired && (hideOptional || !hideTitle)
63
+ }
52
64
  }, {
53
65
  type: ComponentType.TextField,
54
66
  name: `${name}__postcode`,
@@ -1 +1 @@
1
- {"version":3,"file":"UkAddressField.js","names":["ComponentType","ComponentCollection","FormComponent","isFormState","TextField","UkAddressField","constructor","def","props","name","options","isRequired","required","hideOptional","optionalText","hideTitle","collection","type","title","schema","max","autocomplete","classes","regex","parent","formSchema","stateSchema","getFormValueFromState","state","value","isState","undefined","getDisplayStringFromState","getContextValueFromState","join","Object","values","filter","Boolean","getViewModel","payload","errors","viewModel","components","fieldset","hint","label","legend","text","id","attributes","isUkAddress","isText","addressLine1","town","postcode"],"sources":["../../../../../src/server/plugins/engine/components/UkAddressField.ts"],"sourcesContent":["import { ComponentType, type UkAddressFieldComponent } from '@defra/forms-model'\nimport { type ObjectSchema } from 'joi'\n\nimport { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'\nimport {\n FormComponent,\n isFormState\n} from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { TextField } from '~/src/server/plugins/engine/components/TextField.js'\nimport { type QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport {\n type FormPayload,\n type FormState,\n type FormStateValue,\n type FormSubmissionError,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\n\nexport class UkAddressField extends FormComponent {\n declare options: UkAddressFieldComponent['options']\n declare formSchema: ObjectSchema<FormPayload>\n declare stateSchema: ObjectSchema<FormState>\n declare collection: ComponentCollection\n\n constructor(\n def: UkAddressFieldComponent,\n props: ConstructorParameters<typeof FormComponent>[1]\n ) {\n super(def, props)\n\n const { name, options } = def\n\n const isRequired = options.required !== false\n const hideOptional = !!options.optionalText\n const hideTitle = !!options.hideTitle\n\n this.collection = new ComponentCollection(\n [\n {\n type: ComponentType.TextField,\n name: `${name}__addressLine1`,\n title: 'Address line 1',\n schema: { max: 100 },\n options: {\n autocomplete: 'address-line1',\n required: isRequired,\n optionalText: !isRequired && (hideOptional || !hideTitle)\n }\n },\n {\n type: ComponentType.TextField,\n name: `${name}__addressLine2`,\n title: 'Address line 2',\n schema: { max: 100 },\n options: {\n autocomplete: 'address-line2',\n required: false,\n optionalText: !isRequired && (hideOptional || !hideTitle)\n }\n },\n {\n type: ComponentType.TextField,\n name: `${name}__town`,\n title: 'Town or city',\n schema: { max: 100 },\n options: {\n autocomplete: 'address-level2',\n classes: 'govuk-!-width-two-thirds',\n required: isRequired,\n optionalText: !isRequired && (hideOptional || !hideTitle)\n }\n },\n {\n type: ComponentType.TextField,\n name: `${name}__postcode`,\n title: 'Postcode',\n schema: {\n regex: '^[a-zA-Z]{1,2}\\\\d[a-zA-Z\\\\d]?\\\\s?\\\\d[a-zA-Z]{2}$'\n },\n options: {\n autocomplete: 'postal-code',\n classes: 'govuk-input--width-10',\n required: isRequired,\n optionalText: !isRequired && (hideOptional || !hideTitle)\n }\n }\n ],\n { ...props, parent: this }\n )\n\n this.options = options\n this.formSchema = this.collection.formSchema\n this.stateSchema = this.collection.stateSchema\n }\n\n getFormValueFromState(state: FormSubmissionState) {\n const value = super.getFormValueFromState(state)\n return this.isState(value) ? value : undefined\n }\n\n getDisplayStringFromState(state: FormSubmissionState) {\n return this.getContextValueFromState(state)?.join(', ') ?? ''\n }\n\n getContextValueFromState(state: FormSubmissionState) {\n const value = this.getFormValueFromState(state)\n\n if (!value) {\n return null\n }\n\n return Object.values(value).filter(Boolean)\n }\n\n getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) {\n const { collection, name, options } = this\n\n const viewModel = super.getViewModel(payload, errors)\n let { components, fieldset, hint, label } = viewModel\n\n fieldset ??= {\n legend: {\n text: label.text,\n\n /**\n * For screen readers, only hide legend visually. This can be overridden\n * by single component {@link QuestionPageController | `showTitle` handling}\n */\n classes: options.hideTitle\n ? 'govuk-visually-hidden'\n : 'govuk-fieldset__legend--m'\n }\n }\n\n if (hint) {\n hint.id ??= `${name}-hint`\n fieldset.attributes ??= {\n 'aria-describedby': hint.id\n }\n }\n\n components = collection.getViewModel(payload, errors)\n\n return {\n ...viewModel,\n fieldset,\n components\n }\n }\n\n isState(value?: FormStateValue | FormState): value is UkAddressState {\n return UkAddressField.isUkAddress(value)\n }\n\n static isUkAddress(\n value?: FormStateValue | FormState\n ): value is UkAddressState {\n return (\n isFormState(value) &&\n TextField.isText(value.addressLine1) &&\n TextField.isText(value.town) &&\n TextField.isText(value.postcode)\n )\n }\n}\n\nexport interface UkAddressState extends Record<string, string> {\n addressLine1: string\n addressLine2: string\n town: string\n postcode: string\n}\n"],"mappings":"AAAA,SAASA,aAAa,QAAsC,oBAAoB;AAGhF,SAASC,mBAAmB;AAC5B,SACEC,aAAa,EACbC,WAAW;AAEb,SAASC,SAAS;AAUlB,OAAO,MAAMC,cAAc,SAASH,aAAa,CAAC;EAMhDI,WAAWA,CACTC,GAA4B,EAC5BC,KAAqD,EACrD;IACA,KAAK,CAACD,GAAG,EAAEC,KAAK,CAAC;IAEjB,MAAM;MAAEC,IAAI;MAAEC;IAAQ,CAAC,GAAGH,GAAG;IAE7B,MAAMI,UAAU,GAAGD,OAAO,CAACE,QAAQ,KAAK,KAAK;IAC7C,MAAMC,YAAY,GAAG,CAAC,CAACH,OAAO,CAACI,YAAY;IAC3C,MAAMC,SAAS,GAAG,CAAC,CAACL,OAAO,CAACK,SAAS;IAErC,IAAI,CAACC,UAAU,GAAG,IAAIf,mBAAmB,CACvC,CACE;MACEgB,IAAI,EAAEjB,aAAa,CAACI,SAAS;MAC7BK,IAAI,EAAE,GAAGA,IAAI,gBAAgB;MAC7BS,KAAK,EAAE,gBAAgB;MACvBC,MAAM,EAAE;QAAEC,GAAG,EAAE;MAAI,CAAC;MACpBV,OAAO,EAAE;QACPW,YAAY,EAAE,eAAe;QAC7BT,QAAQ,EAAED,UAAU;QACpBG,YAAY,EAAE,CAACH,UAAU,KAAKE,YAAY,IAAI,CAACE,SAAS;MAC1D;IACF,CAAC,EACD;MACEE,IAAI,EAAEjB,aAAa,CAACI,SAAS;MAC7BK,IAAI,EAAE,GAAGA,IAAI,gBAAgB;MAC7BS,KAAK,EAAE,gBAAgB;MACvBC,MAAM,EAAE;QAAEC,GAAG,EAAE;MAAI,CAAC;MACpBV,OAAO,EAAE;QACPW,YAAY,EAAE,eAAe;QAC7BT,QAAQ,EAAE,KAAK;QACfE,YAAY,EAAE,CAACH,UAAU,KAAKE,YAAY,IAAI,CAACE,SAAS;MAC1D;IACF,CAAC,EACD;MACEE,IAAI,EAAEjB,aAAa,CAACI,SAAS;MAC7BK,IAAI,EAAE,GAAGA,IAAI,QAAQ;MACrBS,KAAK,EAAE,cAAc;MACrBC,MAAM,EAAE;QAAEC,GAAG,EAAE;MAAI,CAAC;MACpBV,OAAO,EAAE;QACPW,YAAY,EAAE,gBAAgB;QAC9BC,OAAO,EAAE,0BAA0B;QACnCV,QAAQ,EAAED,UAAU;QACpBG,YAAY,EAAE,CAACH,UAAU,KAAKE,YAAY,IAAI,CAACE,SAAS;MAC1D;IACF,CAAC,EACD;MACEE,IAAI,EAAEjB,aAAa,CAACI,SAAS;MAC7BK,IAAI,EAAE,GAAGA,IAAI,YAAY;MACzBS,KAAK,EAAE,UAAU;MACjBC,MAAM,EAAE;QACNI,KAAK,EAAE;MACT,CAAC;MACDb,OAAO,EAAE;QACPW,YAAY,EAAE,aAAa;QAC3BC,OAAO,EAAE,uBAAuB;QAChCV,QAAQ,EAAED,UAAU;QACpBG,YAAY,EAAE,CAACH,UAAU,KAAKE,YAAY,IAAI,CAACE,SAAS;MAC1D;IACF,CAAC,CACF,EACD;MAAE,GAAGP,KAAK;MAAEgB,MAAM,EAAE;IAAK,CAC3B,CAAC;IAED,IAAI,CAACd,OAAO,GAAGA,OAAO;IACtB,IAAI,CAACe,UAAU,GAAG,IAAI,CAACT,UAAU,CAACS,UAAU;IAC5C,IAAI,CAACC,WAAW,GAAG,IAAI,CAACV,UAAU,CAACU,WAAW;EAChD;EAEAC,qBAAqBA,CAACC,KAA0B,EAAE;IAChD,MAAMC,KAAK,GAAG,KAAK,CAACF,qBAAqB,CAACC,KAAK,CAAC;IAChD,OAAO,IAAI,CAACE,OAAO,CAACD,KAAK,CAAC,GAAGA,KAAK,GAAGE,SAAS;EAChD;EAEAC,yBAAyBA,CAACJ,KAA0B,EAAE;IACpD,OAAO,IAAI,CAACK,wBAAwB,CAACL,KAAK,CAAC,EAAEM,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;EAC/D;EAEAD,wBAAwBA,CAACL,KAA0B,EAAE;IACnD,MAAMC,KAAK,GAAG,IAAI,CAACF,qBAAqB,CAACC,KAAK,CAAC;IAE/C,IAAI,CAACC,KAAK,EAAE;MACV,OAAO,IAAI;IACb;IAEA,OAAOM,MAAM,CAACC,MAAM,CAACP,KAAK,CAAC,CAACQ,MAAM,CAACC,OAAO,CAAC;EAC7C;EAEAC,YAAYA,CAACC,OAAoB,EAAEC,MAA8B,EAAE;IACjE,MAAM;MAAEzB,UAAU;MAAEP,IAAI;MAAEC;IAAQ,CAAC,GAAG,IAAI;IAE1C,MAAMgC,SAAS,GAAG,KAAK,CAACH,YAAY,CAACC,OAAO,EAAEC,MAAM,CAAC;IACrD,IAAI;MAAEE,UAAU;MAAEC,QAAQ;MAAEC,IAAI;MAAEC;IAAM,CAAC,GAAGJ,SAAS;IAErDE,QAAQ,KAAK;MACXG,MAAM,EAAE;QACNC,IAAI,EAAEF,KAAK,CAACE,IAAI;QAEhB;AACR;AACA;AACA;QACQ1B,OAAO,EAAEZ,OAAO,CAACK,SAAS,GACtB,uBAAuB,GACvB;MACN;IACF,CAAC;IAED,IAAI8B,IAAI,EAAE;MACRA,IAAI,CAACI,EAAE,KAAK,GAAGxC,IAAI,OAAO;MAC1BmC,QAAQ,CAACM,UAAU,KAAK;QACtB,kBAAkB,EAAEL,IAAI,CAACI;MAC3B,CAAC;IACH;IAEAN,UAAU,GAAG3B,UAAU,CAACuB,YAAY,CAACC,OAAO,EAAEC,MAAM,CAAC;IAErD,OAAO;MACL,GAAGC,SAAS;MACZE,QAAQ;MACRD;IACF,CAAC;EACH;EAEAb,OAAOA,CAACD,KAAkC,EAA2B;IACnE,OAAOxB,cAAc,CAAC8C,WAAW,CAACtB,KAAK,CAAC;EAC1C;EAEA,OAAOsB,WAAWA,CAChBtB,KAAkC,EACT;IACzB,OACE1B,WAAW,CAAC0B,KAAK,CAAC,IAClBzB,SAAS,CAACgD,MAAM,CAACvB,KAAK,CAACwB,YAAY,CAAC,IACpCjD,SAAS,CAACgD,MAAM,CAACvB,KAAK,CAACyB,IAAI,CAAC,IAC5BlD,SAAS,CAACgD,MAAM,CAACvB,KAAK,CAAC0B,QAAQ,CAAC;EAEpC;AACF","ignoreList":[]}
1
+ {"version":3,"file":"UkAddressField.js","names":["ComponentType","ComponentCollection","FormComponent","isFormState","TextField","UkAddressField","constructor","def","props","name","options","isRequired","required","hideOptional","optionalText","hideTitle","collection","type","title","schema","max","autocomplete","classes","regex","parent","formSchema","stateSchema","getFormValueFromState","state","value","isState","undefined","getDisplayStringFromState","getContextValueFromState","join","Object","values","filter","Boolean","getViewModel","payload","errors","viewModel","components","fieldset","hint","label","legend","text","id","attributes","isUkAddress","isText","addressLine1","town","postcode"],"sources":["../../../../../src/server/plugins/engine/components/UkAddressField.ts"],"sourcesContent":["import { ComponentType, type UkAddressFieldComponent } from '@defra/forms-model'\nimport { type ObjectSchema } from 'joi'\n\nimport { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'\nimport {\n FormComponent,\n isFormState\n} from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { TextField } from '~/src/server/plugins/engine/components/TextField.js'\nimport { type QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport {\n type FormPayload,\n type FormState,\n type FormStateValue,\n type FormSubmissionError,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\n\nexport class UkAddressField extends FormComponent {\n declare options: UkAddressFieldComponent['options']\n declare formSchema: ObjectSchema<FormPayload>\n declare stateSchema: ObjectSchema<FormState>\n declare collection: ComponentCollection\n\n constructor(\n def: UkAddressFieldComponent,\n props: ConstructorParameters<typeof FormComponent>[1]\n ) {\n super(def, props)\n\n const { name, options } = def\n\n const isRequired = options.required !== false\n const hideOptional = !!options.optionalText\n const hideTitle = !!options.hideTitle\n\n this.collection = new ComponentCollection(\n [\n {\n type: ComponentType.TextField,\n name: `${name}__addressLine1`,\n title: 'Address line 1',\n schema: { max: 100 },\n options: {\n autocomplete: 'address-line1',\n required: isRequired,\n optionalText: !isRequired && (hideOptional || !hideTitle)\n }\n },\n {\n type: ComponentType.TextField,\n name: `${name}__addressLine2`,\n title: 'Address line 2',\n schema: { max: 100 },\n options: {\n autocomplete: 'address-line2',\n required: false,\n optionalText: !isRequired && (hideOptional || !hideTitle)\n }\n },\n {\n type: ComponentType.TextField,\n name: `${name}__town`,\n title: 'Town or city',\n schema: { max: 100 },\n options: {\n autocomplete: 'address-level2',\n classes: 'govuk-!-width-two-thirds',\n required: isRequired,\n optionalText: !isRequired && (hideOptional || !hideTitle)\n }\n },\n {\n type: ComponentType.TextField,\n name: `${name}__county`,\n title: 'County',\n schema: { max: 100 },\n options: {\n autocomplete: 'county',\n required: false,\n optionalText: !isRequired && (hideOptional || !hideTitle)\n }\n },\n {\n type: ComponentType.TextField,\n name: `${name}__postcode`,\n title: 'Postcode',\n schema: {\n regex: '^[a-zA-Z]{1,2}\\\\d[a-zA-Z\\\\d]?\\\\s?\\\\d[a-zA-Z]{2}$'\n },\n options: {\n autocomplete: 'postal-code',\n classes: 'govuk-input--width-10',\n required: isRequired,\n optionalText: !isRequired && (hideOptional || !hideTitle)\n }\n }\n ],\n { ...props, parent: this }\n )\n\n this.options = options\n this.formSchema = this.collection.formSchema\n this.stateSchema = this.collection.stateSchema\n }\n\n getFormValueFromState(state: FormSubmissionState) {\n const value = super.getFormValueFromState(state)\n return this.isState(value) ? value : undefined\n }\n\n getDisplayStringFromState(state: FormSubmissionState) {\n return this.getContextValueFromState(state)?.join(', ') ?? ''\n }\n\n getContextValueFromState(state: FormSubmissionState) {\n const value = this.getFormValueFromState(state)\n\n if (!value) {\n return null\n }\n\n return Object.values(value).filter(Boolean)\n }\n\n getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) {\n const { collection, name, options } = this\n\n const viewModel = super.getViewModel(payload, errors)\n let { components, fieldset, hint, label } = viewModel\n\n fieldset ??= {\n legend: {\n text: label.text,\n\n /**\n * For screen readers, only hide legend visually. This can be overridden\n * by single component {@link QuestionPageController | `showTitle` handling}\n */\n classes: options.hideTitle\n ? 'govuk-visually-hidden'\n : 'govuk-fieldset__legend--m'\n }\n }\n\n if (hint) {\n hint.id ??= `${name}-hint`\n fieldset.attributes ??= {\n 'aria-describedby': hint.id\n }\n }\n\n components = collection.getViewModel(payload, errors)\n\n return {\n ...viewModel,\n fieldset,\n components\n }\n }\n\n isState(value?: FormStateValue | FormState): value is UkAddressState {\n return UkAddressField.isUkAddress(value)\n }\n\n static isUkAddress(\n value?: FormStateValue | FormState\n ): value is UkAddressState {\n return (\n isFormState(value) &&\n TextField.isText(value.addressLine1) &&\n TextField.isText(value.town) &&\n TextField.isText(value.postcode)\n )\n }\n}\n\nexport interface UkAddressState extends Record<string, string> {\n addressLine1: string\n addressLine2: string\n town: string\n county: string\n postcode: string\n}\n"],"mappings":"AAAA,SAASA,aAAa,QAAsC,oBAAoB;AAGhF,SAASC,mBAAmB;AAC5B,SACEC,aAAa,EACbC,WAAW;AAEb,SAASC,SAAS;AAUlB,OAAO,MAAMC,cAAc,SAASH,aAAa,CAAC;EAMhDI,WAAWA,CACTC,GAA4B,EAC5BC,KAAqD,EACrD;IACA,KAAK,CAACD,GAAG,EAAEC,KAAK,CAAC;IAEjB,MAAM;MAAEC,IAAI;MAAEC;IAAQ,CAAC,GAAGH,GAAG;IAE7B,MAAMI,UAAU,GAAGD,OAAO,CAACE,QAAQ,KAAK,KAAK;IAC7C,MAAMC,YAAY,GAAG,CAAC,CAACH,OAAO,CAACI,YAAY;IAC3C,MAAMC,SAAS,GAAG,CAAC,CAACL,OAAO,CAACK,SAAS;IAErC,IAAI,CAACC,UAAU,GAAG,IAAIf,mBAAmB,CACvC,CACE;MACEgB,IAAI,EAAEjB,aAAa,CAACI,SAAS;MAC7BK,IAAI,EAAE,GAAGA,IAAI,gBAAgB;MAC7BS,KAAK,EAAE,gBAAgB;MACvBC,MAAM,EAAE;QAAEC,GAAG,EAAE;MAAI,CAAC;MACpBV,OAAO,EAAE;QACPW,YAAY,EAAE,eAAe;QAC7BT,QAAQ,EAAED,UAAU;QACpBG,YAAY,EAAE,CAACH,UAAU,KAAKE,YAAY,IAAI,CAACE,SAAS;MAC1D;IACF,CAAC,EACD;MACEE,IAAI,EAAEjB,aAAa,CAACI,SAAS;MAC7BK,IAAI,EAAE,GAAGA,IAAI,gBAAgB;MAC7BS,KAAK,EAAE,gBAAgB;MACvBC,MAAM,EAAE;QAAEC,GAAG,EAAE;MAAI,CAAC;MACpBV,OAAO,EAAE;QACPW,YAAY,EAAE,eAAe;QAC7BT,QAAQ,EAAE,KAAK;QACfE,YAAY,EAAE,CAACH,UAAU,KAAKE,YAAY,IAAI,CAACE,SAAS;MAC1D;IACF,CAAC,EACD;MACEE,IAAI,EAAEjB,aAAa,CAACI,SAAS;MAC7BK,IAAI,EAAE,GAAGA,IAAI,QAAQ;MACrBS,KAAK,EAAE,cAAc;MACrBC,MAAM,EAAE;QAAEC,GAAG,EAAE;MAAI,CAAC;MACpBV,OAAO,EAAE;QACPW,YAAY,EAAE,gBAAgB;QAC9BC,OAAO,EAAE,0BAA0B;QACnCV,QAAQ,EAAED,UAAU;QACpBG,YAAY,EAAE,CAACH,UAAU,KAAKE,YAAY,IAAI,CAACE,SAAS;MAC1D;IACF,CAAC,EACD;MACEE,IAAI,EAAEjB,aAAa,CAACI,SAAS;MAC7BK,IAAI,EAAE,GAAGA,IAAI,UAAU;MACvBS,KAAK,EAAE,QAAQ;MACfC,MAAM,EAAE;QAAEC,GAAG,EAAE;MAAI,CAAC;MACpBV,OAAO,EAAE;QACPW,YAAY,EAAE,QAAQ;QACtBT,QAAQ,EAAE,KAAK;QACfE,YAAY,EAAE,CAACH,UAAU,KAAKE,YAAY,IAAI,CAACE,SAAS;MAC1D;IACF,CAAC,EACD;MACEE,IAAI,EAAEjB,aAAa,CAACI,SAAS;MAC7BK,IAAI,EAAE,GAAGA,IAAI,YAAY;MACzBS,KAAK,EAAE,UAAU;MACjBC,MAAM,EAAE;QACNI,KAAK,EAAE;MACT,CAAC;MACDb,OAAO,EAAE;QACPW,YAAY,EAAE,aAAa;QAC3BC,OAAO,EAAE,uBAAuB;QAChCV,QAAQ,EAAED,UAAU;QACpBG,YAAY,EAAE,CAACH,UAAU,KAAKE,YAAY,IAAI,CAACE,SAAS;MAC1D;IACF,CAAC,CACF,EACD;MAAE,GAAGP,KAAK;MAAEgB,MAAM,EAAE;IAAK,CAC3B,CAAC;IAED,IAAI,CAACd,OAAO,GAAGA,OAAO;IACtB,IAAI,CAACe,UAAU,GAAG,IAAI,CAACT,UAAU,CAACS,UAAU;IAC5C,IAAI,CAACC,WAAW,GAAG,IAAI,CAACV,UAAU,CAACU,WAAW;EAChD;EAEAC,qBAAqBA,CAACC,KAA0B,EAAE;IAChD,MAAMC,KAAK,GAAG,KAAK,CAACF,qBAAqB,CAACC,KAAK,CAAC;IAChD,OAAO,IAAI,CAACE,OAAO,CAACD,KAAK,CAAC,GAAGA,KAAK,GAAGE,SAAS;EAChD;EAEAC,yBAAyBA,CAACJ,KAA0B,EAAE;IACpD,OAAO,IAAI,CAACK,wBAAwB,CAACL,KAAK,CAAC,EAAEM,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;EAC/D;EAEAD,wBAAwBA,CAACL,KAA0B,EAAE;IACnD,MAAMC,KAAK,GAAG,IAAI,CAACF,qBAAqB,CAACC,KAAK,CAAC;IAE/C,IAAI,CAACC,KAAK,EAAE;MACV,OAAO,IAAI;IACb;IAEA,OAAOM,MAAM,CAACC,MAAM,CAACP,KAAK,CAAC,CAACQ,MAAM,CAACC,OAAO,CAAC;EAC7C;EAEAC,YAAYA,CAACC,OAAoB,EAAEC,MAA8B,EAAE;IACjE,MAAM;MAAEzB,UAAU;MAAEP,IAAI;MAAEC;IAAQ,CAAC,GAAG,IAAI;IAE1C,MAAMgC,SAAS,GAAG,KAAK,CAACH,YAAY,CAACC,OAAO,EAAEC,MAAM,CAAC;IACrD,IAAI;MAAEE,UAAU;MAAEC,QAAQ;MAAEC,IAAI;MAAEC;IAAM,CAAC,GAAGJ,SAAS;IAErDE,QAAQ,KAAK;MACXG,MAAM,EAAE;QACNC,IAAI,EAAEF,KAAK,CAACE,IAAI;QAEhB;AACR;AACA;AACA;QACQ1B,OAAO,EAAEZ,OAAO,CAACK,SAAS,GACtB,uBAAuB,GACvB;MACN;IACF,CAAC;IAED,IAAI8B,IAAI,EAAE;MACRA,IAAI,CAACI,EAAE,KAAK,GAAGxC,IAAI,OAAO;MAC1BmC,QAAQ,CAACM,UAAU,KAAK;QACtB,kBAAkB,EAAEL,IAAI,CAACI;MAC3B,CAAC;IACH;IAEAN,UAAU,GAAG3B,UAAU,CAACuB,YAAY,CAACC,OAAO,EAAEC,MAAM,CAAC;IAErD,OAAO;MACL,GAAGC,SAAS;MACZE,QAAQ;MACRD;IACF,CAAC;EACH;EAEAb,OAAOA,CAACD,KAAkC,EAA2B;IACnE,OAAOxB,cAAc,CAAC8C,WAAW,CAACtB,KAAK,CAAC;EAC1C;EAEA,OAAOsB,WAAWA,CAChBtB,KAAkC,EACT;IACzB,OACE1B,WAAW,CAAC0B,KAAK,CAAC,IAClBzB,SAAS,CAACgD,MAAM,CAACvB,KAAK,CAACwB,YAAY,CAAC,IACpCjD,SAAS,CAACgD,MAAM,CAACvB,KAAK,CAACyB,IAAI,CAAC,IAC5BlD,SAAS,CAACgD,MAAM,CAACvB,KAAK,CAAC0B,QAAQ,CAAC;EAEpC;AACF","ignoreList":[]}
package/README.md CHANGED
@@ -6,24 +6,9 @@ It is designed to be embedded in the frontend of a digital service and provide a
6
6
 
7
7
  ## Table of Contents
8
8
 
9
+ - [Demo of DXT](#demo-of-dxt)
9
10
  - [Installation](#installation)
10
- - [Dependencies](#dependencies)
11
- - [Setup](#setup)
12
- - [Form Config](#form-config)
13
- - [Static Assets and Styles](#static-assets-and-styles)
14
- - [Example](#example)
15
- - [Environment Variables](#environment-variables)
16
- - [Options](#options)
17
- - [Services](#services)
18
- - [Custom Controllers](#custom-controllers)
19
- - [Custom Filters](#custom-filters)
20
- - [Custom Cache](#custom-cache)
21
- - [Exemplar](#exemplar)
22
- - [Templates](#templates)
23
- - [Template Data](#template-data)
24
- - [Liquid Filters](#liquid-filters)
25
- - [Examples](#examples)
26
- - [Templates and Views: Extending the Default Layout](#templates-and-views-extending-the-default-layout)
11
+ - [Documentation](#documentation)
27
12
  - [Publishing the Package](#publishing-the-package)
28
13
  - [Semantic Versioning Control](#semantic-versioning-control)
29
14
  - [Major-Version Release Branches](#major-version-release-branches)
@@ -31,305 +16,25 @@ It is designed to be embedded in the frontend of a digital service and provide a
31
16
  - [Workflow Triggers](#workflow-triggers)
32
17
  - [Safety and Consistency](#safety-and-consistency)
33
18
 
34
- ## Installation
35
-
36
- `npm install @defra/forms-engine-plugin --save`
37
-
38
- ## Dependencies
39
-
40
- The following are [plugin dependencies](<https://hapi.dev/api/?v=21.4.0#server.dependency()>) that are required to be registered with hapi:
41
-
42
- `npm install hapi-pino @hapi/crumb @hapi/yar @hapi/vision --save`
43
-
44
- - [hapi-pino](https://github.com/hapijs/hapi-pino) - [Pino](https://github.com/pinojs/pino) logger for hapi
45
- - [@hapi/crumb](https://github.com/hapijs/crumb) - CSRF crumb generation and validation
46
- - [@hapi/yar](https://github.com/hapijs/yar) - Session manager
47
- - [@hapi/vision](https://github.com/hapijs/vision) - Template rendering support
48
-
49
- Additional npm dependencies that you will need are:
50
-
51
- `npm install nunjucks govuk-frontend --save`
52
-
53
- - [nunjucks](https://www.npmjs.com/package/nunjucks) - [templating engine](https://mozilla.github.io/nunjucks/) used by GOV.UK design system
54
- - [govuk-frontend](https://www.npmjs.com/package/govuk-frontend) - [code](https://github.com/alphagov/govuk-frontend) you need to build a user interface for government platforms and services
55
-
56
- Optional dependencies
57
-
58
- `npm install @hapi/inert --save`
59
-
60
- - [@hapi/inert](https://www.npmjs.com/package/@hapi/inert) - static file and directory handlers for serving GOV.UK assets and styles
61
-
62
- ## Setup
63
-
64
- ### Form config
65
-
66
- The `form-engine-plugin` uses JSON configuration files to serve form journeys.
67
- These files are called `Form definitions` and are built up of:
68
-
69
- - `pages` - includes a `path`, `title`
70
- - `components` - one or more questions on a page
71
- - `conditions` - used to conditionally show and hide pages and
72
- - `lists` - data used to in selection fields like [Select](https://design-system.service.gov.uk/components/select/), [Checkboxes](https://design-system.service.gov.uk/components/checkboxes/) and [Radios](https://design-system.service.gov.uk/components/radios/)
73
-
74
- The [types](https://github.com/DEFRA/forms-designer/blob/main/model/src/form/form-definition/types.ts), `joi` [schema](https://github.com/DEFRA/forms-designer/blob/main/model/src/form/form-definition/index.ts) and the [examples](test/form/definitions) folder are a good place to learn about the structure of these files.
75
-
76
- TODO - Link to wiki for `Form metadata`
77
- TODO - Link to wiki for `Form definition`
78
-
79
- #### Providing form config to the engine
80
-
81
- The engine plugin registers several [routes](https://hapi.dev/tutorials/routing/?lang=en_US) on the hapi server.
82
-
83
- They look like this:
84
-
85
- ```
86
- GET /{slug}/{path}
87
- POST /{slug}/{path}
88
- ```
89
-
90
- A unique `slug` is used to route the user to the correct form, and the `path` used to identify the correct page within the form to show.
91
- The [plugin registration options](#options) have a `services` setting to provide a `formsService` that is responsible for returning `form definition` data.
92
-
93
- WARNING: This below is subject to change
94
-
95
- A `formsService` has two methods, one for returning `formMetadata` and another to return `formDefinition`s.
96
-
97
- ```
98
- const formsService = {
99
- getFormMetadata: async function (slug) {
100
- // Returns the metadata for the slug
101
- },
102
- getFormDefinition: async function (id, state) {
103
- // Returns the form definition for the given id
104
- }
105
- }
106
- ```
107
-
108
- The reason for the two separate methods is caching.
109
- `formMetadata` is a lightweight record designed to give top level information about a form.
110
- This method is invoked for every page request.
111
-
112
- Only when the `formMetadata` indicates that the definition has changed is a call to `getFormDefinition` is made.
113
- The response from this can be quite big as it contains the entire form definition.
114
-
115
- See [example](#example) below for more detail
116
-
117
- ### Static assets and styles
118
-
119
- TODO
120
-
121
- ## Example
122
-
123
- ```
124
- import hapi from '@hapi/hapi'
125
- import yar from '@hapi/yar'
126
- import crumb from '@hapi/crumb'
127
- import inert from '@hapi/inert'
128
- import pino from 'hapi-pino'
129
- import plugin from '@defra/forms-engine-plugin'
130
-
131
- const server = hapi.server({
132
- port: 3000
133
- })
134
-
135
- // Register the dependent plugins
136
- await server.register(pino)
137
- await server.register(inert)
138
- await server.register(crumb)
139
- await server.register({
140
- plugin: yar,
141
- options: {
142
- cookieOptions: {
143
- password: 'ENTER_YOUR_SESSION_COOKIE_PASSWORD_HERE' // Must be > 32 chars
144
- }
145
- }
146
- })
147
-
148
- // Register the `forms-engine-plugin`
149
- await server.register({
150
- plugin
151
- })
152
-
153
- await server.start()
154
- ```
155
-
156
- ## Environment variables
157
-
158
- ## Options
159
-
160
- The forms plugin is configured with [registration options](https://hapi.dev/api/?v=21.4.0#plugins)
161
-
162
- - `services` (optional) - object containing `formsService`, `formSubmissionService` and `outputService`
163
- - `formsService` - used to load `formMetadata` and `formDefinition`
164
- - `formSubmissionService` - used prepare the form during submission (ignore - subject to change)
165
- - `outputService` - used to save the submission
166
- - `controllers` (optional) - Object map of custom page controllers used to override the default. See [custom controllers](#custom-controllers)
167
- - `filters` (optional) - A map of custom template filters to include
168
- - `cacheName` (optional) - The cache name to use. Defaults to hapi's [default server cache]. Recommended for production. See [here]
169
- (#custom-cache) for more details
170
- - `viewPaths` (optional) - Include additional view paths when using custom `page.view`s
171
- - `pluginPath` (optional) - The location of the plugin (defaults to `node_modules/@defra/forms-engine-plugin`)
172
-
173
- ### Services
174
-
175
- TODO
176
-
177
- ### Custom controllers
178
-
179
- TODO
180
-
181
- ### Custom filters
182
-
183
- Use the `filter` plugin option to provide custom template filters.
184
- Filters are available in both [nunjucks](https://mozilla.github.io/nunjucks/templating.html#filters) and [liquid](https://liquidjs.com/filters/overview.html) templates.
185
-
186
- ```
187
- const formatter = new Intl.NumberFormat('en-GB')
188
-
189
- await server.register({
190
- plugin,
191
- options: {
192
- filters: {
193
- money: value => formatter.format(value),
194
- upper: value => typeof value === 'string' ? value.toUpperCase() : value
195
- }
196
- }
197
- })
198
- ```
199
-
200
- ### Custom cache
201
-
202
- The plugin will use the [default server cache](https://hapi.dev/api/?v=21.4.0#-serveroptionscache) to store form answers on the server.
203
- This is just an in-memory cache which is fine for development.
204
-
205
- In production you should create a custom cache one of the available `@hapi/catbox` adapters.
206
-
207
- E.g. [Redis](https://github.com/hapijs/catbox-redis)
208
-
209
- ```
210
- import { Engine as CatboxRedis } from '@hapi/catbox-redis'
211
-
212
- const server = new Hapi.Server({
213
- cache : [
214
- {
215
- name: 'my_cache',
216
- provider: {
217
- constructor: CatboxRedis,
218
- options: {}
219
- }
220
- }
221
- ]
222
- })
223
- ```
224
-
225
- ## Exemplar
19
+ ## Demo of DXT
226
20
 
227
21
  TODO: Link to CDP exemplar
228
22
 
229
- ## Templates
230
-
231
- The following elements support [LiquidJS templates](https://liquidjs.com/):
232
-
233
- - Page **title**
234
- - Form component **title**
235
- - Support for fieldset legend text or label text
236
- - This includes when the title is used in **error messages**
237
- - Html (guidance) component **content**
238
- - Summary component **row** key title (check answers and repeater summary)
239
-
240
- ### Template Data
241
-
242
- The data the templates are evaluated against is the raw answers the user has provided up to the page they're currently on.
243
- For example, given a YesNoField component called `TKsWbP`, the template `{{ TKsWbP }}` would render "true" or "false" depending on how the user answered the question.
244
-
245
- The current FormContext is also available as `context` in the templates. This allows access to the full data including the path the user has taken in their journey and any miscellaneous data returned from `Page event`s in `context.data`.
246
-
247
- ### Liquid Filters
248
-
249
- There are a number of `LiquidJS` filters available to you from within the templates:
250
-
251
- - `page` - returns the page definition for the given path
252
- - `field` - returns the component definition for the given name
253
- - `href` - returns the page href for the given page path
254
- - `answer` - returns the user's answer for a given component
255
- - `evaluate` - evaluates and returns a Liquid template using the current context
256
-
257
- ### Examples
258
-
259
- ```json
260
- "pages": [
261
- {
262
- "title": "What's your name?",
263
- "path": "/full-name",
264
- "components": [
265
- {
266
- "name": "WmHfSb",
267
- "title": "What's your full name?",
268
- "type": "TextField"
269
- }
270
- ]
271
- },
272
- // This example shows how a component can use an answer to a previous question (What's your full name) in it's title
273
- {
274
- "title": "Are you in England?",
275
- "path": "/are-you-in-england",
276
- "components": [
277
- {
278
- "name": "TKsWbP",
279
- "title": "Are you in England, {{ WmHfSb }}?",
280
- "type": "YesNoField"
281
- }
282
- ]
283
- },
284
- // This example shows how a Html (guidance) component can use the available filters to get the form definition and user answers and display them
285
- {
286
- "title": "Template example for {{ WmHfSb }}?",
287
- "path": "/example",
288
- "components": [
289
- {
290
- "title": "Html",
291
- "type": "Html",
292
- "content": "<p class=\"govuk-body\">
293
- // Use Liquid's `assign` to create a variable that holds reference to the \"/are-you-in-england\" page
294
- {%- assign inEngland = \"/are-you-in-england\" | page -%}
295
-
296
- // Use the reference to `evaluate` the title
297
- {{ inEngland.title | evaluate }}<br>
298
-
299
- // Use the href filter to display the full page path
300
- {{ \"/are-you-in-england\" | href }}<br>
301
-
302
- // Use the `answer` filter to render the user provided answer to a question
303
- {{ 'TKsWbP' | answer }}
304
- </p>\n"
305
- }
306
- ]
307
- }
308
- ]
309
- ```
310
-
311
- ## Templates and views
312
-
313
- ### Extending the default layout
314
-
315
- TODO
316
-
317
- To override the default page template, vision and nunjucks both need to be configured to search in the `forms-engine-plugin` views directory when looking for template files.
23
+ ## Installation
318
24
 
319
- For vision this is done through the `path` [plugin option](https://github.com/hapijs/vision/blob/master/API.md#options)
320
- For nunjucks it is configured through the environment [configure options](https://mozilla.github.io/nunjucks/api.html#configure).
25
+ [See our getting started developer guide](./docs/GETTING_STARTED.md).
321
26
 
322
- The `forms-engine-plugin` path to add can be imported from:
27
+ ## Documentation
323
28
 
324
- `import { VIEW_PATH } from '@defra/forms-engine-plugin'`
29
+ DXT has a mix of configuration-driven and code-based features that developers can utilise.
325
30
 
326
- Which can then be appended to the `node_modules` path `node_modules/@defra/forms-engine`.
31
+ [See our documentation folder](./docs/INDEX.md) to learn more about the features of DXT.
327
32
 
328
- The main template layout is `govuk-frontend`'s `template.njk` file, this also needs to be added to the `path`s that nunjucks can look in.
33
+ ## Contributing
329
34
 
330
- ### Custom page view
35
+ [See our contribution guide](./docs/CONTRIBUTING.md).
331
36
 
332
- ## Publishing the Package
37
+ ## Publishing the package
333
38
 
334
39
  Our GitHub Actions workflow (`publish.yml`) is set up to make publishing a breeze, using semantic versioning and a variety of release strategies. Here's how you can make the most of it:
335
40
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/forms-engine-plugin",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Defra forms engine",
5
5
  "type": "module",
6
6
  "files": [
@@ -90,13 +90,13 @@ export const config = convict({
90
90
  doc: 'The service version, this variable is injected into your docker container in CDP environments',
91
91
  format: String,
92
92
  nullable: true,
93
- default: null,
93
+ default: '',
94
94
  env: 'SERVICE_VERSION'
95
95
  } as SchemaObj<string>,
96
96
  feedbackLink: {
97
97
  doc: 'Used in your phase banner. Can be a URL or more commonly mailto mailto:feedback@department.gov.uk',
98
98
  format: String,
99
- default: null,
99
+ default: '',
100
100
  env: 'FEEDBACK_LINK'
101
101
  } as SchemaObj<string>,
102
102
  phaseTag: {
@@ -121,7 +121,7 @@ export const config = convict({
121
121
  },
122
122
  sessionCookiePassword: {
123
123
  format: String,
124
- default: null,
124
+ default: '',
125
125
  sensitive: true,
126
126
  env: 'SESSION_COOKIE_PASSWORD'
127
127
  } as SchemaObj<string>,
@@ -129,26 +129,26 @@ export const config = convict({
129
129
  host: {
130
130
  doc: 'Redis cache host',
131
131
  format: String,
132
- default: null,
132
+ default: '',
133
133
  env: 'REDIS_HOST'
134
134
  } as SchemaObj<string>,
135
135
  username: {
136
136
  doc: 'Redis cache username',
137
137
  format: String,
138
- default: null,
138
+ default: '',
139
139
  env: 'REDIS_USERNAME'
140
140
  } as SchemaObj<string>,
141
141
  password: {
142
142
  doc: 'Redis cache password',
143
143
  format: '*',
144
- default: null,
144
+ default: '',
145
145
  sensitive: true,
146
146
  env: 'REDIS_PASSWORD'
147
147
  } as SchemaObj<string>,
148
148
  keyPrefix: {
149
149
  doc: 'Redis cache key prefix name used to isolate the cached results across multiple clients',
150
150
  format: String,
151
- default: null,
151
+ default: '',
152
152
  env: 'REDIS_KEY_PREFIX'
153
153
  } as SchemaObj<string>
154
154
  },
@@ -167,12 +167,12 @@ export const config = convict({
167
167
  */
168
168
  notifyTemplateId: {
169
169
  format: String,
170
- default: null,
170
+ default: '',
171
171
  env: 'NOTIFY_TEMPLATE_ID'
172
172
  } as SchemaObj<string>,
173
173
  notifyAPIKey: {
174
174
  format: String,
175
- default: null,
175
+ default: '',
176
176
  env: 'NOTIFY_API_KEY'
177
177
  } as SchemaObj<string>,
178
178
 
@@ -181,25 +181,25 @@ export const config = convict({
181
181
  */
182
182
  managerUrl: {
183
183
  format: String,
184
- default: null,
184
+ default: 'http://localhost:3001',
185
185
  env: 'MANAGER_URL'
186
186
  } as SchemaObj<string>,
187
187
 
188
188
  designerUrl: {
189
189
  format: String,
190
- default: null,
190
+ default: 'http://localhost:3000',
191
191
  env: 'DESIGNER_URL'
192
192
  } as SchemaObj<string>,
193
193
 
194
194
  submissionUrl: {
195
195
  format: String,
196
- default: null,
196
+ default: 'http://localhost:3002',
197
197
  env: 'SUBMISSION_URL'
198
198
  } as SchemaObj<string>,
199
199
 
200
200
  uploaderUrl: {
201
201
  format: String,
202
- default: null,
202
+ default: 'http://localhost:7337',
203
203
  env: 'UPLOADER_URL'
204
204
  } as SchemaObj<string>,
205
205
 
@@ -65,6 +65,13 @@ describe('UkAddressField', () => {
65
65
  })
66
66
  )
67
67
 
68
+ expect(keys).toHaveProperty(
69
+ 'myComponent__county',
70
+ expect.objectContaining({
71
+ flags: expect.objectContaining({ label: 'County' })
72
+ })
73
+ )
74
+
68
75
  expect(keys).toHaveProperty(
69
76
  `myComponent__postcode`,
70
77
  expect.objectContaining({
@@ -82,6 +89,7 @@ describe('UkAddressField', () => {
82
89
  'myComponent__addressLine1',
83
90
  'myComponent__addressLine2',
84
91
  'myComponent__town',
92
+ 'myComponent__county',
85
93
  'myComponent__postcode'
86
94
  ])
87
95
 
@@ -118,6 +126,14 @@ describe('UkAddressField', () => {
118
126
  })
119
127
  )
120
128
 
129
+ expect(keys).toHaveProperty(
130
+ 'myComponent__county',
131
+ expect.objectContaining({
132
+ allow: [''], // Required but empty string is allowed
133
+ flags: expect.objectContaining({ presence: 'required' })
134
+ })
135
+ )
136
+
121
137
  expect(keys).toHaveProperty(
122
138
  `myComponent__postcode`,
123
139
  expect.objectContaining({
@@ -157,6 +173,11 @@ describe('UkAddressField', () => {
157
173
  expect.objectContaining({ allow: [''] })
158
174
  )
159
175
 
176
+ expect(keys).toHaveProperty(
177
+ 'myComponent__county',
178
+ expect.objectContaining({ allow: [''] })
179
+ )
180
+
160
181
  expect(keys).toHaveProperty(
161
182
  `myComponent__postcode`,
162
183
  expect.objectContaining({ allow: [''] })
@@ -167,6 +188,7 @@ describe('UkAddressField', () => {
167
188
  addressLine1: '',
168
189
  addressLine2: '',
169
190
  town: '',
191
+ county: '',
170
192
  postcode: ''
171
193
  })
172
194
  )
@@ -180,6 +202,7 @@ describe('UkAddressField', () => {
180
202
  addressLine1: 'Richard Fairclough House',
181
203
  addressLine2: 'Knutsford Road',
182
204
  town: 'Warrington',
205
+ county: 'Cheshire',
183
206
  postcode: 'WA4 1HT'
184
207
  })
185
208
  )
@@ -189,6 +212,7 @@ describe('UkAddressField', () => {
189
212
  addressLine1: 'Richard Fairclough House',
190
213
  addressLine2: '', // Optional field
191
214
  town: 'Warrington',
215
+ county: '', // Optional field
192
216
  postcode: 'WA4 1HT'
193
217
  })
194
218
  )
@@ -203,6 +227,7 @@ describe('UkAddressField', () => {
203
227
  addressLine1: '',
204
228
  addressLine2: '',
205
229
  town: '',
230
+ county: '',
206
231
  postcode: ''
207
232
  })
208
233
  )
@@ -228,6 +253,7 @@ describe('UkAddressField', () => {
228
253
  addressLine1: ['invalid'],
229
254
  addressLine2: ['invalid'],
230
255
  town: ['invalid'],
256
+ county: ['invalid'],
231
257
  postcode: ['invalid']
232
258
  })
233
259
  )
@@ -237,6 +263,7 @@ describe('UkAddressField', () => {
237
263
  addressLine1: 'invalid',
238
264
  addressLine2: 'invalid',
239
265
  town: 'invalid',
266
+ county: 'invalid',
240
267
  postcode: 'invalid'
241
268
  })
242
269
  )
@@ -252,6 +279,7 @@ describe('UkAddressField', () => {
252
279
  addressLine1: 'Richard Fairclough House',
253
280
  addressLine2: 'Knutsford Road',
254
281
  town: 'Warrington',
282
+ county: 'Cheshire',
255
283
  postcode: 'WA4 1HT'
256
284
  }
257
285
 
@@ -263,7 +291,7 @@ describe('UkAddressField', () => {
263
291
  const answer2 = getAnswer(field, state2)
264
292
 
265
293
  expect(answer1).toBe(
266
- 'Richard Fairclough House<br>Knutsford Road<br>Warrington<br>WA4 1HT<br>'
294
+ 'Richard Fairclough House<br>Knutsford Road<br>Warrington<br>Cheshire<br>WA4 1HT<br>'
267
295
  )
268
296
 
269
297
  expect(answer2).toBe('')
@@ -302,6 +330,7 @@ describe('UkAddressField', () => {
302
330
  'Richard Fairclough House',
303
331
  'Knutsford Road',
304
332
  'Warrington',
333
+ 'Cheshire',
305
334
  'WA4 1HT'
306
335
  ])
307
336
 
@@ -325,6 +354,7 @@ describe('UkAddressField', () => {
325
354
  addressLine1: 'Richard Fairclough House',
326
355
  addressLine2: 'Knutsford Road',
327
356
  town: 'Warrington',
357
+ county: 'Cheshire',
328
358
  postcode: 'WA4 1HT'
329
359
  }
330
360
 
@@ -363,6 +393,14 @@ describe('UkAddressField', () => {
363
393
  })
364
394
  }),
365
395
 
396
+ expect.objectContaining({
397
+ model: getViewModel(address, 'county', {
398
+ label: { text: 'County (optional)' },
399
+ attributes: { autocomplete: 'county' },
400
+ value: address.county
401
+ })
402
+ }),
403
+
366
404
  expect.objectContaining({
367
405
  model: getViewModel(address, 'postcode', {
368
406
  label: { text: 'Postcode' },
@@ -395,6 +433,7 @@ describe('UkAddressField', () => {
395
433
  addressLine1: 'Richard Fairclough House',
396
434
  addressLine2: 'Knutsford Road',
397
435
  town: 'Warrington',
436
+ county: 'Cheshire',
398
437
  postcode: 'WA4 1HT'
399
438
  }
400
439
 
@@ -407,6 +446,9 @@ describe('UkAddressField', () => {
407
446
  const townInvalid =
408
447
  'Town 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
409
448
 
449
+ const countyInvalid =
450
+ 'County 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
451
+
410
452
  const postcodeInvalid = '111 XX2'
411
453
 
412
454
  describe.each([
@@ -424,6 +466,7 @@ describe('UkAddressField', () => {
424
466
  addressLine1: ' Richard Fairclough House',
425
467
  addressLine2: ' Knutsford Road',
426
468
  town: ' Warrington',
469
+ county: 'Cheshire',
427
470
  postcode: ' WA4 1HT'
428
471
  }),
429
472
  output: {
@@ -435,6 +478,7 @@ describe('UkAddressField', () => {
435
478
  addressLine1: 'Richard Fairclough House ',
436
479
  addressLine2: 'Knutsford Road ',
437
480
  town: 'Warrington ',
481
+ county: 'Cheshire ',
438
482
  postcode: 'WA4 1HT '
439
483
  }),
440
484
  output: {
@@ -446,6 +490,7 @@ describe('UkAddressField', () => {
446
490
  addressLine1: ' Richard Fairclough House \n\n',
447
491
  addressLine2: ' Knutsford Road \n\n',
448
492
  town: ' Warrington \n\n',
493
+ county: ' Cheshire \n\n',
449
494
  postcode: ' WA4 1HT \n\n'
450
495
  }),
451
496
  output: {
@@ -468,6 +513,7 @@ describe('UkAddressField', () => {
468
513
  addressLine1: addressLine1Invalid,
469
514
  addressLine2: 'Knutsford Road',
470
515
  town: 'Warrington',
516
+ county: 'Cheshire',
471
517
  postcode: 'WA4 1HT'
472
518
  }),
473
519
  output: {
@@ -475,6 +521,7 @@ describe('UkAddressField', () => {
475
521
  addressLine1: addressLine1Invalid,
476
522
  addressLine2: 'Knutsford Road',
477
523
  town: 'Warrington',
524
+ county: 'Cheshire',
478
525
  postcode: 'WA4 1HT'
479
526
  }),
480
527
  errors: [
@@ -489,6 +536,7 @@ describe('UkAddressField', () => {
489
536
  addressLine1: 'Richard Fairclough House',
490
537
  addressLine2: addressLine2Invalid,
491
538
  town: 'Warrington',
539
+ county: 'Cheshire',
492
540
  postcode: 'WA4 1HT'
493
541
  }),
494
542
  output: {
@@ -496,6 +544,7 @@ describe('UkAddressField', () => {
496
544
  addressLine1: 'Richard Fairclough House',
497
545
  addressLine2: addressLine2Invalid,
498
546
  town: 'Warrington',
547
+ county: 'Cheshire',
499
548
  postcode: 'WA4 1HT'
500
549
  }),
501
550
  errors: [
@@ -510,6 +559,7 @@ describe('UkAddressField', () => {
510
559
  addressLine1: 'Richard Fairclough House',
511
560
  addressLine2: 'Knutsford Road',
512
561
  town: townInvalid,
562
+ county: 'Cheshire',
513
563
  postcode: 'WA4 1HT'
514
564
  }),
515
565
  output: {
@@ -517,6 +567,7 @@ describe('UkAddressField', () => {
517
567
  addressLine1: 'Richard Fairclough House',
518
568
  addressLine2: 'Knutsford Road',
519
569
  town: townInvalid,
570
+ county: 'Cheshire',
520
571
  postcode: 'WA4 1HT'
521
572
  }),
522
573
  errors: [
@@ -531,6 +582,30 @@ describe('UkAddressField', () => {
531
582
  addressLine1: 'Richard Fairclough House',
532
583
  addressLine2: 'Knutsford Road',
533
584
  town: 'Warrington',
585
+ county: countyInvalid,
586
+ postcode: 'WA4 1HT'
587
+ }),
588
+ output: {
589
+ value: getFormData({
590
+ addressLine1: 'Richard Fairclough House',
591
+ addressLine2: 'Knutsford Road',
592
+ town: 'Warrington',
593
+ county: countyInvalid,
594
+ postcode: 'WA4 1HT'
595
+ }),
596
+ errors: [
597
+ expect.objectContaining({
598
+ text: 'County must be 100 characters or less'
599
+ })
600
+ ]
601
+ }
602
+ },
603
+ {
604
+ input: getFormData({
605
+ addressLine1: 'Richard Fairclough House',
606
+ addressLine2: 'Knutsford Road',
607
+ town: 'Warrington',
608
+ county: 'Cheshire',
534
609
  postcode: postcodeInvalid
535
610
  }),
536
611
  output: {
@@ -538,6 +613,7 @@ describe('UkAddressField', () => {
538
613
  addressLine1: 'Richard Fairclough House',
539
614
  addressLine2: 'Knutsford Road',
540
615
  town: 'Warrington',
616
+ county: 'Cheshire',
541
617
  postcode: postcodeInvalid
542
618
  }),
543
619
  errors: [
@@ -602,6 +678,7 @@ function getFormData(address: FormPayload): FormPayload {
602
678
  myComponent__addressLine1: address.addressLine1,
603
679
  myComponent__addressLine2: address.addressLine2,
604
680
  myComponent__town: address.town,
681
+ myComponent__county: address.county,
605
682
  myComponent__postcode: address.postcode
606
683
  }
607
684
  }
@@ -610,7 +687,7 @@ function getFormData(address: FormPayload): FormPayload {
610
687
  * UK address session state
611
688
  */
612
689
  function getFormState(address: FormPayload): FormState {
613
- const [addressLine1, addressLine2, town, postcode] = Object.values(
690
+ const [addressLine1, addressLine2, town, county, postcode] = Object.values(
614
691
  getFormData(address)
615
692
  )
616
693
 
@@ -618,6 +695,7 @@ function getFormState(address: FormPayload): FormState {
618
695
  myComponent__addressLine1: addressLine1 ?? null,
619
696
  myComponent__addressLine2: addressLine2 ?? null,
620
697
  myComponent__town: town ?? null,
698
+ myComponent__county: county ?? null,
621
699
  myComponent__postcode: postcode ?? null
622
700
  }
623
701
  }
@@ -70,6 +70,17 @@ export class UkAddressField extends FormComponent {
70
70
  optionalText: !isRequired && (hideOptional || !hideTitle)
71
71
  }
72
72
  },
73
+ {
74
+ type: ComponentType.TextField,
75
+ name: `${name}__county`,
76
+ title: 'County',
77
+ schema: { max: 100 },
78
+ options: {
79
+ autocomplete: 'county',
80
+ required: false,
81
+ optionalText: !isRequired && (hideOptional || !hideTitle)
82
+ }
83
+ },
73
84
  {
74
85
  type: ComponentType.TextField,
75
86
  name: `${name}__postcode`,
@@ -168,5 +179,6 @@ export interface UkAddressState extends Record<string, string> {
168
179
  addressLine1: string
169
180
  addressLine2: string
170
181
  town: string
182
+ county: string
171
183
  postcode: string
172
184
  }
@@ -132,7 +132,7 @@
132
132
  {% set feedbackLink = feedbackLink or config.feedbackLink -%}
133
133
  {% set phaseTag = phaseTag or config.phaseTag -%}
134
134
 
135
- {% if phaseTag %}
135
+ {% if phaseTag and feedbackLink %}
136
136
  {% set feedbackLinkHtml -%}
137
137
  <a href="{{ currentPath if context.isForceAccess else feedbackLink }}" class="govuk-link" {%- if not context.isForceAccess %} target="_blank" rel="noopener noreferrer" {%- endif %}>
138
138
  {%- if "mailto:" in feedbackLink -%}