@defra/forms-engine-plugin 4.0.11 → 4.0.12

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.
Files changed (30) hide show
  1. package/.server/server/forms/register-as-a-unicorn-breeder.yaml +2 -2
  2. package/.server/server/plugins/engine/components/EastingNorthingField.js +4 -24
  3. package/.server/server/plugins/engine/components/EastingNorthingField.js.map +1 -1
  4. package/.server/server/plugins/engine/components/LatLongField.js +8 -31
  5. package/.server/server/plugins/engine/components/LatLongField.js.map +1 -1
  6. package/.server/server/plugins/engine/components/LocationFieldBase.d.ts +1 -3
  7. package/.server/server/plugins/engine/components/LocationFieldBase.js +1 -8
  8. package/.server/server/plugins/engine/components/LocationFieldBase.js.map +1 -1
  9. package/.server/server/plugins/engine/components/NationalGridFieldNumberField.d.ts +0 -2
  10. package/.server/server/plugins/engine/components/NationalGridFieldNumberField.js +7 -18
  11. package/.server/server/plugins/engine/components/NationalGridFieldNumberField.js.map +1 -1
  12. package/.server/server/plugins/engine/components/NumberField.d.ts +0 -18
  13. package/.server/server/plugins/engine/components/NumberField.js +2 -103
  14. package/.server/server/plugins/engine/components/NumberField.js.map +1 -1
  15. package/.server/server/plugins/engine/components/OsGridRefField.d.ts +0 -2
  16. package/.server/server/plugins/engine/components/OsGridRefField.js +4 -32
  17. package/.server/server/plugins/engine/components/OsGridRefField.js.map +1 -1
  18. package/package.json +1 -1
  19. package/src/server/forms/register-as-a-unicorn-breeder.yaml +2 -2
  20. package/src/server/plugins/engine/components/EastingNorthingField.test.ts +0 -14
  21. package/src/server/plugins/engine/components/EastingNorthingField.ts +4 -24
  22. package/src/server/plugins/engine/components/LatLongField.test.ts +4 -24
  23. package/src/server/plugins/engine/components/LatLongField.ts +8 -33
  24. package/src/server/plugins/engine/components/LocationFieldBase.ts +1 -15
  25. package/src/server/plugins/engine/components/NationalGridFieldNumberField.test.ts +22 -8
  26. package/src/server/plugins/engine/components/NationalGridFieldNumberField.ts +9 -20
  27. package/src/server/plugins/engine/components/NumberField.test.ts +12 -504
  28. package/src/server/plugins/engine/components/NumberField.ts +4 -133
  29. package/src/server/plugins/engine/components/OsGridRefField.test.ts +11 -33
  30. package/src/server/plugins/engine/components/OsGridRefField.ts +5 -38
