Skip to content

Commit

Permalink
input-duplicator: migrate to TypeScript
Browse files Browse the repository at this point in the history
  • Loading branch information
liamwhite committed Apr 20, 2024
1 parent 7ba9579 commit 8bce249
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 83 deletions.
91 changes: 91 additions & 0 deletions assets/js/__tests__/input-duplicator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { inputDuplicatorCreator } from '../input-duplicator';
import { assertNotNull } from '../utils/assert';
import { $, $$, removeEl } from '../utils/dom';

describe('Input duplicator functionality', () => {
beforeEach(() => {
document.documentElement.insertAdjacentHTML('beforeend', `<form action="/">
<div class="js-max-input-count">3</div>
<div class="js-input-source">
<input id="0" name="0" class="js-input" type="text"/>
<label>
<a href="#" class="js-remove-input">Delete</a>
</label>
</div>
<div class="js-button-container">
<button type="button" class="js-add-input">Add input</button>
</div>
</form>`);
});

afterEach(() => {
removeEl($$<HTMLFormElement>('form'));
});

function runCreator() {
inputDuplicatorCreator({
addButtonSelector: '.js-add-input',
fieldSelector: '.js-input-source',
maxInputCountSelector: '.js-max-input-count',
removeButtonSelector: '.js-remove-input',
});
}

it('should ignore forms without a duplicator button', () => {
removeEl($$<HTMLButtonElement>('button'));
expect(runCreator()).toBeUndefined();
});

it('should duplicate the input elements', () => {
runCreator();

expect($$('input')).toHaveLength(1);

assertNotNull($<HTMLButtonElement>('.js-add-input')).click();

expect($$('input')).toHaveLength(2);
});

it('should duplicate the input elements when the button is before the inputs', () => {
const form = assertNotNull($<HTMLFormElement>('form'));
const buttonDiv = assertNotNull($<HTMLDivElement>('.js-button-container'));
removeEl(buttonDiv);
form.insertAdjacentElement('afterbegin', buttonDiv);
runCreator();

assertNotNull($<HTMLButtonElement>('.js-add-input')).click();

expect($$('input')).toHaveLength(2);
});

it('should not create more input elements than the limit', () => {
runCreator();

for (let i = 0; i < 5; i += 1) {
assertNotNull($<HTMLButtonElement>('.js-add-input')).click();
}

expect($$('input')).toHaveLength(3);
});

it('should remove duplicated input elements', () => {
runCreator();

assertNotNull($<HTMLButtonElement>('.js-add-input')).click();
assertNotNull($<HTMLAnchorElement>('.js-remove-input')).click();

expect($$('input')).toHaveLength(1);
});

it('should not remove the last input element', () => {
runCreator();

assertNotNull($<HTMLAnchorElement>('.js-remove-input')).click();
assertNotNull($<HTMLAnchorElement>('.js-remove-input')).click();
for (let i = 0; i < 5; i += 1) {
assertNotNull($<HTMLAnchorElement>('.js-remove-input')).click();
}

expect($$('input')).toHaveLength(1);
});
});
83 changes: 0 additions & 83 deletions assets/js/input-duplicator.js

This file was deleted.

76 changes: 76 additions & 0 deletions assets/js/input-duplicator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { assertNotNull } from './utils/assert';
import { $, $$, disableEl, enableEl, removeEl } from './utils/dom';
import { delegate, leftClick } from './utils/events';

export interface InputDuplicatorOptions {
addButtonSelector: string;
fieldSelector: string;
maxInputCountSelector: string;
removeButtonSelector: string;
}

export function inputDuplicatorCreator({
addButtonSelector,
fieldSelector,
maxInputCountSelector,
removeButtonSelector
}: InputDuplicatorOptions) {
const addButton = $<HTMLButtonElement>(addButtonSelector);
if (!addButton) {
return;
}

const form = assertNotNull(addButton.closest('form'));
const fieldRemover = (event: MouseEvent, target: HTMLElement) => {
event.preventDefault();

// Prevent removing the final field element to not "brick" the form
const existingFields = $$(fieldSelector, form);
if (existingFields.length <= 1) {
return;
}

removeEl(assertNotNull(target.closest<HTMLElement>(fieldSelector)));
enableEl(addButton);
};

delegate(form, 'click', {
[removeButtonSelector]: leftClick(fieldRemover)
});


const maxOptionCountElement = assertNotNull($(maxInputCountSelector, form));
const maxOptionCount = parseInt(maxOptionCountElement.innerHTML, 10);

addButton.addEventListener('click', e => {
e.preventDefault();

const existingFields = $$<HTMLElement>(fieldSelector, form);
let existingFieldsLength = existingFields.length;

if (existingFieldsLength < maxOptionCount) {
// The last element matched by the `fieldSelector` will be the last field, make a copy
const prevField = existingFields[existingFieldsLength - 1];
const prevFieldCopy = prevField.cloneNode(true) as HTMLElement;

$$<HTMLInputElement>('input', prevFieldCopy).forEach(prevFieldCopyInput => {
// Reset new input's value
prevFieldCopyInput.value = '';
prevFieldCopyInput.removeAttribute('value');

// Increment sequential attributes of the input
prevFieldCopyInput.setAttribute('name', prevFieldCopyInput.name.replace(/\d+/g, `${existingFieldsLength}`));
prevFieldCopyInput.setAttribute('id', prevFieldCopyInput.id.replace(/\d+/g, `${existingFieldsLength}`));
});

prevField.insertAdjacentElement('afterend', prevFieldCopy);

existingFieldsLength++;
}

// Remove the button if we reached the max number of options
if (existingFieldsLength >= maxOptionCount) {
disableEl(addButton);
}
});
}

0 comments on commit 8bce249

Please sign in to comment.