From 0e0ca9b1cd373c557bd4070b3fa9dc6294b6f565 Mon Sep 17 00:00:00 2001 From: Joe Haig Date: Fri, 7 Jul 2023 09:41:02 +0100 Subject: [PATCH 1/4] Write motivation --- docs/javascript_restructure.md | 100 +++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 docs/javascript_restructure.md diff --git a/docs/javascript_restructure.md b/docs/javascript_restructure.md new file mode 100644 index 0000000000..c6714a25ac --- /dev/null +++ b/docs/javascript_restructure.md @@ -0,0 +1,100 @@ +## Javascript restructure + +### Motivation + +A style guide is provided for Javascript in Gov.UK sites at https://github.com/alphagov/govuk-frontend/blob/main/docs/contributing/coding-standards/js.md + +Currently the Javascript modules in CCCD do not follow this guide and instead modules are defined in two different ways; + +Executing a function expression to return an object; + +```javascript +// app/webpack/javascripts/modules/Helpers.API.Core.js + +(function (exports, $) { + const Module = exports.Helpers.API || {} + + ... + + Module._CORE = { + query + } + + exports.Helpers.API = Module +}(moj, jQuery)) +``` + +Adding to the `moj` module so that it is initialized together with all modules by `moj.init()` in `app/webpack/javascripts/application.js`; + +```javascript +// app/webpack/javascripts/modules/Helpers.DataTables.js + +moj.Helpers.DataTables = { + init: function (options, el) { + ... + }, + + ... +} +``` + +The Gov.UK style guide recommends using the class design pattern to structure the code; + +```javascript +class Example { + // Code goes here +} +``` + +The primary benefits of refactoring our Javascript is to have a single, consistent style so that future developers can better understand the codebase. +Specifically using the recommended Gov.UK style will further facilitate new developers who have worked on other Gov.UK projects. + +This is a *work in progress* so javascript modules may be written in any of the above formats but the ideal is the class design pattern. + +### Removal of jQuery + +There has also been a long-standing plan to remove the dependency on jQuery as modern versions of Javascript have all (most?) of the features that jQuery provided. There are some [notes here](https://tobiasahlin.com/blog/move-from-jquery-to-vanilla-javascript/) to help with migrating jQuery to vanilla Javascript and below are some further pointers. + +#### Accessing the data of elements + +jQuery provides a way of accessing data from elements as follows; + +```javascript +// + +table = $('#determinations') +scheme = table.data('scheme') +``` + +Without jQuery this becomes; + +```javascript +//
+ +table = document.getElementById('determinations') +scheme = table.dataset.scheme +``` + +#### DataTable + +CCCD uses the DataTable library in various places and this is built on jQuery a replacement will need to be found. One possibility is [Simple-DataTable.](https://github.com/fiduswriter/Simple-DataTables) + +### Running tests + +Jasmine tests are run with: + +```bash +bundle exec rails jasmine:run +# or +npx jasmine-browser-runner runSpecs +``` + +To view the test output in the browser the server needs to be run before the tests: + +```bash +npx jasmine-browser-runner serve +``` + +and then view the output at http://localhost:8888 + +In particular, this makes it possible to view the output of `console.log`. \ No newline at end of file From 5629cd9bdcc29437c471ee21ebd3d89497822938 Mon Sep 17 00:00:00 2001 From: Joe Haig Date: Fri, 7 Jul 2023 18:04:45 +0100 Subject: [PATCH 2/4] Determination javascript using Gov.UK recommended style ... and without jQuery. --- app/views/layouts/application.html.haml | 2 +- .../shared/_determinations_form.html.haml | 2 +- .../claims/DeterminationCalculator.js | 115 ---------- .../javascripts/modules/determination.mjs | 86 ++++++++ .../modules/determination_spec.mjs | 200 ++++++++++++++++++ app/webpack/packs/application.js | 1 - app/webpack/packs/govuk-frontend.js | 7 + spec/support/jasmine-browser.json | 7 +- 8 files changed, 299 insertions(+), 121 deletions(-) delete mode 100644 app/webpack/javascripts/modules/case_worker/claims/DeterminationCalculator.js create mode 100644 app/webpack/javascripts/modules/determination.mjs create mode 100644 app/webpack/javascripts/modules/determination_spec.mjs diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 283fdba8dd..97258eb10d 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -25,7 +25,7 @@ %script != ga_outlet - %body{ class: "govuk-template__body controller-#{controller.controller_name}" } + %body{ class: "govuk-template__body controller-#{controller.controller_name} govuk-frontend-supported" } :javascript document.body.className = ((document.body.className) ? document.body.className + ' js-enabled' : 'js-enabled'); diff --git a/app/views/shared/_determinations_form.html.haml b/app/views/shared/_determinations_form.html.haml index 3883666350..7afe393ae7 100644 --- a/app/views/shared/_determinations_form.html.haml +++ b/app/views/shared/_determinations_form.html.haml @@ -18,7 +18,7 @@ %div = validation_error_message(@error_presenter, :determinations) - = govuk_table( id: 'determinations', class: 'js-cw-claim-assessment', data: { apply_vat: "#{claim.apply_vat}", vat_url: vat_path(format: :json), submitted_date: claim.vat_date(:db), scheme: claim.agfs? ? 'agfs' : 'lgfs' }) do + = govuk_table( id: 'determinations', class: 'js-cw-claim-assessment', data: { module: 'govuk-determination', apply_vat: "#{claim.apply_vat}", vat_url: vat_path(format: :json), submitted_date: claim.vat_date(:db), scheme: claim.agfs? ? 'agfs' : 'lgfs' }) do = govuk_table_caption(class: 'govuk-visually-hidden') do %h3.govuk-heading-m = t('.assessment_summary') diff --git a/app/webpack/javascripts/modules/case_worker/claims/DeterminationCalculator.js b/app/webpack/javascripts/modules/case_worker/claims/DeterminationCalculator.js deleted file mode 100644 index 736aa65623..0000000000 --- a/app/webpack/javascripts/modules/case_worker/claims/DeterminationCalculator.js +++ /dev/null @@ -1,115 +0,0 @@ -moj.Modules.DeterminationCalculator = { - el: '#determinations', - $determinationsTable: {}, - $totalExclVat: {}, - $totalInclVat: {}, - $totalVat: {}, - ajaxVat: false, - vatUrl: '', - vatDate: '', - - cacheEls: function () { - this.$determinationsTable = $(this.el) - this.$totalExclVat = $('.js-total-exc-vat-determination', this.$determinationsTable) - this.$totalVat = $('.js-vat-determination', this.$determinationsTable) - this.$LgfsVat = $('.js-lgfs-vat-determination', this.$determinationsTable) - this.$totalInclVat = $('.js-total-determination', this.$determinationsTable) - this.scheme = this.$determinationsTable.data('scheme') - this.ajaxVat = this.$determinationsTable.data('applyVat') - this.vatUrl = this.$determinationsTable.data('vatUrl') - this.vatDate = this.$determinationsTable.data('submittedDate') - }, - - init: function () { - this.cacheEls() - - this.addChangeEvent() - - const self = this - - this.$determinationsTable - // Find all the rows - .find('tr') - // that have input fields - .has(':text') - .first() - // Work out the total for each row - .each(function () { - // cache the current row - const $tr = $(this) - const firstInput = $tr.find(':text').get(0) - - // Calculate the rows total. - self.calculateTotalRows(firstInput) - }) - }, - - calculateAmount: function (fee, expenses, disbursements) { - let f = fee || 0 - let e = expenses || 0 - let d = disbursements || 0 - - f = f < 0 ? 0 : f - e = e < 0 ? 0 : e - d = d < 0 ? 0 : d - - let t = (f + e + d).toFixed(2) - t = t < 0 ? 0 : t - return t - }, - - addChangeEvent: function () { - const self = this - $(this.el).on('change', ':text', function () { - self.calculateTotalRows(this) - }) - }, - - calculateTotalRows: function (element) { - // Cache the element that triggered the event - const $element = $(element) - const $table = $element.closest('table') - const $fees = $table.find('.js-fees') - const $expenses = $table.find('.js-expenses') - const $disbursements = $table.find('.js-disbursements') - - const fees = $fees.exists() ? parseFloat($fees.val().replace(/,/g, '')) : 0 - const expenses = $expenses.exists() ? parseFloat($expenses.val().replace(/,/g, '')) : 0 - const disbursements = $disbursements.exists() ? parseFloat($disbursements.val().replace(/,/g, '')) : 0 - - const total = this.calculateAmount(fees, expenses, disbursements) - - this.applyVAT(total) - }, - - getVAT: function (netAmount) { - return $.ajax({ - url: this.vatUrl, - data: { - scheme: this.scheme, - lgfs_vat_amount: this.$LgfsVat.val(), - date: this.vatDate, - apply_vat: this.ajaxVat, - net_amount: netAmount - } - }) - }, - - applyVAT: function (netAmount) { - const self = this - - // If Total is not a valid number - if (isNaN(netAmount)) { - self.$totalExclVat.text('£0.00') - self.$totalVat.text('£0.00') - self.$totalExclVat.text('£0.00') - } else { - $.when(this.getVAT(netAmount)) - .then(function (data) { - self.$totalExclVat.text(data.net_amount) - self.$totalVat.text(data.vat_amount) - self.$totalInclVat.text(data.total_inc_vat) - }) - } - } -} diff --git a/app/webpack/javascripts/modules/determination.mjs b/app/webpack/javascripts/modules/determination.mjs new file mode 100644 index 0000000000..810f740b8f --- /dev/null +++ b/app/webpack/javascripts/modules/determination.mjs @@ -0,0 +1,86 @@ +/** + * Determination + * + * Calculate totals for the assessment summary on case_workers/claims/ + * + * + *
+ * + * ... + * + * ... + * + * + * + * + * + * + * + * £0.00 + *
+ */ + +export class Determination { + /** + * @param {Element} $module - HTML element to use for component + */ + constructor ($module) { + if (($module instanceof window.HTMLElement) && document.body.classList.contains('govuk-frontend-supported')) { + this.$module = $module + } + } + + /** + * Initialise component + */ + init () { + if (!this.$module) { return } + + const $module = this.$module + + this.$totalExclVat = $module.querySelector('.js-total-exc-vat-determination') + this.$totalVat = $module.querySelector('.js-vat-determination') + this.$LgfsVat = $module.querySelector('.js-lgfs-vat-determination') + this.$totalInclVat = $module.querySelector('.js-total-determination') + this.scheme = $module.dataset.scheme + // Should this be this.applyVat? + this.ajaxVat = $module.dataset.applyVat + this.vatUrl = $module.dataset.vatUrl + this.vatDate = $module.dataset.submittedDate + + const fieldIds = [ + 'fees', + 'expenses', + 'disbursements', + 'vat_amount' + ] + + this.fields = fieldIds.map(id => document.querySelector(`#claim_assessment_attributes_${id}`)).filter(field => field) + this.fields.forEach(element => element.addEventListener('change', () => { + this.calculateTotalRows() + return true + })) + } + + calculateTotalRows () { + const total = this.fields.reduce((n, field) => n + (parseFloat(field?.value) || 0.0), 0).toFixed(2) + return this.applyVat(total).then(data => { + this.$totalExclVat.innerHTML = data.net_amount + if (this.$totalVat) { this.$totalVat.innerHTML = data.vat_amount } + this.$totalInclVat.innerHTML = data.total_inc_vat + }) + } + + async applyVat (netAmount) { + const params = new URLSearchParams({ + scheme: this.scheme, + lgfs_vat_amount: this.$LgfsVat?.value, + date: this.vatDate, + apply_vat: this.ajaxVat, + net_amount: netAmount + }) + const response = await fetch(this.vatUrl + '?' + params.toString()) + + return await response.json() + } +} diff --git a/app/webpack/javascripts/modules/determination_spec.mjs b/app/webpack/javascripts/modules/determination_spec.mjs new file mode 100644 index 0000000000..0281e755a2 --- /dev/null +++ b/app/webpack/javascripts/modules/determination_spec.mjs @@ -0,0 +1,200 @@ +import { Determination } from './determination.mjs' + +describe('Determination', () => { + let determination = null + let fetchSpy = null + + describe('calculateTotalRows', () => { + const inputRow = (id, klass) => { + const row = document.createElement('tr') + const cell = document.createElement('td') + row.append(cell) + const input = document.createElement('input') + input.type = 'text' + input.value = 0.0 + input.id = id + if (klass) { input.classList.add(klass) } + cell.append(input) + return row + } + + const attributeRow = (id) => { + const row = document.createElement('tr') + const cell = document.createElement('td') + cell.classList.add(id) + cell.innerHTML = 0.0 + row.append(cell) + return row + } + + describe('AGFS claim', () => { + beforeEach(() => { + document.body.classList.add('govuk-frontend-supported') + + const testArea = document.createElement('div') + testArea.classList.add('test-area') + document.body.appendChild(testArea) + + const table = document.createElement('table') + table.id = 'determinations' + table.setAttribute('data-module', 'govuk-determination') + table.setAttribute('data-apply-vat', 'true') + table.setAttribute('data-vat-url', '/vat.json') + table.setAttribute('data-submitted-date', '2023-07-18') + table.setAttribute('data-scheme', 'agfs') + + const tableBody = document.createElement('thead') + tableBody.appendChild(inputRow('claim_assessment_attributes_fees')) + tableBody.appendChild(inputRow('claim_assessment_attributes_expenses')) + tableBody.appendChild(attributeRow('js-total-exc-vat-determination')) + tableBody.appendChild(attributeRow('js-vat-determination')) + tableBody.appendChild(attributeRow('js-total-determination')) + + table.appendChild(tableBody) + testArea.appendChild(table) + + console.log(testArea) + determination = new Determination(table) + determination.init() + + fetchSpy = spyOn(window, 'fetch').and.resolveTo( + new Response( + JSON.stringify({ + net_amount: '£196.50', + vat_amount: '£39.30', + total_inc_vat: '£235.80' + }), + { status: 200, statusText: 'OK' } + ) + ) + }) + + afterEach(() => { + document.body.classList.remove('govuk-frontend-supported') + document.querySelector('.test-area').remove() + }) + + it('makes a request to the API', () => { + document.querySelector('#claim_assessment_attributes_fees').value = 3.14 + document.querySelector('#claim_assessment_attributes_expenses').value = 2.72 + + return determination.calculateTotalRows().then(() => { + const searchParams = new URLSearchParams() + searchParams.set('scheme', 'agfs') + searchParams.set('lgfs_vat_amount', 'undefined') + searchParams.set('date', '2023-07-18') + searchParams.set('apply_vat', 'true') + searchParams.set('net_amount', '5.86') + + expect(fetchSpy).toHaveBeenCalledWith('/vat.json?' + searchParams) + }) + }) + + it('sets the net amount', () => { + return determination.calculateTotalRows().then(() => { + const netAmount = document.querySelector('.js-total-exc-vat-determination') + + expect(netAmount.innerHTML).toEqual('£196.50') + }) + }) + + it('sets the vat amount', () => { + return determination.calculateTotalRows().then(() => { + const netAmount = document.querySelector('.js-vat-determination') + + expect(netAmount.innerHTML).toEqual('£39.30') + }) + }) + + it('sets the total amount', () => { + return determination.calculateTotalRows().then(() => { + const totalAmount = document.querySelector('.js-total-determination') + + expect(totalAmount.innerHTML).toEqual('£235.80') + }) + }) + }) + + describe('LGFS claim', () => { + beforeEach(() => { + document.body.classList.add('govuk-frontend-supported') + + const testArea = document.createElement('div') + testArea.classList.add('test-area') + document.body.appendChild(testArea) + + const table = document.createElement('table') + table.id = 'determinations' + table.setAttribute('data-module', 'govuk-determination') + table.setAttribute('data-apply-vat', 'true') + table.setAttribute('data-vat-url', '/vat.json') + table.setAttribute('data-submitted-date', '2023-07-18') + table.setAttribute('data-scheme', 'lgfs') + + const tableBody = document.createElement('thead') + tableBody.appendChild(inputRow('claim_assessment_attributes_fees')) + tableBody.appendChild(inputRow('claim_assessment_attributes_expenses')) + tableBody.appendChild(inputRow('claim_assessment_attributes_disbursements')) + tableBody.appendChild(attributeRow('js-total-exc-vat-determination')) + tableBody.appendChild(inputRow('claim_assessment_attributes_vat_amount', 'js-lgfs-vat-determination')) + tableBody.appendChild(attributeRow('js-total-determination')) + + table.appendChild(tableBody) + testArea.appendChild(table) + + console.log(testArea) + determination = new Determination(table) + determination.init() + + fetchSpy = spyOn(window, 'fetch').and.resolveTo( + new Response( + JSON.stringify({ + net_amount: '£196.50', + vat_amount: '£39.30', + total_inc_vat: '£235.80' + }), + { status: 200, statusText: 'OK' } + ) + ) + }) + + afterEach(() => { + document.body.classList.remove('govuk-frontend-supported') + document.querySelector('.test-area').remove() + }) + + it('makes a request to the API', () => { + document.querySelector('#claim_assessment_attributes_fees').value = 3.14 + document.querySelector('#claim_assessment_attributes_vat_amount').value = 1.17 + document.querySelector('#claim_assessment_attributes_expenses').value = 2.72 + + return determination.calculateTotalRows().then(() => { + const searchParams = new URLSearchParams() + searchParams.set('scheme', 'lgfs') + searchParams.set('lgfs_vat_amount', '1.17') + searchParams.set('date', '2023-07-18') + searchParams.set('apply_vat', 'true') + searchParams.set('net_amount', '7.03') + + expect(fetchSpy).toHaveBeenCalledWith('/vat.json?' + searchParams) + }) + }) + + it('sets the net amount', () => { + return determination.calculateTotalRows().then(() => { + const netAmount = document.querySelector('.js-total-exc-vat-determination') + + expect(netAmount.innerHTML).toEqual('£196.50') + }) + }) + + it('sets the total amount', () => { + return determination.calculateTotalRows().then(() => { + const totalAmount = document.querySelector('.js-total-determination') + + expect(totalAmount.innerHTML).toEqual('£235.80') + }) + }) + }) + }) +}) diff --git a/app/webpack/packs/application.js b/app/webpack/packs/application.js index e5d28bbdfd..624ef50fe2 100644 --- a/app/webpack/packs/application.js +++ b/app/webpack/packs/application.js @@ -36,7 +36,6 @@ import '../javascripts/modules/Modules.ExpensesDataTable.js' import '../javascripts/modules/Modules.Debounce.js' import '../javascripts/modules/case_worker/admin/Modules.ManagementInformation.js' import '../javascripts/modules/case_worker/Allocation.js' -import '../javascripts/modules/case_worker/claims/DeterminationCalculator.js' import '../javascripts/modules/external_users/claims/FeeFieldsDisplay.js' import '../javascripts/modules/external_users/claims/DisbursementsCtrl.js' import '../javascripts/modules/external_users/claims/FeeTypeCtrl.js' diff --git a/app/webpack/packs/govuk-frontend.js b/app/webpack/packs/govuk-frontend.js index f7c404dfdc..31edeaf33e 100644 --- a/app/webpack/packs/govuk-frontend.js +++ b/app/webpack/packs/govuk-frontend.js @@ -1,6 +1,13 @@ import '../stylesheets/govuk-frontend.scss' +import { Determination } from '../javascripts/modules/determination.mjs' + // fonts & images require.context('govuk-frontend/govuk/assets', true) +const determinations = document.querySelectorAll('[data-module="govuk-determination"]') +determinations.forEach((determination) => { + new Determination(determination).init() +}) + require('govuk-frontend').initAll() diff --git a/spec/support/jasmine-browser.json b/spec/support/jasmine-browser.json index 827f170c8f..34f9b07446 100644 --- a/spec/support/jasmine-browser.json +++ b/spec/support/jasmine-browser.json @@ -3,12 +3,13 @@ "srcFiles": [ "app/assets/builds/*.js" ], - "specDir": "spec/javascripts", + "specDir": "./", "specFiles": [ - "**/*[sS]pec.?(m)js" + "app/webpack/javascripts/modules/**/*[sS]pec.?(m)js", + "spec/javascripts/**/*[sS]pec.?(m)js" ], "helpers": [ - "helpers/**/*.?(m)js" + "spec/javascripts/helpers/**/*.?(m)js" ], "env": { "stopSpecOnExpectationFailure": false, From 1f3813f1dcd375b58ddfbf92082d0e21a8a6b61e Mon Sep 17 00:00:00 2001 From: Joe Haig Date: Mon, 9 Oct 2023 10:00:10 +0100 Subject: [PATCH 3/4] Re-write 'select all' --- .../allocations/_re_allocation.html.haml | 10 ++- .../javascripts/modules/Modules.SelectAll.js | 21 ----- app/webpack/javascripts/modules/selectAll.mjs | 47 ++++++++++ .../javascripts/modules/selectAll_spec.mjs | 86 +++++++++++++++++++ app/webpack/packs/application.js | 1 - app/webpack/packs/govuk-frontend.js | 6 ++ 6 files changed, 147 insertions(+), 24 deletions(-) delete mode 100644 app/webpack/javascripts/modules/Modules.SelectAll.js create mode 100644 app/webpack/javascripts/modules/selectAll.mjs create mode 100644 app/webpack/javascripts/modules/selectAll_spec.mjs diff --git a/app/views/case_workers/admin/allocations/_re_allocation.html.haml b/app/views/case_workers/admin/allocations/_re_allocation.html.haml index 8dec2337e3..22088c2f12 100644 --- a/app/views/case_workers/admin/allocations/_re_allocation.html.haml +++ b/app/views/case_workers/admin/allocations/_re_allocation.html.haml @@ -40,7 +40,13 @@ = govuk_table_thead do = govuk_table_row do = govuk_table_th do - = govuk_link_to t('.select_all'), '#', class: 'select-all', data: { 'all-checked': 'false' }, 'aria-label': t('.select_all_label') + .govuk-form-group{ data: { module: 'govuk-select-all', 'select-all-class': 'select-all', 'collection-class': 'select-all-box' } } + .govuk-checkboxes.govuk-checkboxes--small{ data: { module: 'govuk-checkboxes' } } + .govuk-checkboxes__item + %input.govuk-checkboxes__input.select-all{ type: :checkbox, name: 'select-all', id: 'select-all', selected: false } + %label.govuk-label.govuk-checkboxes__label{ for: 'select-all' } + %span.govuk-visually-hidden + Select all = govuk_table_th do = t('.case_number') = govuk_table_th do @@ -65,7 +71,7 @@ .govuk-form-group.error-message-container .govuk-checkboxes.govuk-checkboxes--small{ 'data-module': 'govuk-checkboxes' } .govuk-checkboxes__item - = b.check_box(class: 'govuk-checkboxes__input') + = b.check_box(class: 'govuk-checkboxes__input select-all-box') = b.label(class: 'govuk-label govuk-checkboxes__label'){ t('.choose_label_html', case_number: claim.case_number) } - claim.injection_error do |message| diff --git a/app/webpack/javascripts/modules/Modules.SelectAll.js b/app/webpack/javascripts/modules/Modules.SelectAll.js deleted file mode 100644 index 418aa97acd..0000000000 --- a/app/webpack/javascripts/modules/Modules.SelectAll.js +++ /dev/null @@ -1,21 +0,0 @@ -moj.Modules.SelectAll = { - init: function () { - $('.select-all').on('click', function () { - const $element = $(this) - const checkedState = $element.data('all-checked') - - $element.data('all-checked', !checkedState) - - $('tr').each(function (idx, el) { - const $el = $(el) - if ($el.find('input').length) { - $el.find('input').prop('checked', !checkedState) - } - }) - - $element.text(checkedState ? 'Select all' : 'Select none') - - return false - }) - } -} diff --git a/app/webpack/javascripts/modules/selectAll.mjs b/app/webpack/javascripts/modules/selectAll.mjs new file mode 100644 index 0000000000..e32591e80c --- /dev/null +++ b/app/webpack/javascripts/modules/selectAll.mjs @@ -0,0 +1,47 @@ +/** + * SelectAll + * + * A checkbox that is used to automatically select and deselect a collection of other checkboxes. + * + * * data-select-all-class defines the class to identify the 'select all' checkbox + * * data-collection-class defines the class to identify the checkboxes in the collection + * + *
+ * + *
+ * + * + * + * + * + * + */ +export class SelectAll { + /** + * @param {Element} $module - HTML element to use for component + */ + constructor ($module) { + if (($module instanceof window.HTMLElement) && document.body.classList.contains('govuk-frontend-supported')) { + this.$module = $module + } + } + + /** + * Initialise component + */ + init () { + // Check that required elements are present + if (!this.$module) { return } + + const selector = this.$module.dataset.selectAllClass + this.collection = this.$module.dataset.collectionClass + + this.selectAllBox = this.$module.querySelector(`.${selector}`) + this.selectAllBox.addEventListener('change', () => this.toggleSelection()) + } + + toggleSelection = () => { + const checkBoxes = document.querySelectorAll(`.${this.collection}`) + checkBoxes.forEach((box) => { box.checked = this.selectAllBox.checked }) + } +} diff --git a/app/webpack/javascripts/modules/selectAll_spec.mjs b/app/webpack/javascripts/modules/selectAll_spec.mjs new file mode 100644 index 0000000000..46d1abc02a --- /dev/null +++ b/app/webpack/javascripts/modules/selectAll_spec.mjs @@ -0,0 +1,86 @@ +import { SelectAll } from './selectAll.mjs' + +describe('SelectAll', () => { + describe('toggleSelection', () => { + let selectAllBox = null + let selectedBox = null + let unselectedBox = null + let testArea = null + + beforeEach(() => { + document.body.classList.add('govuk-frontend-supported') + + testArea = document.createElement('div') + document.body.appendChild(testArea) + + //
+ // + //
+ const selectAllDiv = document.createElement('div') + selectAllDiv.setAttribute('data-module', 'govuk-select-all') + selectAllDiv.setAttribute('data-select-all-class', 'selector-box') + selectAllDiv.setAttribute('data-collection-class', 'pick-me') + selectAllBox = document.createElement('input') + selectAllBox.type = 'checkbox' + selectAllBox.classList.add('selector-box') + selectAllBox.checked = false + selectAllDiv.appendChild(selectAllBox) + testArea.appendChild(selectAllDiv) + + // + selectedBox = createCheckbox('pick-me', true, 'box1') + testArea.appendChild(selectedBox) + + // + unselectedBox = createCheckbox('pick-me', true, 'box1') + testArea.appendChild(unselectedBox) + + const selectAll = new SelectAll(selectAllDiv) + selectAll.init() + }) + + afterEach(() => { + document.body.classList.remove('govuk-frontend-supported') + testArea.remove() + }) + + const createCheckbox = (className, checked = false, name = '') => { + const checkbox = document.createElement('input') + checkbox.type = 'checkbox' + checkbox.classList.add(className) + checkbox.name = name + checkbox.checked = checked + return checkbox + } + + const toggleSelectAll = () => { + const event = new Event('change') + selectAllBox.checked = !selectAllBox.checked + selectAllBox.dispatchEvent(event) + } + + it('should mark all checkboxes as checked', () => { + toggleSelectAll() + + expect(selectedBox.checked).toBeTrue() + expect(unselectedBox.checked).toBeTrue() + }) + + it('should mark all checkboxes as unchecked', () => { + toggleSelectAll() + toggleSelectAll() + + expect(selectedBox.checked).toBeFalse() + expect(unselectedBox.checked).toBeFalse() + }) + + it('should select newly created boxes', () => { + const newBox = createCheckbox('pick-me', false, 'new-box') + document.body.appendChild(newBox) + + toggleSelectAll() + + expect(newBox.checked).toBeTrue() + }) + }) +}) diff --git a/app/webpack/packs/application.js b/app/webpack/packs/application.js index 624ef50fe2..4f76bc6f0f 100644 --- a/app/webpack/packs/application.js +++ b/app/webpack/packs/application.js @@ -62,7 +62,6 @@ import '../javascripts/modules/show-hide-content.js' import '../javascripts/modules/Helpers.FormControls.js' import '../javascripts/modules/Modules.ReAllocationFilterSubmit.js' import '../javascripts/modules/Helpers.API.Distance.js' -import '../javascripts/modules/Modules.SelectAll.js' import '../javascripts/modules/Helpers.API.Core.js' import '../javascripts/modules/Modules.Messaging.js' import '../javascripts/modules/Helpers.DataTables.js' diff --git a/app/webpack/packs/govuk-frontend.js b/app/webpack/packs/govuk-frontend.js index 31edeaf33e..b002b0f7e4 100644 --- a/app/webpack/packs/govuk-frontend.js +++ b/app/webpack/packs/govuk-frontend.js @@ -1,6 +1,7 @@ import '../stylesheets/govuk-frontend.scss' import { Determination } from '../javascripts/modules/determination.mjs' +import { SelectAll } from '../javascripts/modules/selectAll.mjs' // fonts & images require.context('govuk-frontend/govuk/assets', true) @@ -10,4 +11,9 @@ determinations.forEach((determination) => { new Determination(determination).init() }) +const selectAlls = document.querySelectorAll('[data-module="govuk-select-all"]') +selectAlls.forEach((selectAll) => { + new SelectAll(selectAll).init() +}) + require('govuk-frontend').initAll() From ea441840de1c50a03eca1c68d7fd4b0cd54d0047 Mon Sep 17 00:00:00 2001 From: Joe Haig Date: Fri, 20 Oct 2023 21:34:26 +0100 Subject: [PATCH 4/4] Add Response to the globals list For some reason Response is not being recognised even though it is part of Javascript (https://caniuse.com/?search=Response). This is possibly due to out of date dependencies preventing ES6 coding standards being checkd. It is added to the list of globals to allow `yarn standard` to pass. It may be possible to remove this after refactoring. --- docs/javascript_restructure.md | 10 +++++++++- package.json | 5 +++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/javascript_restructure.md b/docs/javascript_restructure.md index c6714a25ac..01bd47e036 100644 --- a/docs/javascript_restructure.md +++ b/docs/javascript_restructure.md @@ -97,4 +97,12 @@ npx jasmine-browser-runner serve and then view the output at http://localhost:8888 -In particular, this makes it possible to view the output of `console.log`. \ No newline at end of file +In particular, this makes it possible to view the output of `console.log`. + +### Linting + +`yarn standard` will complain about the use of `Response` in `app/webpack/javascripts/modules/determination_spec.mjs` +(for example) even though it is part of [ES6.](https://caniuse.com/mdn-api_response_response) +This is due to some dependencies that, it is hoped, will be removed after the +cleanup is completed. `"Response"` is added to the `globals` section of the +`standard` configuration in `package.json`. \ No newline at end of file diff --git a/package.json b/package.json index 2a2e1e9cfe..8495018ff1 100644 --- a/package.json +++ b/package.json @@ -79,11 +79,12 @@ "globals": [ "GOVUK", "moj", - "Stickyfill" + "Stickyfill", + "Response" ], "ignore": [ "app/webpack/javascripts/vendor/", "spec/javascripts/helpers/" ] } -} +} \ No newline at end of file