@@ -1 +1 @@
1
- {"version":3,"file":"NumberField.js","names":["joi","FormComponent","isFormValue","messageTemplate","NumberField","constructor","def","props","options","schema","formSchema","number","custom","getValidatorPrecision","label","required","allow","messages","customValidationMessages","empty","min","max","precision","integer","customValidationMessage","message","default","stateSchema","getFormValueFromState","state","name","getFormValue","value","isValue","undefined","getViewModel","payload","errors","viewModel","attributes","prefix","suffix","inputmode","text","isNumber","getAllPossibleErrors","baseErrors","type","template","numberInteger","advancedSettingsErrors","numberMin","numberMax","numberPrecision","validateStringLength","minLength","maxLength","isValid","valueStr","String","length","error","validateMinimumPrecision","minPrecision","Number","isInteger","decimalIndex","indexOf","decimalPlaces","handleLengthValidationError","lengthCheck","helpers","errorType","contextData","context","component","validator","limit","validationSchema","prefs","convert","attempt"],"sources":["../../../../../src/server/plugins/engine/components/NumberField.ts"],"sourcesContent":["import { type NumberFieldComponent } from '@defra/forms-model'\nimport joi, {\n type CustomHelpers,\n type CustomValidator,\n type NumberSchema\n} from 'joi'\n\nimport {\n FormComponent,\n isFormValue\n} from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'\nimport {\n type ErrorMessageTemplateList,\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 NumberField extends FormComponent {\n declare options: NumberFieldComponent['options']\n declare schema: NumberFieldComponent['schema']\n declare formSchema: NumberSchema\n declare stateSchema: NumberSchema\n\n constructor(\n def: NumberFieldComponent,\n props: ConstructorParameters<typeof FormComponent>[1]\n ) {\n super(def, props)\n\n const { options, schema } = def\n\n let formSchema = joi\n .number()\n .custom(getValidatorPrecision(this))\n .label(this.label)\n .required()\n\n if (options.required === false) {\n formSchema = formSchema.allow('')\n } else {\n const messages = options.customValidationMessages\n\n formSchema = formSchema.empty('').messages({\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n 'any.required':\n messages?.['any.required'] ?? (messageTemplate.required as string)\n })\n }\n\n if (typeof schema.min === 'number') {\n formSchema = formSchema.min(schema.min)\n }\n\n if (typeof schema.max === 'number') {\n formSchema = formSchema.max(schema.max)\n }\n\n if (typeof schema.precision === 'number' && schema.precision <= 0) {\n formSchema = formSchema.integer()\n }\n\n if (options.customValidationMessage) {\n const message = options.customValidationMessage\n\n formSchema = formSchema.messages({\n 'any.required': message,\n 'number.base': message,\n 'number.precision': message,\n 'number.integer': message,\n 'number.min': message,\n 'number.max': message\n })\n } else if (options.customValidationMessages) {\n formSchema = formSchema.messages(options.customValidationMessages)\n }\n\n this.formSchema = formSchema.default('')\n this.stateSchema = formSchema.default(null).allow(null)\n this.options = options\n this.schema = schema\n }\n\n getFormValueFromState(state: FormSubmissionState) {\n const { name } = this\n return this.getFormValue(state[name])\n }\n\n getFormValue(value?: FormStateValue | FormState) {\n return this.isValue(value) ? value : undefined\n }\n\n getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) {\n const { options, schema } = this\n\n const viewModel = super.getViewModel(payload, errors)\n let { attributes, prefix, suffix, value } = viewModel\n\n if (typeof schema.precision === 'undefined' || schema.precision <= 0) {\n // If precision isn't provided or provided and\n // less than or equal to 0, use numeric inputmode\n attributes.inputmode = 'numeric'\n }\n\n if (options.prefix) {\n prefix = {\n text: options.prefix\n }\n }\n\n if (options.suffix) {\n suffix = {\n text: options.suffix\n }\n }\n\n // Allow any `toString()`-able value so non-numeric\n // values are shown alongside their error messages\n if (!isFormValue(value)) {\n value = undefined\n }\n\n return {\n ...viewModel,\n attributes,\n prefix,\n suffix,\n value\n }\n }\n\n isValue(value?: FormStateValue | FormState) {\n return NumberField.isNumber(value)\n }\n\n /**\n * For error preview page that shows all possible errors on a component\n */\n getAllPossibleErrors(): ErrorMessageTemplateList {\n return NumberField.getAllPossibleErrors()\n }\n\n /**\n * Static version of getAllPossibleErrors that doesn't require a component instance.\n */\n static getAllPossibleErrors(): ErrorMessageTemplateList {\n return {\n baseErrors: [\n { type: 'required', template: messageTemplate.required },\n { type: 'numberInteger', template: messageTemplate.numberInteger }\n ],\n advancedSettingsErrors: [\n { type: 'numberMin', template: messageTemplate.numberMin },\n { type: 'numberMax', template: messageTemplate.numberMax },\n { type: 'numberPrecision', template: messageTemplate.numberPrecision }\n ]\n }\n }\n\n static isNumber(value?: FormStateValue | FormState): value is number {\n return typeof value === 'number'\n }\n}\n\n/**\n * Validates string length of a numeric value\n * @param value - The numeric value to validate\n * @param minLength - Minimum required string length\n * @param maxLength - Maximum allowed string length\n * @returns Object with validation result\n */\nexport function validateStringLength(\n value: number,\n minLength?: number,\n maxLength?: number\n): { isValid: boolean; error?: 'minLength' | 'maxLength' } {\n if (typeof minLength !== 'number' && typeof maxLength !== 'number') {\n return { isValid: true }\n }\n\n const valueStr = String(value)\n\n if (typeof minLength === 'number' && valueStr.length < minLength) {\n return { isValid: false, error: 'minLength' }\n }\n\n if (typeof maxLength === 'number' && valueStr.length > maxLength) {\n return { isValid: false, error: 'maxLength' }\n }\n\n return { isValid: true }\n}\n\n/**\n * Validates minimum decimal precision\n * @param value - The numeric value to validate\n * @param minPrecision - Minimum required decimal places\n * @returns true if valid, false if invalid\n */\nexport function validateMinimumPrecision(\n value: number,\n minPrecision: number\n): boolean {\n if (Number.isInteger(value)) {\n return false\n }\n\n const valueStr = String(value)\n const decimalIndex = valueStr.indexOf('.')\n\n if (decimalIndex !== -1) {\n const decimalPlaces = valueStr.length - decimalIndex - 1\n return decimalPlaces >= minPrecision\n }\n\n return false\n}\n\n/**\n * Helper function to handle length validation errors\n * Returns the appropriate error response based on the validation result\n */\nfunction handleLengthValidationError(\n lengthCheck: ReturnType<typeof validateStringLength>,\n helpers: CustomHelpers,\n custom: string | undefined,\n minLength: number | undefined,\n maxLength: number | undefined\n) {\n if (!lengthCheck.isValid && lengthCheck.error) {\n const errorType = `number.${lengthCheck.error}`\n\n if (custom) {\n // Only pass the relevant length value in context\n const contextData =\n lengthCheck.error === 'minLength'\n ? { minLength: minLength ?? 0 }\n : { maxLength: maxLength ?? 0 }\n return helpers.message({ custom }, contextData)\n }\n\n const context =\n lengthCheck.error === 'minLength'\n ? { minLength: minLength ?? 0 }\n : { maxLength: maxLength ?? 0 }\n return helpers.error(errorType, context)\n }\n return null\n}\n\nexport function getValidatorPrecision(component: NumberField) {\n const validator: CustomValidator = (value: number, helpers) => {\n const { options, schema } = component\n const { customValidationMessage: custom } = options\n const {\n precision: limit,\n minPrecision,\n minLength,\n maxLength\n } = schema as {\n precision?: number\n minPrecision?: number\n minLength?: number\n maxLength?: number\n }\n\n if (!limit || limit <= 0) {\n const lengthCheck = validateStringLength(value, minLength, maxLength)\n const error = handleLengthValidationError(\n lengthCheck,\n helpers,\n custom,\n minLength,\n maxLength\n )\n if (error) return error\n return value\n }\n\n // Validate precision (max decimal places)\n const validationSchema = joi\n .number()\n .precision(limit)\n .prefs({ convert: false })\n\n try {\n joi.attempt(value, validationSchema)\n } catch {\n return custom\n ? helpers.message({ custom }, { limit })\n : helpers.error('number.precision', { limit })\n }\n\n // Validate minimum precision (min decimal places)\n if (typeof minPrecision === 'number' && minPrecision > 0) {\n if (!validateMinimumPrecision(value, minPrecision)) {\n return helpers.error('number.minPrecision', { minPrecision })\n }\n }\n\n // Check string length validation after precision checks\n const lengthCheck = validateStringLength(value, minLength, maxLength)\n const error = handleLengthValidationError(\n lengthCheck,\n helpers,\n custom,\n minLength,\n maxLength\n )\n if (error) return error\n\n return value\n }\n\n return validator\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"NumberField.js","names":["joi","FormComponent","isFormValue","messageTemplate","NumberField","constructor","def","props","options","schema","formSchema","number","custom","getValidatorPrecision","label","required","allow","messages","customValidationMessages","empty","min","max","precision","integer","customValidationMessage","message","default","stateSchema","getFormValueFromState","state","name","getFormValue","value","isValue","undefined","getViewModel","payload","errors","viewModel","attributes","prefix","suffix","inputmode","text","isNumber","getAllPossibleErrors","baseErrors","type","template","numberInteger","advancedSettingsErrors","numberMin","numberMax","numberPrecision","component","validator","helpers","limit","validationSchema","prefs","convert","attempt","error"],"sources":["../../../../../src/server/plugins/engine/components/NumberField.ts"],"sourcesContent":["import { type NumberFieldComponent } from '@defra/forms-model'\nimport joi, { type CustomValidator, type NumberSchema } from 'joi'\n\nimport {\n FormComponent,\n isFormValue\n} from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'\nimport {\n type ErrorMessageTemplateList,\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 NumberField extends FormComponent {\n declare options: NumberFieldComponent['options']\n declare schema: NumberFieldComponent['schema']\n declare formSchema: NumberSchema\n declare stateSchema: NumberSchema\n\n constructor(\n def: NumberFieldComponent,\n props: ConstructorParameters<typeof FormComponent>[1]\n ) {\n super(def, props)\n\n const { options, schema } = def\n\n let formSchema = joi\n .number()\n .custom(getValidatorPrecision(this))\n .label(this.label)\n .required()\n\n if (options.required === false) {\n formSchema = formSchema.allow('')\n } else {\n const messages = options.customValidationMessages\n\n formSchema = formSchema.empty('').messages({\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n 'any.required':\n messages?.['any.required'] ?? (messageTemplate.required as string)\n })\n }\n\n if (typeof schema.min === 'number') {\n formSchema = formSchema.min(schema.min)\n }\n\n if (typeof schema.max === 'number') {\n formSchema = formSchema.max(schema.max)\n }\n\n if (typeof schema.precision === 'number' && schema.precision <= 0) {\n formSchema = formSchema.integer()\n }\n\n if (options.customValidationMessage) {\n const message = options.customValidationMessage\n\n formSchema = formSchema.messages({\n 'any.required': message,\n 'number.base': message,\n 'number.precision': message,\n 'number.integer': message,\n 'number.min': message,\n 'number.max': message\n })\n } else if (options.customValidationMessages) {\n formSchema = formSchema.messages(options.customValidationMessages)\n }\n\n this.formSchema = formSchema.default('')\n this.stateSchema = formSchema.default(null).allow(null)\n this.options = options\n this.schema = schema\n }\n\n getFormValueFromState(state: FormSubmissionState) {\n const { name } = this\n return this.getFormValue(state[name])\n }\n\n getFormValue(value?: FormStateValue | FormState) {\n return this.isValue(value) ? value : undefined\n }\n\n getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) {\n const { options, schema } = this\n\n const viewModel = super.getViewModel(payload, errors)\n let { attributes, prefix, suffix, value } = viewModel\n\n if (typeof schema.precision === 'undefined' || schema.precision <= 0) {\n // If precision isn't provided or provided and\n // less than or equal to 0, use numeric inputmode\n attributes.inputmode = 'numeric'\n }\n\n if (options.prefix) {\n prefix = {\n text: options.prefix\n }\n }\n\n if (options.suffix) {\n suffix = {\n text: options.suffix\n }\n }\n\n // Allow any `toString()`-able value so non-numeric\n // values are shown alongside their error messages\n if (!isFormValue(value)) {\n value = undefined\n }\n\n return {\n ...viewModel,\n attributes,\n prefix,\n suffix,\n value\n }\n }\n\n isValue(value?: FormStateValue | FormState) {\n return NumberField.isNumber(value)\n }\n\n /**\n * For error preview page that shows all possible errors on a component\n */\n getAllPossibleErrors(): ErrorMessageTemplateList {\n return NumberField.getAllPossibleErrors()\n }\n\n /**\n * Static version of getAllPossibleErrors that doesn't require a component instance.\n */\n static getAllPossibleErrors(): ErrorMessageTemplateList {\n return {\n baseErrors: [\n { type: 'required', template: messageTemplate.required },\n { type: 'numberInteger', template: messageTemplate.numberInteger }\n ],\n advancedSettingsErrors: [\n { type: 'numberMin', template: messageTemplate.numberMin },\n { type: 'numberMax', template: messageTemplate.numberMax },\n { type: 'numberPrecision', template: messageTemplate.numberPrecision }\n ]\n }\n }\n\n static isNumber(value?: FormStateValue | FormState): value is number {\n return typeof value === 'number'\n }\n}\n\nexport function getValidatorPrecision(component: NumberField) {\n const validator: CustomValidator = (value: number, helpers) => {\n const { options, schema } = component\n\n const { customValidationMessage: custom } = options\n const { precision: limit } = schema\n\n if (!limit || limit <= 0) {\n return value\n }\n\n const validationSchema = joi\n .number()\n .precision(limit)\n .prefs({ convert: false })\n\n try {\n return joi.attempt(value, validationSchema)\n } catch {\n return custom\n ? helpers.message({ custom }, { limit })\n : helpers.error('number.precision', { limit })\n }\n }\n\n return validator\n}\n"],"mappings":"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","ignoreList":[]}
@@ -1,12 +1,10 @@
1
1
  import { type OsGridRefFieldComponent } from '@defra/forms-model';
2
- import type joi from 'joi';
3
2
  import { LocationFieldBase } from '~/src/server/plugins/engine/components/LocationFieldBase.js';
4
3
  export declare class OsGridRefField extends LocationFieldBase {
5
4
  options: OsGridRefFieldComponent['options'];
6
5
  protected getValidationConfig(): {
7
6
  pattern: RegExp;
8
7
  patternErrorMessage: string;
9
- customValidation: (value: string, helpers: joi.CustomHelpers) => string | joi.ErrorReport;
10
8
  };
11
9
  protected getErrorTemplates(): {
12
10
  type: string;
@@ -3,39 +3,11 @@ export class OsGridRefField extends LocationFieldBase {
3
3
  getValidationConfig() {
4
4
  // Regex for OS grid references and parcel IDs
5
5
  // Validates specific valid OS grid letter combinations with:
6
- // - 6 digits (e.g., SD865005 or SD 865 005)
7
- // - 8 digits in 2 blocks of 4 (parcel ID) e.g., ST 6789 6789
8
- // - 10 digits in 2 blocks of 5 (OS grid reference) e.g., SO 12345 12345
9
- const osGridPattern = /^(?:[sn][a-hj-z]|[to][abfglmqrvw]|h[l-z]|j[lmqrvw])\s?(?:\d{3}\s?\d{3}|\d{4}\s?\d{4}|\d{5}\s?\d{5})$/i;
10
-
11
- // More permissive pattern for initial validation (allows spaces to be cleaned)
12
- const initialPattern = /^[A-Za-z]{2}[\d\s]*$/;
6
+ // - 2 letters & 6 digits (e.g., SD865005 or SD 865 005)
7
+ const pattern = /^((([sS]|[nN])[a-hA-Hj-zJ-Z])|(([tT]|[oO])[abfglmqrvwABFGLMQRVW])|([hH][l-zL-Z])|([jJ][lmqrvwLMQRVW]))\s?(([0-9]{3})\s?([0-9]{3}))$/;
13
8
  return {
14
- pattern: initialPattern,
15
- patternErrorMessage: `Enter a valid OS grid reference for ${this.title} like TQ123456`,
16
- customValidation: (value, helpers) => {
17
- // Strip spaces from the input for processing
18
- const cleanValue = value.replace(/\s/g, '');
19
- const letters = cleanValue.substring(0, 2);
20
- const numbers = cleanValue.substring(2);
21
-
22
- // Validate number length
23
- if (numbers.length !== 6 && numbers.length !== 8 && numbers.length !== 10) {
24
- return helpers.error('string.pattern.base');
25
- }
26
-
27
- // Format with spaces: XX 123 456, XX 1234 5678, or XX 12345 67890
28
- const halfLength = numbers.length / 2;
29
- const formattedValue = `${letters} ${numbers.substring(0, halfLength)} ${numbers.substring(halfLength)}`;
30
-
31
- // Validate the formatted value against the OS grid pattern
32
- if (!osGridPattern.test(formattedValue)) {
33
- return helpers.error('string.pattern.base');
34
- }
35
-
36
- // Return formatted value with spaces per GDS guidance
37
- return formattedValue;
38
- }
9
+ pattern,
10
+ patternErrorMessage: `Enter a valid OS grid reference for ${this.title} like TQ123456`
39
11
  };
40
12
  }
41
13
  getErrorTemplates() {
@@ -1 +1 @@
1
- {"version":3,"file":"OsGridRefField.js","names":["LocationFieldBase","OsGridRefField","getValidationConfig","osGridPattern","initialPattern","pattern","patternErrorMessage","title","customValidation","value","helpers","cleanValue","replace","letters","substring","numbers","length","error","halfLength","formattedValue","test","getErrorTemplates","type","template","getAllPossibleErrors","instance","Object","create","prototype"],"sources":["../../../../../src/server/plugins/engine/components/OsGridRefField.ts"],"sourcesContent":["import { type OsGridRefFieldComponent } from '@defra/forms-model'\nimport type joi from 'joi'\n\nimport { LocationFieldBase } from '~/src/server/plugins/engine/components/LocationFieldBase.js'\n\nexport class OsGridRefField extends LocationFieldBase {\n declare options: OsGridRefFieldComponent['options']\n\n protected getValidationConfig() {\n // Regex for OS grid references and parcel IDs\n // Validates specific valid OS grid letter combinations with:\n // - 6 digits (e.g., SD865005 or SD 865 005)\n // - 8 digits in 2 blocks of 4 (parcel ID) e.g., ST 6789 6789\n // - 10 digits in 2 blocks of 5 (OS grid reference) e.g., SO 12345 12345\n const osGridPattern =\n /^(?:[sn][a-hj-z]|[to][abfglmqrvw]|h[l-z]|j[lmqrvw])\\s?(?:\\d{3}\\s?\\d{3}|\\d{4}\\s?\\d{4}|\\d{5}\\s?\\d{5})$/i\n\n // More permissive pattern for initial validation (allows spaces to be cleaned)\n const initialPattern = /^[A-Za-z]{2}[\\d\\s]*$/\n\n return {\n pattern: initialPattern,\n patternErrorMessage: `Enter a valid OS grid reference for ${this.title} like TQ123456`,\n customValidation: (value: string, helpers: joi.CustomHelpers) => {\n // Strip spaces from the input for processing\n const cleanValue = value.replace(/\\s/g, '')\n const letters = cleanValue.substring(0, 2)\n const numbers = cleanValue.substring(2)\n\n // Validate number length\n if (\n numbers.length !== 6 &&\n numbers.length !== 8 &&\n numbers.length !== 10\n ) {\n return helpers.error('string.pattern.base')\n }\n\n // Format with spaces: XX 123 456, XX 1234 5678, or XX 12345 67890\n const halfLength = numbers.length / 2\n const formattedValue = `${letters} ${numbers.substring(0, halfLength)} ${numbers.substring(halfLength)}`\n\n // Validate the formatted value against the OS grid pattern\n if (!osGridPattern.test(formattedValue)) {\n return helpers.error('string.pattern.base')\n }\n\n // Return formatted value with spaces per GDS guidance\n return formattedValue\n }\n }\n }\n\n protected getErrorTemplates() {\n return [\n {\n type: 'pattern',\n template:\n 'Enter a valid OS grid reference for [short description] like TQ123456'\n }\n ]\n }\n\n /**\n * Static version of getAllPossibleErrors that doesn't require a component instance.\n */\n static getAllPossibleErrors() {\n const instance = Object.create(OsGridRefField.prototype) as OsGridRefField\n return instance.getAllPossibleErrors()\n }\n}\n"],"mappings":"AAGA,SAASA,iBAAiB;AAE1B,OAAO,MAAMC,cAAc,SAASD,iBAAiB,CAAC;EAG1CE,mBAAmBA,CAAA,EAAG;IAC9B;IACA;IACA;IACA;IACA;IACA,MAAMC,aAAa,GACjB,uGAAuG;;IAEzG;IACA,MAAMC,cAAc,GAAG,sBAAsB;IAE7C,OAAO;MACLC,OAAO,EAAED,cAAc;MACvBE,mBAAmB,EAAE,uCAAuC,IAAI,CAACC,KAAK,gBAAgB;MACtFC,gBAAgB,EAAEA,CAACC,KAAa,EAAEC,OAA0B,KAAK;QAC/D;QACA,MAAMC,UAAU,GAAGF,KAAK,CAACG,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;QAC3C,MAAMC,OAAO,GAAGF,UAAU,CAACG,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC;QAC1C,MAAMC,OAAO,GAAGJ,UAAU,CAACG,SAAS,CAAC,CAAC,CAAC;;QAEvC;QACA,IACEC,OAAO,CAACC,MAAM,KAAK,CAAC,IACpBD,OAAO,CAACC,MAAM,KAAK,CAAC,IACpBD,OAAO,CAACC,MAAM,KAAK,EAAE,EACrB;UACA,OAAON,OAAO,CAACO,KAAK,CAAC,qBAAqB,CAAC;QAC7C;;QAEA;QACA,MAAMC,UAAU,GAAGH,OAAO,CAACC,MAAM,GAAG,CAAC;QACrC,MAAMG,cAAc,GAAG,GAAGN,OAAO,IAAIE,OAAO,CAACD,SAAS,CAAC,CAAC,EAAEI,UAAU,CAAC,IAAIH,OAAO,CAACD,SAAS,CAACI,UAAU,CAAC,EAAE;;QAExG;QACA,IAAI,CAACf,aAAa,CAACiB,IAAI,CAACD,cAAc,CAAC,EAAE;UACvC,OAAOT,OAAO,CAACO,KAAK,CAAC,qBAAqB,CAAC;QAC7C;;QAEA;QACA,OAAOE,cAAc;MACvB;IACF,CAAC;EACH;EAEUE,iBAAiBA,CAAA,EAAG;IAC5B,OAAO,CACL;MACEC,IAAI,EAAE,SAAS;MACfC,QAAQ,EACN;IACJ,CAAC,CACF;EACH;;EAEA;AACF;AACA;EACE,OAAOC,oBAAoBA,CAAA,EAAG;IAC5B,MAAMC,QAAQ,GAAGC,MAAM,CAACC,MAAM,CAAC1B,cAAc,CAAC2B,SAAS,CAAmB;IAC1E,OAAOH,QAAQ,CAACD,oBAAoB,CAAC,CAAC;EACxC;AACF","ignoreList":[]}
1
+ {"version":3,"file":"OsGridRefField.js","names":["LocationFieldBase","OsGridRefField","getValidationConfig","pattern","patternErrorMessage","title","getErrorTemplates","type","template","getAllPossibleErrors","instance","Object","create","prototype"],"sources":["../../../../../src/server/plugins/engine/components/OsGridRefField.ts"],"sourcesContent":["import { type OsGridRefFieldComponent } from '@defra/forms-model'\n\nimport { LocationFieldBase } from '~/src/server/plugins/engine/components/LocationFieldBase.js'\n\nexport class OsGridRefField extends LocationFieldBase {\n declare options: OsGridRefFieldComponent['options']\n\n protected getValidationConfig() {\n // Regex for OS grid references and parcel IDs\n // Validates specific valid OS grid letter combinations with:\n // - 2 letters & 6 digits (e.g., SD865005 or SD 865 005)\n const pattern =\n /^((([sS]|[nN])[a-hA-Hj-zJ-Z])|(([tT]|[oO])[abfglmqrvwABFGLMQRVW])|([hH][l-zL-Z])|([jJ][lmqrvwLMQRVW]))\\s?(([0-9]{3})\\s?([0-9]{3}))$/\n\n return {\n pattern,\n patternErrorMessage: `Enter a valid OS grid reference for ${this.title} like TQ123456`\n }\n }\n\n protected getErrorTemplates() {\n return [\n {\n type: 'pattern',\n template:\n 'Enter a valid OS grid reference for [short description] like TQ123456'\n }\n ]\n }\n\n /**\n * Static version of getAllPossibleErrors that doesn't require a component instance.\n */\n static getAllPossibleErrors() {\n const instance = Object.create(OsGridRefField.prototype) as OsGridRefField\n return instance.getAllPossibleErrors()\n }\n}\n"],"mappings":"AAEA,SAASA,iBAAiB;AAE1B,OAAO,MAAMC,cAAc,SAASD,iBAAiB,CAAC;EAG1CE,mBAAmBA,CAAA,EAAG;IAC9B;IACA;IACA;IACA,MAAMC,OAAO,GACX,qIAAqI;IAEvI,OAAO;MACLA,OAAO;MACPC,mBAAmB,EAAE,uCAAuC,IAAI,CAACC,KAAK;IACxE,CAAC;EACH;EAEUC,iBAAiBA,CAAA,EAAG;IAC5B,OAAO,CACL;MACEC,IAAI,EAAE,SAAS;MACfC,QAAQ,EACN;IACJ,CAAC,CACF;EACH;;EAEA;AACF;AACA;EACE,OAAOC,oBAAoBA,CAAA,EAAG;IAC5B,MAAMC,QAAQ,GAAGC,MAAM,CAACC,MAAM,CAACX,cAAc,CAACY,SAAS,CAAmB;IAC1E,OAAOH,QAAQ,CAACD,oBAAoB,CAAC,CAAC;EACxC;AACF","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/forms-engine-plugin",
3
- "version": "4.0.11",
3
+ "version": "4.0.12",
4
4
  "description": "Defra forms engine",
5
5
  "type": "module",
6
6
  "files": [
@@ -203,7 +203,7 @@ pages:
203
203
  path: '/how-many-members-of-staff-will-look-after-the-unicorns'
204
204
  section: susaYr
205
205
  next:
206
- - path: '/summary'
206
+ - path: '/declaration'
207
207
  components:
208
208
  - name: zhJMaM
209
209
  options:
@@ -219,7 +219,7 @@ pages:
219
219
  controller: FileUploadPageController
220
220
  section: Regnsa
221
221
  next:
222
- - path: '/declaration'
222
+ - path: '/how-many-unicorns-do-you-expect-to-breed-each-year'
223
223
  components:
224
224
  - name: dLzALM
225
225
  title: Documents
@@ -556,14 +556,7 @@ describe('EastingNorthingField', () => {
556
556
  easting: 12345.5,
557
557
  northing: 1234567
558
558
  }),
559
- // Two errors expected: decimal input triggers both integer validation
560
- // and length validation ('12345.5' is 7 chars, max is 6)
561
559
  errors: [
562
- expect.objectContaining({
563
- text: expect.stringMatching(
564
- /Easting for .* must be between 1 and 6 digits/
565
- )
566
- }),
567
560
  expect.objectContaining({
568
561
  text: expect.stringMatching(
569
562
  /Easting for .* must be between 1 and 6 digits/
@@ -582,14 +575,7 @@ describe('EastingNorthingField', () => {
582
575
  easting: 12345,
583
576
  northing: 1234567.5
584
577
  }),
585
- // Two errors expected: decimal input triggers both integer validation
586
- // and length validation ('1234567.5' is 9 chars, max is 7)
587
578
  errors: [
588
- expect.objectContaining({
589
- text: expect.stringMatching(
590
- /Northing for .* must be between 1 and 7 digits/
591
- )
592
- }),
593
579
  expect.objectContaining({
594
580
  text: expect.stringMatching(
595
581
  /Northing for .* must be between 1 and 7 digits/
@@ -32,18 +32,6 @@ const DEFAULT_EASTING_MAX = 700000
32
32
  const DEFAULT_NORTHING_MIN = 0
33
33
  const DEFAULT_NORTHING_MAX = 1300000
34
34
 
35
- // Easting length constraints (integer values only, no decimals)
36
- // Min: 1 char for values like "0" or single digit values
37
- // Max: 6 chars for values up to 700000 (British National Grid easting limit)
38
- const EASTING_MIN_LENGTH = 1
39
- const EASTING_MAX_LENGTH = 6
40
-
41
- // Northing length constraints (integer values only, no decimals)
42
- // Min: 1 char for values like "0" or single digit values
43
- // Max: 7 chars for values up to 1300000 (British National Grid northing limit)
44
- const NORTHING_MIN_LENGTH = 1
45
- const NORTHING_MAX_LENGTH = 7
46
-
47
35
  export class EastingNorthingField extends FormComponent {
48
36
  declare options: EastingNorthingFieldComponent['options']
49
37
  declare formSchema: ObjectSchema<FormPayload>
@@ -73,9 +61,7 @@ export class EastingNorthingField extends FormComponent {
73
61
  'number.max': `{{#label}} for ${this.title} must be between ${eastingMin} and {{#limit}}`,
74
62
  'number.precision': `{{#label}} for ${this.title} must be between 1 and 6 digits`,
75
63
  'number.integer': `{{#label}} for ${this.title} must be between 1 and 6 digits`,
76
- 'number.unsafe': `{{#label}} for ${this.title} must be between 1 and 6 digits`,
77
- 'number.minLength': `{{#label}} for ${this.title} must be between 1 and 6 digits`,
78
- 'number.maxLength': `{{#label}} for ${this.title} must be between 1 and 6 digits`
64
+ 'number.unsafe': `{{#label}} for ${this.title} must be between 1 and 6 digits`
79
65
  })
80
66
 
81
67
  const northingValidationMessages: LanguageMessages =
@@ -86,9 +72,7 @@ export class EastingNorthingField extends FormComponent {
86
72
  'number.max': `{{#label}} for ${this.title} must be between ${northingMin} and {{#limit}}`,
87
73
  'number.precision': `{{#label}} for ${this.title} must be between 1 and 7 digits`,
88
74
  'number.integer': `{{#label}} for ${this.title} must be between 1 and 7 digits`,
89
- 'number.unsafe': `{{#label}} for ${this.title} must be between 1 and 7 digits`,
90
- 'number.minLength': `{{#label}} for ${this.title} must be between 1 and 7 digits`,
91
- 'number.maxLength': `{{#label}} for ${this.title} must be between 1 and 7 digits`
75
+ 'number.unsafe': `{{#label}} for ${this.title} must be between 1 and 7 digits`
92
76
  })
93
77
 
94
78
  this.collection = new ComponentCollection(
@@ -100,9 +84,7 @@ export class EastingNorthingField extends FormComponent {
100
84
  schema: {
101
85
  min: eastingMin,
102
86
  max: eastingMax,
103
- precision: 0,
104
- minLength: EASTING_MIN_LENGTH,
105
- maxLength: EASTING_MAX_LENGTH
87
+ precision: 0
106
88
  },
107
89
  options: {
108
90
  required: isRequired,
@@ -118,9 +100,7 @@ export class EastingNorthingField extends FormComponent {
118
100
  schema: {
119
101
  min: northingMin,
120
102
  max: northingMax,
121
- precision: 0,
122
- minLength: NORTHING_MIN_LENGTH,
123
- maxLength: NORTHING_MAX_LENGTH
103
+ precision: 0
124
104
  },
125
105
  options: {
126
106
  required: isRequired,
@@ -149,7 +149,7 @@ describe('LatLongField', () => {
149
149
 
150
150
  const result2 = collection.validate(
151
151
  getFormData({
152
- latitude: '49.1',
152
+ latitude: '50.5',
153
153
  longitude: '-8.9'
154
154
  })
155
155
  )
@@ -578,15 +578,7 @@ describe('LatLongField', () => {
578
578
  value: getFormData({
579
579
  latitude: 52,
580
580
  longitude: -1
581
- }),
582
- errors: [
583
- expect.objectContaining({
584
- text: 'Latitude must have at least 1 decimal place'
585
- }),
586
- expect.objectContaining({
587
- text: 'Longitude must have at least 1 decimal place'
588
- })
589
- ]
581
+ })
590
582
  }
591
583
  },
592
584
  {
@@ -619,7 +611,6 @@ describe('LatLongField', () => {
619
611
  description: 'Length and precision validation',
620
612
  component: createLatLongComponent(),
621
613
  assertions: [
622
- // Latitude too short
623
614
  {
624
615
  input: getFormData({
625
616
  latitude: '52',
@@ -629,12 +620,7 @@ describe('LatLongField', () => {
629
620
  value: getFormData({
630
621
  latitude: 52,
631
622
  longitude: -1.5
632
- }),
633
- errors: [
634
- expect.objectContaining({
635
- text: 'Latitude must have at least 1 decimal place'
636
- })
637
- ]
623
+ })
638
624
  }
639
625
  },
640
626
  // Latitude too long
@@ -655,7 +641,6 @@ describe('LatLongField', () => {
655
641
  ]
656
642
  }
657
643
  },
658
- // Longitude too short
659
644
  {
660
645
  input: getFormData({
661
646
  latitude: '52.1',
@@ -665,12 +650,7 @@ describe('LatLongField', () => {
665
650
  value: getFormData({
666
651
  latitude: 52.1,
667
652
  longitude: -1
668
- }),
669
- errors: [
670
- expect.objectContaining({
671
- text: 'Longitude must have at least 1 decimal place'
672
- })
673
- ]
653
+ })
674
654
  }
675
655
  },
676
656
  // Longitude too long
@@ -26,19 +26,6 @@ import { convertToLanguageMessages } from '~/src/server/utils/type-utils.js'
26
26
  // Precision constants
27
27
  // UK latitude/longitude requires high precision for accurate location (within ~11mm)
28
28
  const DECIMAL_PRECISION = 7 // 7 decimal places
29
- const MIN_DECIMAL_PLACES = 1 // At least 1 decimal place required
30
-
31
- // Latitude length constraints
32
- // Min: 3 chars for values like "52.1" (2 digits + decimal + 1 decimal place)
33
- // Max: 10 chars for values like "59.1234567" (2 digits + decimal + 7 decimal places)
34
- const LATITUDE_MIN_LENGTH = 3
35
- const LATITUDE_MAX_LENGTH = 10
36
-
37
- // Longitude length constraints
38
- // Min: 2 chars for values like "-1" or single digit with decimal (needs min decimal places)
39
- // Max: 10 chars for values like "-1.1234567" (minus + 1 digit + decimal + 7 decimal places)
40
- const LONGITUDE_MIN_LENGTH = 2
41
- const LONGITUDE_MAX_LENGTH = 10
42
29
 
43
30
  export class LatLongField extends FormComponent {
44
31
  declare options: LatLongFieldComponent['options']
@@ -57,10 +44,10 @@ export class LatLongField extends FormComponent {
57
44
  const isRequired = options.required !== false
58
45
 
59
46
  // Read schema values from def.schema with fallback defaults
60
- const latitudeMin = schema?.latitude?.min ?? 49
61
- const latitudeMax = schema?.latitude?.max ?? 60
62
- const longitudeMin = schema?.longitude?.min ?? -9
63
- const longitudeMax = schema?.longitude?.max ?? 2
47
+ const latitudeMin = schema?.latitude?.min ?? 49.85
48
+ const latitudeMax = schema?.latitude?.max ?? 60.859
49
+ const longitudeMin = schema?.longitude?.min ?? -13.687
50
+ const longitudeMax = schema?.longitude?.max ?? 1.767
64
51
 
65
52
  const customValidationMessages: LanguageMessages =
66
53
  convertToLanguageMessages({
@@ -68,8 +55,6 @@ export class LatLongField extends FormComponent {
68
55
  'number.base': messageTemplate.objectMissing,
69
56
  'number.precision':
70
57
  '{{#label}} must have no more than 7 decimal places',
71
- 'number.minPrecision':
72
- '{{#label}} must have at least {{#minPrecision}} decimal place',
73
58
  'number.unsafe': '{{#label}} must be a valid number'
74
59
  })
75
60
 
@@ -77,18 +62,14 @@ export class LatLongField extends FormComponent {
77
62
  ...customValidationMessages,
78
63
  'number.base': `Enter a valid latitude for ${this.title} like 51.519450`,
79
64
  'number.min': `Latitude for ${this.title} must be between ${latitudeMin} and ${latitudeMax}`,
80
- 'number.max': `Latitude for ${this.title} must be between ${latitudeMin} and ${latitudeMax}`,
81
- 'number.minLength': `Latitude for ${this.title} must be between 3 and 10 characters`,
82
- 'number.maxLength': `Latitude for ${this.title} must be between 3 and 10 characters`
65
+ 'number.max': `Latitude for ${this.title} must be between ${latitudeMin} and ${latitudeMax}`
83
66
  })
84
67
 
85
68
  const longitudeMessages: LanguageMessages = convertToLanguageMessages({
86
69
  ...customValidationMessages,
87
70
  'number.base': `Enter a valid longitude for ${this.title} like -0.127758`,
88
71
  'number.min': `Longitude for ${this.title} must be between ${longitudeMin} and ${longitudeMax}`,
89
- 'number.max': `Longitude for ${this.title} must be between ${longitudeMin} and ${longitudeMax}`,
90
- 'number.minLength': `Longitude for ${this.title} must be between 2 and 10 characters`,
91
- 'number.maxLength': `Longitude for ${this.title} must be between 2 and 10 characters`
72
+ 'number.max': `Longitude for ${this.title} must be between ${longitudeMin} and ${longitudeMax}`
92
73
  })
93
74
 
94
75
  this.collection = new ComponentCollection(
@@ -100,10 +81,7 @@ export class LatLongField extends FormComponent {
100
81
  schema: {
101
82
  min: latitudeMin,
102
83
  max: latitudeMax,
103
- precision: DECIMAL_PRECISION,
104
- minPrecision: MIN_DECIMAL_PLACES,
105
- minLength: LATITUDE_MIN_LENGTH,
106
- maxLength: LATITUDE_MAX_LENGTH
84
+ precision: DECIMAL_PRECISION
107
85
  },
108
86
  options: {
109
87
  required: isRequired,
@@ -120,10 +98,7 @@ export class LatLongField extends FormComponent {
120
98
  schema: {
121
99
  min: longitudeMin,
122
100
  max: longitudeMax,
123
- precision: DECIMAL_PRECISION,
124
- minPrecision: MIN_DECIMAL_PLACES,
125
- minLength: LONGITUDE_MIN_LENGTH,
126
- maxLength: LONGITUDE_MAX_LENGTH
101
+ precision: DECIMAL_PRECISION
127
102
  },
128
103
  options: {
129
104
  required: isRequired,
@@ -28,11 +28,6 @@ interface LocationFieldOptions {
28
28
  interface ValidationConfig {
29
29
  pattern: RegExp
30
30
  patternErrorMessage: string
31
- customValidation?: (
32
- value: string,
33
- helpers: joi.CustomHelpers
34
- ) => string | joi.ErrorReport
35
- additionalMessages?: LanguageMessages
36
31
  }
37
32
 
38
33
  /**
@@ -71,14 +66,9 @@ export abstract class LocationFieldBase extends FormComponent {
71
66
  .required()
72
67
  .pattern(config.pattern)
73
68
  .messages({
74
- 'string.pattern.base': config.patternErrorMessage,
75
- ...config.additionalMessages
69
+ 'string.pattern.base': config.patternErrorMessage
76
70
  })
77
71
 
78
- if (config.customValidation) {
79
- formSchema = formSchema.custom(config.customValidation)
80
- }
81
-
82
72
  if (locationOptions.required === false) {
83
73
  formSchema = formSchema.allow('')
84
74
  }
@@ -91,10 +81,6 @@ export abstract class LocationFieldBase extends FormComponent {
91
81
  'string.pattern.base'
92
82
  ]
93
83
 
94
- if (config.additionalMessages) {
95
- messageKeys.push(...Object.keys(config.additionalMessages))
96
- }
97
-
98
84
  const messages = messageKeys.reduce<LanguageMessages>((acc, key) => {
99
85
  acc[key] = message
100
86
  return acc
@@ -99,13 +99,27 @@ describe('NationalGridFieldNumberField', () => {
99
99
  })
100
100
 
101
101
  it('accepts valid values', () => {
102
- const result1 = collection.validate(getFormData('NG12345678'))
103
- const result2 = collection.validate(getFormData('ng12345678'))
104
- const result3 = collection.validate(getFormData('AB98765432'))
102
+ // Test 8-digit parcel ID format (2x4)
103
+ const result1 = collection.validate(getFormData('TQ12345678'))
104
+ const result2 = collection.validate(getFormData('TQ 1234 5678'))
105
+
106
+ // Test 10-digit OS grid reference format (2x5)
107
+ const result3 = collection.validate(getFormData('SU1234567890'))
108
+ const result4 = collection.validate(getFormData('SU 12345 67890'))
109
+
110
+ expect(result1.errors).toBeUndefined()
111
+ expect(result2.errors).toBeUndefined()
112
+ expect(result3.errors).toBeUndefined()
113
+ expect(result4.errors).toBeUndefined()
114
+
115
+ // Test case-insensitive
116
+ const result5 = collection.validate(getFormData('nt12345678'))
105
117
 
106
118
  expect(result1.errors).toBeUndefined()
107
119
  expect(result2.errors).toBeUndefined()
108
120
  expect(result3.errors).toBeUndefined()
121
+ expect(result4.errors).toBeUndefined()
122
+ expect(result5.errors).toBeUndefined()
109
123
  })
110
124
 
111
125
  it('formats values with spaces per GDS guidance', () => {
@@ -114,8 +128,8 @@ describe('NationalGridFieldNumberField', () => {
114
128
  const result3 = collection.validate(getFormData('NG12345,678'))
115
129
 
116
130
  expect(result1.value.myComponent).toBe('NG 1234 5678')
117
- expect(result2.value.myComponent).toBe('NG 1234 5678')
118
- expect(result3.value.myComponent).toBe('NG 1234 5678')
131
+ expect(result2.value.myComponent).toBe('NG12345678')
132
+ expect(result3.value.myComponent).toBe('NG12345,678')
119
133
  })
120
134
 
121
135
  it('adds errors for empty value', () => {
@@ -258,15 +272,15 @@ describe('NationalGridFieldNumberField', () => {
258
272
  assertions: [
259
273
  {
260
274
  input: getFormData(' NG12345678'),
261
- output: { value: getFormData('NG 1234 5678') }
275
+ output: { value: getFormData('NG12345678') }
262
276
  },
263
277
  {
264
278
  input: getFormData('NG12345678 '),
265
- output: { value: getFormData('NG 1234 5678') }
279
+ output: { value: getFormData('NG12345678') }
266
280
  },
267
281
  {
268
282
  input: getFormData(' NG12345678 \n\n'),
269
- output: { value: getFormData('NG 1234 5678') }
283
+ output: { value: getFormData('NG12345678') }
270
284
  }
271
285
  ]
272
286
  },
@@ -1,5 +1,4 @@
1
1
  import { type NationalGridFieldNumberFieldComponent } from '@defra/forms-model'
2
- import type joi from 'joi'
3
2
 
4
3
  import { LocationFieldBase } from '~/src/server/plugins/engine/components/LocationFieldBase.js'
5
4
 
@@ -7,26 +6,16 @@ export class NationalGridFieldNumberField extends LocationFieldBase {
7
6
  declare options: NationalGridFieldNumberFieldComponent['options']
8
7
 
9
8
  protected getValidationConfig() {
10
- return {
11
- // Pattern allows spaces and commas in the input since custom validation will clean them
12
- pattern: /^[A-Z]{2}[\d\s,]*$/i,
13
- patternErrorMessage: `Enter a valid National Grid field number for ${this.title} like NG 1234 5678`,
14
- customValidation: (value: string, helpers: joi.CustomHelpers) => {
15
- // Strip spaces and commas for validation
16
- const cleanValue = value.replace(/[\s,]/g, '')
17
-
18
- // Check if it matches the exact pattern after cleaning
19
- if (!/^[A-Z]{2}\d{8}$/i.test(cleanValue)) {
20
- return helpers.error('string.pattern.base')
21
- }
22
-
23
- // Format with spaces per GDS guidance: NG 1234 5678
24
- const letters = cleanValue.substring(0, 2)
25
- const numbers = cleanValue.substring(2)
26
- const formattedValue = `${letters} ${numbers.substring(0, 4)} ${numbers.substring(4)}`
9
+ // Regex for OS grid references and parcel IDs
10
+ // Validates specific valid OS grid letter combinations with:
11
+ // - 2 letters & 8 digits in 2 blocks of 4 (parcel ID) e.g., ST 6789 6789
12
+ // - 2 letters & 10 digits in 2 blocks of 5 (OS grid reference) e.g., SO 12345 12345
13
+ const pattern =
14
+ /^((([sS]|[nN])[a-hA-Hj-zJ-Z])|(([tT]|[oO])[abfglmqrvwABFGLMQRVW])|([hH][l-zL-Z])|([jJ][lmqrvwLMQRVW]))\s?(([0-9]{4})\s?([0-9]{4})|([0-9]{5})\s?([0-9]{5}))$/
27
15
 
28
- return formattedValue
29
- }
16
+ return {
17
+ pattern,
18
+ patternErrorMessage: `Enter a valid National Grid field number for ${this.title} like NG 1234 5678`
30
19
  }
31
20
  }
32
21