Skip to content

Commit

Permalink
Introduce "add another" component
Browse files Browse the repository at this point in the history
This is based on the MoJ component and existing component in Whitehall. It is to be used when users need to add similar information a couple of times, such as several featured links for an organisation.

An empty field and destroy checkboxes for the existing fields are required and displayed to the user when javascript is disabled in keeping with rails conventions.

When Javascript is enabled, an "add another" button is introduced to allow users to add copies of the empty field and the checkboxes replaced with "Delete" buttons which hide the fields and checks the appropriate checkbox.
  • Loading branch information
ryanb-gds authored and dnkrj committed Nov 26, 2024
1 parent f3b4be4 commit 85cb35d
Show file tree
Hide file tree
Showing 8 changed files with 476 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
window.GOVUK = window.GOVUK || {}
window.GOVUK.Modules = window.GOVUK.Modules || {};

(function (Modules) {
function AddAnother (module) {
this.module = module
this.emptyFieldset = undefined
this.addAnotherButton = undefined
}

function createButton (textContent, additionalClass = '') {
var button = document.createElement('button')
button.className = 'gem-c-button govuk-button ' + additionalClass
button.type = 'button'
button.textContent = textContent
return button
}

AddAnother.prototype.init = function () {
this.createAddAnotherButton()
this.createRemoveButtons()
this.removeEmptyFieldset()
this.updateFieldsetsAndButtons()
}

AddAnother.prototype.createAddAnotherButton = function () {
this.addAnotherButton =
createButton(
this.module.dataset.addButtonText,
'js-add-another__add-button govuk-button--secondary'
)
this.addAnotherButton.addEventListener('click', this.addNewFieldset.bind(this))
this.module.appendChild(this.addAnotherButton)
}

AddAnother.prototype.createRemoveButton = function (fieldset, removeFunction) {
var removeButton =
createButton(
'Delete',
'js-add-another__remove-button gem-c-add-another__remove-button govuk-button--warning'
)
removeButton.addEventListener('click', function (event) {
removeFunction(event)
this.updateFieldsetsAndButtons()
this.addAnotherButton.focus()
}.bind(this))
fieldset.appendChild(removeButton)
}

AddAnother.prototype.createRemoveButtons = function () {
var fieldsets =
document.querySelectorAll('.js-add-another__fieldset')
fieldsets.forEach(function (fieldset) {
this.createRemoveButton(fieldset, this.removeExistingFieldset.bind(this))
fieldset.querySelector('.js-add-another__destroy-checkbox').hidden = true
}.bind(this))
}

AddAnother.prototype.removeEmptyFieldset = function () {
this.emptyFieldset = this.module.querySelector('.js-add-another__empty')
this.emptyFieldset.remove()
}

AddAnother.prototype.updateFieldsetsAndButtons = function () {
this.module.querySelectorAll('.js-add-another__fieldset:not([hidden]) legend')
.forEach(function (legend, index) {
legend.textContent = this.module.dataset.fieldsetLegend + ' ' + (index + 1)
}.bind(this))

this.module.querySelector('.js-add-another__remove-button').classList.toggle(
'js-add-another__remove-button--hidden',
this.module.querySelectorAll('.js-add-another__fieldset:not([hidden])').length === 1
)
}

AddAnother.prototype.addNewFieldset = function (event) {
var button = event.target
var newFieldset = this.emptyFieldset.cloneNode(true)
newFieldset.classList.remove('js-add-another__empty')
newFieldset.classList.add('js-add-another__fieldset')
this.createRemoveButton(newFieldset, this.removeNewFieldset.bind(this))
button.before(newFieldset)

this.incrementAttributes(this.emptyFieldset)
this.updateFieldsetsAndButtons()

// Move focus to first visible field in new set
newFieldset
.querySelector('input:not([type="hidden"]), select, textarea')
.focus()
}

AddAnother.prototype.removeExistingFieldset = function (event) {
var fieldset = event.target.parentNode
var destroyCheckbox =
fieldset.querySelector('.js-add-another__destroy-checkbox input')

destroyCheckbox.checked = true
fieldset.hidden = true
}

AddAnother.prototype.removeNewFieldset = function (event) {
var fieldset = event.target.parentNode
fieldset.remove()
}

// Set attribute values for id, for and name of supplied fieldset
AddAnother.prototype.incrementAttributes = function (fieldset) {
var matcher = /(.*[_[])([0-9]+)([_\]].*?)$/
fieldset
.querySelectorAll('label, input, select, textarea')
.forEach(function (element) {
['name', 'id', 'for'].forEach(function (attribute) {
var value = element.getAttribute(attribute)
var matched = matcher.exec(value)
if (!matched) return
var index = parseInt(matched[2], 10) + 1
element.setAttribute(attribute, matched[1] + index + matched[3])
})
})
}

Modules.AddAnother = AddAnother
})(window.GOVUK.Modules)
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
// components
@import "components/accordion";
@import "components/action-link";
@import "components/add-another";
@import "components/attachment";
@import "components/attachment-link";
@import "components/back-link";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@import "govuk_publishing_components/individual_component_support";
@import "govuk/components/button/button";
@import "govuk/components/fieldset/fieldset";

