From f54c402c54621d7d464fc8ca5a1d4eae6bb6e4cc Mon Sep 17 00:00:00 2001 From: Colin Rotherham Date: Wed, 1 May 2019 13:24:02 +0100 Subject: [PATCH] refactor: allow component inputs to be more configurable Alter the date-input and postal-address-object template macros to allow the addition and overriding of certain macro parameters. Signed-off-by: Colin Rotherham --- .../casa/components/date-input/README.md | 35 +++ .../casa/components/date-input/template.njk | 46 ++-- .../postal-address-object/README.md | 32 ++- .../postal-address-object/template.njk | 61 ++--- test/unit/templates/components/date-input.njk | 3 + .../templates/components/date-inputTests.js | 143 +++++++++++ .../components/postal-address-object.njk | 3 + .../components/postal-address-objectTests.js | 232 ++++++++++++++++++ 8 files changed, 505 insertions(+), 50 deletions(-) create mode 100644 test/unit/templates/components/date-input.njk create mode 100644 test/unit/templates/components/date-inputTests.js create mode 100644 test/unit/templates/components/postal-address-object.njk create mode 100644 test/unit/templates/components/postal-address-objectTests.js diff --git a/app/views/casa/components/date-input/README.md b/app/views/casa/components/date-input/README.md index b5bcfba2..e8d82692 100644 --- a/app/views/casa/components/date-input/README.md +++ b/app/views/casa/components/date-input/README.md @@ -23,6 +23,41 @@ casaGovukDateInput({ }) ``` +With configurable items: + +```nunjucks +{% from "casa/components/date-input/macro.njk" import casaGovukDateInput %} + +casaGovukDateInput({ + namePrefix: "dateOfBirth", + casaValue: formData.dateOfBirth, + casaErrors: formErrors, + items: [ + { + autocomplete: "bday-day", + attributes: { + min: "1", + max: "31" + } + }, + { + autocomplete: "bday-month", + attributes: { + min: "1", + max: "12" + } + }, + { + autocomplete: "bday-year", + attributes: { + min: "1", + max: "9999" + } + } + ] +}) +``` + ## Companion validation There is a companion validation rule - [`dateObject`](../../../../../docs/field-validation-rules.md#dateObject) - that you can use to ensure the dates are passed in correctly. diff --git a/app/views/casa/components/date-input/template.njk b/app/views/casa/components/date-input/template.njk index 3c0758b5..bdddfe29 100644 --- a/app/views/casa/components/date-input/template.njk +++ b/app/views/casa/components/date-input/template.njk @@ -20,26 +20,32 @@ id: params.id if params.id else 'f-' + params.namePrefix, namePrefix: '', attributes: mergedAttributes, - items: [{ - label: t('macros:dateInput.day'), - name: params.namePrefix + '[dd]', - id: 'f-' + params.namePrefix + '[dd]', - value: params.casaValue.dd, - classes: 'govuk-input--width-2 ' + (inputErrorClass if includes(fieldErrors[0].focusSuffix, '[dd]') or not hasSuffixHighlights) - }, { - label: t('macros:dateInput.month'), - name: params.namePrefix + '[mm]', - id: 'f-' + params.namePrefix + '[mm]', - value: params.casaValue.mm, - classes: 'govuk-input--width-2 ' + (inputErrorClass if includes(fieldErrors[0].focusSuffix, '[mm]') or not hasSuffixHighlights), - attributes: attributes_mm - }, { - label: t('macros:dateInput.year'), - name: params.namePrefix + '[yyyy]', - id: 'f-' + params.namePrefix + '[yyyy]', - value: params.casaValue.yyyy, - classes: 'govuk-input--width-4 ' + (inputErrorClass if includes(fieldErrors[0].focusSuffix, '[yyyy]') or not hasSuffixHighlights) - }], + items: [ + mergeObjectsDeep({ + label: t('macros:dateInput.day'), + classes: 'govuk-input--width-2 ' + (inputErrorClass if includes(fieldErrors[0].focusSuffix, '[dd]') or not hasSuffixHighlights) + }, params.items[0] if params.items[0] else {}, { + id: 'f-' + params.namePrefix + '[dd]', + name: params.namePrefix + '[dd]', + value: params.casaValue.dd + }), + mergeObjectsDeep({ + label: t('macros:dateInput.month'), + classes: 'govuk-input--width-2 ' + (inputErrorClass if includes(fieldErrors[0].focusSuffix, '[mm]') or not hasSuffixHighlights) + }, params.items[1] if params.items[1] else {}, { + id: 'f-' + params.namePrefix + '[mm]', + name: params.namePrefix + '[mm]', + value: params.casaValue.mm + }), + mergeObjectsDeep({ + label: t('macros:dateInput.year'), + classes: 'govuk-input--width-4 ' + (inputErrorClass if includes(fieldErrors[0].focusSuffix, '[yyyy]') or not hasSuffixHighlights) + }, params.items[2] if params.items[2] else {}, { + id: 'f-' + params.namePrefix + '[yyyy]', + name: params.namePrefix + '[yyyy]', + value: params.casaValue.yyyy + }) + ], errorMessage: { text: t(params.casaErrors[params.namePrefix][0].inline) } if params.casaErrors[params.namePrefix] else null diff --git a/app/views/casa/components/postal-address-object/README.md b/app/views/casa/components/postal-address-object/README.md index 1a43660a..b542e6ce 100644 --- a/app/views/casa/components/postal-address-object/README.md +++ b/app/views/casa/components/postal-address-object/README.md @@ -2,8 +2,9 @@ Custom parameters: -* `casaValue` - the value of the chosen radio button. This is a convenience for toggling the `checked` flag on the appropriate `item`, but you can also manually set `checked` on each item if you need to use more specific logic for determining checked state. +* `value` - the value of the address object, in `{address1: '', address2: '', address3: '', address4: '', postcode: ''}` format * `casaErrors` - form errors (just pass `formErrors`) +* `address[1-4]` and `postcode` - for specifying extra attributes for each of the address input components ## Example usage @@ -15,7 +16,34 @@ Basic example: {{ casaPostalAddressObject({ name: 'address', value: formData.address, - casaErrors: formErrors, + casaErrors: formErrors +}) }} +``` + +With configurable items: + +```nunjucks +{% from "casa/components/postal-address-object/macro.njk" import casaPostalAddressObject %} + +{{ casaPostalAddressObject({ + name: 'address', + value: formData.address, + address1: { + autocomplete: 'address-line1' + }, + address2: { + autocomplete: 'address-line2' + }, + address3: { + autocomplete: 'address-level2' + }, + address4: { + autocomplete: 'address-level1' + }, + postcode: { + autocomplete: 'postal-code' + }, + casaErrors: formErrors }) }} ``` diff --git a/app/views/casa/components/postal-address-object/template.njk b/app/views/casa/components/postal-address-object/template.njk index 6e5e7293..b0209984 100644 --- a/app/views/casa/components/postal-address-object/template.njk +++ b/app/views/casa/components/postal-address-object/template.njk @@ -14,36 +14,38 @@ {% if fieldAddress1Errors %} {% set fieldAttributes = mergeObjects(fieldAttributes, {'data-validation': {fn: params.name + '[address1]', va: fieldAddress1Errors[0].validator} | dump}) %} {% endif %} - {{ govukInput({ + {{ govukInput(mergeObjectsDeep({ label: { html: t('macros:postalAddressObject.address1') }, - id: 'f-' + params.name + '[address1]', - name: params.name + '[address1]', - value: params.value.address1, attributes: fieldAttributes, errorMessage: { text: t(fieldAddress1Errors[0].inline) } if fieldAddress1Errors else null - }) }} + }, params.address1 if params.address1 else {}, { + id: 'f-' + params.name + '[address1]', + name: params.name + '[address1]', + value: params.value.address1 + })) }} {# Line 2 #} {% set fieldAttributes = {} %} {% if fieldAddress2Errors %} {% set fieldAttributes = mergeObjects(fieldAttributes, {'data-validation': {fn: params.name + '[address2]', va: fieldAddress2Errors[0].validator} | dump}) %} {% endif %} - {{ govukInput({ + {{ govukInput(mergeObjectsDeep({ label: { html: t('macros:postalAddressObject.address2') }, - id: 'f-' + params.name + '[address2]', - name: params.name + '[address2]', - value: params.value.address2, attributes: fieldAttributes, errorMessage: { text: t(fieldAddress2Errors[0].inline) } if fieldAddress2Errors else null - }) }} + }, params.address2 if params.address2 else {}, { + id: 'f-' + params.name + '[address2]', + name: params.name + '[address2]', + value: params.value.address2 + })) }} {# Town #} @@ -52,19 +54,20 @@ {% if fieldErrors %} {% set fieldAttributes = mergeObjects(fieldAttributes, {'data-validation': {fn: params.name + '[address3]', va: fieldErrors[0].validator} | dump}) %} {% endif %} - {{ govukInput({ + {{ govukInput(mergeObjectsDeep({ + classes: "govuk-!-width-two-thirds", label: { html: t('macros:postalAddressObject.address3') }, - id: 'f-' + params.name + '[address3]', - name: params.name + '[address3]', - value: params.value.address3, - classes: "govuk-!-width-two-thirds", attributes: fieldAttributes, errorMessage: { text: t(fieldErrors[0].inline) } if fieldErrors else null - }) }} + }, params.address3 if params.address3 else {}, { + id: 'f-' + params.name + '[address3]', + name: params.name + '[address3]', + value: params.value.address3 + })) }} {# County #} {% set fieldErrors = params.casaErrors[params.name+"[address4]"] %} @@ -72,19 +75,20 @@ {% if fieldErrors %} {% set fieldAttributes = mergeObjects(fieldAttributes, {'data-validation': {fn: params.name + '[address4]', va: fieldErrors[0].validator} | dump}) %} {% endif %} - {{ govukInput({ + {{ govukInput(mergeObjectsDeep({ + classes: "govuk-!-width-two-thirds", label: { html: t('macros:postalAddressObject.address4') }, - id: 'f-' + params.name + '[address4]', - name: params.name + '[address4]', - value: params.value.address4, - classes: "govuk-!-width-two-thirds", attributes: fieldAttributes, errorMessage: { text: t(fieldErrors[0].inline) } if fieldErrors else null - }) }} + }, params.address4 if params.address4 else {}, { + id: 'f-' + params.name + '[address4]', + name: params.name + '[address4]', + value: params.value.address4 + })) }} {# Postcode #} {% set fieldErrors = params.casaErrors[params.name+"[postcode]"] %} @@ -92,17 +96,18 @@ {% if fieldErrors %} {% set fieldAttributes = mergeObjects(fieldAttributes, {'data-validation': {fn: params.name + '[postcode]', va: fieldErrors[0].validator} | dump}) %} {% endif %} - {{ govukInput({ + {{ govukInput(mergeObjectsDeep({ + classes: "govuk-input--width-10", label: { html: t('macros:postalAddressObject.postcode') }, - id: 'f-' + params.name + '[postcode]', - name: params.name + '[postcode]', - value: params.value.postcode, - classes: "govuk-input--width-10", attributes: fieldAttributes, errorMessage: { text: t(fieldErrors[0].inline) } if fieldErrors else null - }) }} + }, params.postcode if params.postcode else {}, { + id: 'f-' + params.name + '[postcode]', + name: params.name + '[postcode]', + value: params.value.postcode + })) }} {% endcall %} diff --git a/test/unit/templates/components/date-input.njk b/test/unit/templates/components/date-input.njk new file mode 100644 index 00000000..f7d749d3 --- /dev/null +++ b/test/unit/templates/components/date-input.njk @@ -0,0 +1,3 @@ +{% from "casa/components/date-input/macro.njk" import casaGovukDateInput %} + +{{ casaGovukDateInput(params) }} diff --git a/test/unit/templates/components/date-inputTests.js b/test/unit/templates/components/date-inputTests.js new file mode 100644 index 00000000..7bb150ad --- /dev/null +++ b/test/unit/templates/components/date-inputTests.js @@ -0,0 +1,143 @@ +const npath = require('path'); +const { expect } = require('chai'); +const helpers = require('../helpers'); + +const dirMacros = npath.resolve(__dirname); + +describe('casaGovukDateInput macro', () => { + /** + * Build a DOM object. + * + * @param {Object} params Parameters with which to render the template + * @return {Object} DOM element (cheerio) + */ + function buildDom(params = {}) { + const p = Object.assign({ + namePrefix: null, + casaValue: null, + casaErrors: null, + }, params || {}); + return helpers.renderTemplateFile(`${dirMacros}/date-input.njk`, { + params: p, + }); + } + + /* ----------------------------------------------------------- Basic markup */ + + describe('Basic', () => { + let $; + + before(() => { + $ = buildDom({ + namePrefix: 'TEST', + casaValue: { + dd: '01', + mm: '02', + yyyy: '2000', + }, + }); + }); + + it('should generate 3 inputs', () => { + expect($('input').length).to.equal(3); + }); + + it('should have input ids prefixed with f- by default', () => { + $('input').each((i, el) => { + expect($(el).attr('id')).to.match(/^f-TEST/); + }); + }); + + it('should have input names and ids suffixed with dd, mm, yyyy by default', () => { + expect($('input').eq(0).attr('name')).to.equal('TEST[dd]'); + expect($('input').eq(0).attr('id')).to.equal('f-TEST[dd]'); + expect($('input').eq(1).attr('name')).to.equal('TEST[mm]'); + expect($('input').eq(1).attr('id')).to.equal('f-TEST[mm]'); + expect($('input').eq(2).attr('name')).to.equal('TEST[yyyy]'); + expect($('input').eq(2).attr('id')).to.equal('f-TEST[yyyy]'); + }); + + it('should have a correct value attribute', () => { + expect($('input[name="TEST[dd]"]').attr('value')).to.equal('01'); + expect($('input[name="TEST[mm]"]').attr('value')).to.equal('02'); + expect($('input[name="TEST[yyyy]"]').attr('value')).to.equal('2000'); + }); + + it('should allow custom attributes per item', () => { + const $custom = buildDom({ + namePrefix: 'TEST', + casaValue: { + dd: '01', + mm: '02', + yyyy: '2000', + }, + items: [{ + id: 'id-not-overrideable', + name: 'name-not-overrideable', + value: 'value-not-overrideable', + label: 'label-override', + class: 'class-override', + }, { + id: 'id-not-overrideable', + name: 'name-not-overrideable', + value: 'value-not-overrideable', + label: 'label-override', + class: 'class-override', + }, { + id: 'id-not-overrideable', + name: 'name-not-overrideable', + value: 'value-not-overrideable', + label: 'label-override', + class: 'class-override', + }], + }); + + expect($custom('input').eq(0).attr('id')).to.equal('f-TEST[dd]'); + expect($custom('input').eq(0).attr('name')).to.equal('TEST[dd]'); + expect($custom('input').eq(0).val()).to.equal('01'); + expect($custom('label[for="f-TEST[dd]"]').text().trim()).to.equal('label-override'); + expect($custom('input').eq(0).attr('class')).to.not.match(/class-override/); + + expect($custom('input').eq(1).attr('id')).to.equal('f-TEST[mm]'); + expect($custom('input').eq(1).attr('name')).to.equal('TEST[mm]'); + expect($custom('input').eq(1).val()).to.equal('02'); + expect($custom('label[for="f-TEST[mm]"]').text().trim()).to.equal('label-override'); + expect($custom('input').eq(1).attr('class')).to.not.match(/class-override/); + + expect($custom('input').eq(2).attr('id')).to.equal('f-TEST[yyyy]'); + expect($custom('input').eq(2).attr('name')).to.equal('TEST[yyyy]'); + expect($custom('input').eq(2).val()).to.equal('2000'); + expect($custom('label[for="f-TEST[yyyy]"]').text().trim()).to.equal('label-override'); + expect($custom('input').eq(2).attr('class')).to.not.match(/class-override/); + }); + }); + + /* ----------------------------------------------------------------- Errors */ + + describe('Errors', () => { + let $; + + before(() => { + $ = buildDom({ + namePrefix: 'errtest', + casaErrors: { + errtest: [{ + inline: 'Test Error Message', + }], + }, + }); + }); + + it('should have a error message with correct id', () => { + expect($('.govuk-error-message').attr('id')).to.equal('f-errtest-error'); + }); + + it('should have correct error mesage', () => { + expect($('.govuk-error-message').text().trim()).to.equal('Error: Test Error Message'); + }); + + it('should have a data-validation attribute', () => { + expect($('#f-errtest').attr('data-validation')).to.equal('{"fn":"errtest"}'); + }); + }); +}); diff --git a/test/unit/templates/components/postal-address-object.njk b/test/unit/templates/components/postal-address-object.njk new file mode 100644 index 00000000..7d4d00ac --- /dev/null +++ b/test/unit/templates/components/postal-address-object.njk @@ -0,0 +1,3 @@ +{% from "casa/components/postal-address-object/macro.njk" import casaPostalAddressObject %} + +{{ casaPostalAddressObject(params) }} diff --git a/test/unit/templates/components/postal-address-objectTests.js b/test/unit/templates/components/postal-address-objectTests.js new file mode 100644 index 00000000..0d00b9a7 --- /dev/null +++ b/test/unit/templates/components/postal-address-objectTests.js @@ -0,0 +1,232 @@ +const npath = require('path'); +const { expect } = require('chai'); +const helpers = require('../helpers'); + +const dirMacros = npath.resolve(__dirname); + +describe('casaPostalAddressObject macro', () => { + /** + * Build a DOM object. + * + * @param {Object} params Parameters with which to render the template + * @return {Object} DOM element (cheerio) + */ + function buildDom(params = {}) { + const p = Object.assign({ + namePrefix: null, + casaValue: null, + casaErrors: null, + }, params || {}); + return helpers.renderTemplateFile(`${dirMacros}/postal-address-object.njk`, { + params: p, + }); + } + + /* ----------------------------------------------------------- Basic markup */ + + describe('Basic', () => { + let $; + + before(() => { + $ = buildDom({ + name: 'TEST', + value: { + address1: 'addr1', + address2: 'addr2', + address3: 'addr3', + address4: 'addr4', + postcode: 'postcode', + }, + }); + }); + + it('should generate inputs for each address component', () => { + expect($('input').length).to.equal(5); + }); + + it('should have input ids prefixed with f-', () => { + $('input').each((i, el) => { + expect($(el).attr('id')).to.match(/^f-TEST/); + }); + }); + + it('should have input names and ids suffixed with address components', () => { + for (let i = 1; i < 5; i++) { + expect($('input').eq(i - 1).attr('name')).to.equal(`TEST[address${i}]`); + expect($('input').eq(i - 1).attr('id')).to.equal(`f-TEST[address${i}]`); + } + expect($('input').eq(4).attr('name')).to.equal('TEST[postcode]'); + expect($('input').eq(4).attr('id')).to.equal('f-TEST[postcode]'); + }); + + it('should have a correct value attribute', () => { + expect($('input[name="TEST[address1]"]').attr('value')).to.equal('addr1'); + expect($('input[name="TEST[address2]"]').attr('value')).to.equal('addr2'); + expect($('input[name="TEST[address3]"]').attr('value')).to.equal('addr3'); + expect($('input[name="TEST[address4]"]').attr('value')).to.equal('addr4'); + expect($('input[name="TEST[postcode]"]').attr('value')).to.equal('postcode'); + }); + + it('should allow custom attributes per item', () => { + const $custom = buildDom({ + name: 'TEST', + casaErrors: { + 'TEST[address1]': [{ + inline: 'err-message', + }], + }, + value: { + address1: 'addr1', + address2: 'addr2', + address3: 'addr3', + address4: 'addr4', + postcode: 'postcode', + }, + address1: { + id: 'id-not-overrideable', + name: 'name-not-overrideable', + value: 'value-not-overrideable', + label: { + html: 'label-override', + }, + attributes: { + testattr: '123', + }, + }, + address2: { + id: 'id2-not-overrideable', + name: 'name2-not-overrideable', + value: 'value2-not-overrideable', + label: { + html: 'label2-override', + }, + attributes: { + testattr: '456', + }, + }, + address3: { + id: 'id3-not-overrideable', + name: 'name3-not-overrideable', + value: 'value3-not-overrideable', + label: { + html: 'label3-override', + }, + classes: 'class3-override', + attributes: { + testattr: '789', + }, + }, + address4: { + id: 'id4-not-overrideable', + name: 'name4-not-overrideable', + value: 'value4-not-overrideable', + label: { + html: 'label4-override', + }, + classes: 'class4-override', + attributes: { + testattr: '012', + }, + }, + postcode: { + id: 'id5-not-overrideable', + name: 'name5-not-overrideable', + value: 'value5-not-overrideable', + label: { + html: 'label5-override', + }, + classes: 'class5-override', + attributes: { + testattr: '345', + }, + }, + }); + + expect($custom('input').eq(0).attr('id')).to.equal('f-TEST[address1]'); + expect($custom('input').eq(0).attr('name')).to.equal('TEST[address1]'); + expect($custom('input').eq(0).val()).to.equal('addr1'); + expect($custom('label[for="f-TEST[address1]"]').text().trim()).to.equal('label-override'); + expect($custom('input').eq(0).attr('testattr')).to.equal('123'); + /* eslint-disable-next-line no-unused-expressions */ + expect($custom('input').eq(0).attr('data-validation')).not.to.be.empty; + + expect($custom('input').eq(1).attr('id')).to.equal('f-TEST[address2]'); + expect($custom('input').eq(1).attr('name')).to.equal('TEST[address2]'); + expect($custom('input').eq(1).val()).to.equal('addr2'); + expect($custom('label[for="f-TEST[address2]"]').text().trim()).to.equal('label2-override'); + expect($custom('input').eq(1).attr('testattr')).to.equal('456'); + + expect($custom('input').eq(2).attr('id')).to.equal('f-TEST[address3]'); + expect($custom('input').eq(2).attr('name')).to.equal('TEST[address3]'); + expect($custom('input').eq(2).val()).to.equal('addr3'); + expect($custom('label[for="f-TEST[address3]"]').text().trim()).to.equal('label3-override'); + expect($custom('input').eq(2).attr('testattr')).to.equal('789'); + expect($custom('input').eq(2).attr('class')).to.match(/class3-override/); + + expect($custom('input').eq(3).attr('id')).to.equal('f-TEST[address4]'); + expect($custom('input').eq(3).attr('name')).to.equal('TEST[address4]'); + expect($custom('input').eq(3).val()).to.equal('addr4'); + expect($custom('label[for="f-TEST[address4]"]').text().trim()).to.equal('label4-override'); + expect($custom('input').eq(3).attr('testattr')).to.equal('012'); + expect($custom('input').eq(3).attr('class')).to.match(/class4-override/); + + expect($custom('input').eq(4).attr('id')).to.equal('f-TEST[postcode]'); + expect($custom('input').eq(4).attr('name')).to.equal('TEST[postcode]'); + expect($custom('input').eq(4).val()).to.equal('postcode'); + expect($custom('label[for="f-TEST[postcode]"]').text().trim()).to.equal('label5-override'); + expect($custom('input').eq(4).attr('testattr')).to.equal('345'); + expect($custom('input').eq(4).attr('class')).to.match(/class5-override/); + }); + }); + + /* ----------------------------------------------------------------- Errors */ + + describe('Errors', () => { + let $; + + before(() => { + $ = buildDom({ + name: 'errtest', + casaErrors: { + 'errtest[address1]': [{ + inline: 'Test Error Message addr1', + }], + 'errtest[address2]': [{ + inline: 'Test Error Message addr2', + }], + 'errtest[address3]': [{ + inline: 'Test Error Message addr3', + }], + 'errtest[address4]': [{ + inline: 'Test Error Message addr4', + }], + 'errtest[postcode]': [{ + inline: 'Test Error Message postcode', + }], + }, + }); + }); + + it('should have a error message with correct id', () => { + expect($('#f-errtest\\[address1\\]-error').length).to.equal(1); + expect($('#f-errtest\\[address2\\]-error').length).to.equal(1); + expect($('#f-errtest\\[address3\\]-error').length).to.equal(1); + expect($('#f-errtest\\[address4\\]-error').length).to.equal(1); + expect($('#f-errtest\\[postcode\\]-error').length).to.equal(1); + }); + + it('should have correct error mesage', () => { + for (let i = 1; i < 5; i++) { + expect($(`#f-errtest\\[address${i}\\]-error`).text().trim()).to.equal(`Error: Test Error Message addr${i}`); + } + expect($('#f-errtest\\[postcode\\]-error').text().trim()).to.equal('Error: Test Error Message postcode'); + }); + + it('should have a data-validation attribute', () => { + for (let i = 1; i < 5; i++) { + expect($(`#f-errtest\\[address${i}\\]`).attr('data-validation')).to.equal(`{"fn":"errtest[address${i}]"}`); + } + expect($('#f-errtest\\[postcode\\]').attr('data-validation')).to.equal('{"fn":"errtest[postcode]"}'); + }); + }); +});