.gem-c-add-another__remove-button {
margin-top: govuk-spacing(6);
margin-bottom: 0;
}

.js-add-another__remove-button--hidden {
display: none;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<%
add_gem_component_stylesheet("add-another")
items ||= []
empty ||= ""
fieldset_legend ||= ""
add_button_text ||= "Add another"
%>

<div data-module="add-another" class="gem-c-add-another" data-add-button-text="<%= add_button_text %>" data-fieldset-legend="<%= fieldset_legend %>">
<% items.each_with_index do |item, index| %>
<%= render "govuk_publishing_components/components/fieldset", {
classes: "js-add-another__fieldset",
legend_text: "#{fieldset_legend} #{index + 1}",
heading_size: "m"
} do %>
<div class="js-add-another__destroy-checkbox">
<%= item[:destroy_checkbox] %>
</div>
<%= item[:fields] %>
<% end %>
<% end %>
<%= render "govuk_publishing_components/components/fieldset", {
classes: "js-add-another__empty",
legend_text: "#{fieldset_legend} #{items.length + 1}",
heading_size: "m"
} do %>
<%= empty %>
<% end %>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Add another (experimental)
description: The "add another" component lets users input multiple values for a set of form fields.
body: |
This component is currently experimental because more research is needed to validate it.
Applications using this component must include a deletion checkbox in the rendered repeating items so that users can remove
items from the list in the event that Javascript is not enabled. See the examples below for how to do this.
accessibility_criteria: |
The form controls within the fieldsets must be fully accessible as per the design system guidance for each of the form
control components.
uses_component_wrapper_helper: false
govuk_frontend_components:
- button
examples:
default:
data:
fieldset_legend: "Person"
add_button_text: "Add another person"
items:
- fields: >
<div class="govuk-form-group">
<label for="person_0_name" class="gem-c-label govuk-label">Full name</label>
<input class="gem-c-input govuk-input" id="person_0_name" name="person[0]name">
</div>
destroy_checkbox: >
<div class="govuk-checkboxes" data-module="govuk-checkboxes" data-govuk-checkboxes-init="">
<div class="govuk-checkboxes__item">
<input type="checkbox" name="person[0][_destroy]" id="person_0__destroy" class="govuk-checkboxes__input">
<label for="person_0__destroy" class="govuk-label govuk-checkboxes__label">Delete</label>
</div>
</div>
empty:
<div class="govuk-form-group">
<label for="person_1_name" class="gem-c-label govuk-label">Full name</label>
<input class="gem-c-input govuk-input" id="person_1_name" name="person[1]name">
</div>
48 changes: 48 additions & 0 deletions spec/components/add_another_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
require "rails_helper"

describe "Add another", type: :view do
def component_name
"add_another"
end

def default_items
[
{
fields: sanitize("<div class=\"item1\">item1</div>"),
destroy_checkbox: sanitize("<input type=\"checkbox\" />"),
},
{
fields: sanitize("<div class=\"item2\">item2</div>"),
destroy_checkbox: sanitize("<input type=\"checkbox\" />"),
},
]
end

it "renders a wrapper element" do
render_component(items: default_items)

assert_select "div.gem-c-add-another[data-module='add-another']"
end

it "renders the items provided" do
empty = ""
render_component({ items: default_items, empty: })

assert_select "div.js-add-another__fieldset .item1"
assert_select "div.js-add-another__fieldset .item2"
end

it "renders a destroy checkbox for each item" do
empty = ""
render_component({ items: default_items, empty: })

assert_select "div.js-add-another__fieldset .js-add-another__destroy-checkbox", count: 2
end

it "renders the empty item" do
empty = sanitize("<div class=\"empty\">empty</div>")
render_component({ items: default_items, empty: })

assert_select ".js-add-another__empty div.empty"
end
end
Loading

0 comments on commit 85cb35d

Please sign in to comment.