Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(textarea): textarea form association (#300)
* Add the form association logic to foundation * Update and start writing the field's tests * Form association tests * Allow to add other types of hidden elements * Start implementing the tests * Text area tests * Improve textfield tests * Remove "only" from textarea test * Fixed deps * Remove another "only" * Linting fixes * removing unneded code - to resolve lint issues * fixing some implementation (partial fix) and tests * Test cleanup * Add protection to the API Co-authored-by: Yuri Guller <gullerya@gmail.com>
- Loading branch information
1 parent
82496e9
commit 19ebd4e
Showing
11 changed files
with
476 additions
and
137 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# `vwc-textfield` | ||
|
||
#### `should have internal contents` | ||
|
||
```html | ||
<label class="mdc-text-field mdc-text-field--filled mdc-text-field--no-label"> | ||
<span class="mdc-text-field__ripple"> | ||
</span> | ||
<input | ||
aria-labelledby="label" | ||
class="mdc-text-field__input" | ||
placeholder="" | ||
type="text" | ||
> | ||
<span class="mdc-line-ripple"> | ||
</span> | ||
</label> | ||
|
||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
const types = ['checkbox', 'textarea', 'input']; | ||
export type HiddenInputType = typeof types; | ||
|
||
function getFormByIdOrClosest(element: HTMLElement): HTMLFormElement | null { | ||
const formId = element.getAttribute('form'); | ||
const formElement = formId ? document.getElementById(formId) : element.closest('form'); | ||
return formElement instanceof HTMLFormElement ? formElement : null; | ||
} | ||
|
||
function addHiddenInput(hostingForm: HTMLElement, { name, value }: { name: string, value: string }, hiddenType: HiddenInputType[number]) { | ||
const hiddenInput = document.createElement(hiddenType) as HTMLInputElement; | ||
hiddenInput.style.display = 'none'; | ||
hiddenInput.setAttribute('name', name); | ||
hiddenInput.defaultValue = value; | ||
hostingForm.appendChild(hiddenInput); | ||
|
||
return hiddenInput; | ||
} | ||
|
||
function setValueAndValidity(inputField: HTMLInputElement | undefined, value: string, validationMessage = '') { | ||
if (!inputField) { | ||
return; | ||
} | ||
inputField.value = value; | ||
inputField.setCustomValidity(validationMessage); | ||
} | ||
|
||
export function addInputToForm(inputElement: any, hiddenType: HiddenInputType[number] = 'input'): void { | ||
const hostingForm = getFormByIdOrClosest(inputElement); | ||
|
||
if (!hostingForm || !inputElement) { | ||
return; | ||
} | ||
|
||
inputElement.hiddenInput = addHiddenInput(hostingForm, inputElement, hiddenType); | ||
setValueAndValidity(inputElement.hiddenInput, inputElement.value, inputElement.formElement.validationMessage); | ||
|
||
hostingForm.addEventListener('reset', () => { | ||
inputElement.value = inputElement.formElement.value = inputElement.hiddenInput?.defaultValue ?? ''; | ||
setValueAndValidity(inputElement.hiddenInput, inputElement.value, inputElement.formElement.validationMessage); | ||
}); | ||
|
||
inputElement.hiddenInput.addEventListener('invalid', (event: Event) => { | ||
event.stopPropagation(); | ||
event.preventDefault(); | ||
}); | ||
|
||
inputElement.addEventListener('change', () => { | ||
setValueAndValidity(inputElement.hiddenInput, inputElement.value, inputElement.formElement.validationMessage); | ||
}); | ||
|
||
inputElement.addEventListener('input', () => { | ||
setValueAndValidity(inputElement.hiddenInput, inputElement.value, inputElement.formElement.validationMessage); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
import { addInputToForm } from '../form-association'; | ||
import { textToDomToParent, randomAlpha } from '../../../test/test-helpers'; | ||
|
||
describe(`Form Association Foundation`, function () { | ||
let addedElements = []; | ||
|
||
afterEach(function () { | ||
addedElements.forEach(elm => elm.remove()); | ||
}); | ||
|
||
describe(`addInputToForm`, function () { | ||
let originalSetCustomValidity; | ||
beforeEach(function() { | ||
originalSetCustomValidity = HTMLElement.prototype.setCustomValidity; | ||
HTMLElement.prototype.setCustomValidity = function () { | ||
return 5; | ||
}; | ||
}); | ||
|
||
afterEach(() => { | ||
HTMLElement.prototype.setCustomValidity = originalSetCustomValidity; | ||
}); | ||
|
||
it(`should attach a hidden input to form with given name`, function () { | ||
const fieldName = 'fieldName'; | ||
addedElements = textToDomToParent(`<form><div><input></input></div></form>`); | ||
const formElement = addedElements[0]; | ||
const inputElementWrapper = formElement.children[0]; | ||
inputElementWrapper.name = fieldName; | ||
inputElementWrapper.formElement = inputElementWrapper.querySelector('input'); | ||
|
||
const numberOfNamedInputsBefore = formElement.querySelectorAll(`input[name="${fieldName}]"`).length; | ||
|
||
addInputToForm(inputElementWrapper); | ||
expect(numberOfNamedInputsBefore).to.equal(0); | ||
expect(formElement.querySelectorAll(`input[name="${fieldName}"]`).length).to.equal(1); | ||
}); | ||
|
||
it(`should attach a hidden input to form with given id`, function () { | ||
const otherFormId = randomAlpha(); | ||
addedElements = textToDomToParent(` | ||
<form><div><input></input></div></form> | ||
<form id="${otherFormId}"></form> | ||
`); | ||
const formElement = addedElements[0]; | ||
const otherForm = addedElements[1]; | ||
|
||
const inputElementWrapper = formElement.children[0]; | ||
inputElementWrapper.setAttribute('form', otherFormId); | ||
inputElementWrapper.formElement = inputElementWrapper.querySelector('input'); | ||
|
||
addInputToForm(inputElementWrapper); | ||
|
||
expect(formElement.querySelectorAll('input').length).to.equal(1); | ||
expect(otherForm.querySelectorAll('input').length).to.equal(1); | ||
}); | ||
|
||
it(`should attach to no form if given form id is not found`, function () { | ||
const otherFormId = randomAlpha(); | ||
const fieldName = 'fieldName'; | ||
addedElements = textToDomToParent(`<form><div><input></input></div></form><form id="${otherFormId}"></form>`); | ||
const formElement = addedElements[0]; | ||
const inputElementWrapper = formElement.children[0]; | ||
inputElementWrapper.setAttribute('form', 'someOtherFormId'); | ||
inputElementWrapper.name = fieldName; | ||
|
||
inputElementWrapper.formElement = inputElementWrapper.querySelector('input'); | ||
|
||
addInputToForm(inputElementWrapper); | ||
|
||
expect(document.querySelectorAll(`input[name="${fieldName}"]`).length).to.equal(0); | ||
}); | ||
|
||
it(`should reset value of the internal input, the wrapper and the hidden input on form reset`, function () { | ||
const otherFormId = randomAlpha(); | ||
const defaultValue = 'defaultValue'; | ||
const fieldName = 'fieldName'; | ||
|
||
addedElements = textToDomToParent(`<form><div><input></input></div></form><form id="${otherFormId}"></form>`); | ||
const formElement = addedElements[0]; | ||
const otherForm = addedElements[1]; | ||
|
||
const inputElementWrapper = formElement.children[0]; | ||
inputElementWrapper.setAttribute('form', otherFormId); | ||
inputElementWrapper.value = defaultValue; | ||
inputElementWrapper.name = fieldName; | ||
inputElementWrapper.formElement = inputElementWrapper.querySelector('input'); | ||
|
||
addInputToForm(inputElementWrapper); | ||
|
||
const hiddenInput = document.querySelector(`input[name="${fieldName}"]`); | ||
|
||
otherForm.reset(); | ||
|
||
expect(hiddenInput.value).to.equal(defaultValue); | ||
expect(inputElementWrapper.formElement.value).to.equal(defaultValue) | ||
expect(inputElementWrapper.value).to.equal(defaultValue) | ||
}); | ||
|
||
it(`should set the validity and value of the hidden input according to the internal input`, function () { | ||
const otherFormId = randomAlpha(); | ||
const validValue = 'defaultValue'; | ||
const invalidValue = 'defaultValue'; | ||
const fieldName = 'fieldName'; | ||
|
||
addedElements = textToDomToParent(`<form><div required><input required></input></div></form><form id="${otherFormId}"></form>`); | ||
const formElement = addedElements[0]; | ||
|
||
const inputElementWrapper = formElement.children[0]; | ||
inputElementWrapper.form = otherFormId; | ||
inputElementWrapper.formElement = inputElementWrapper.querySelector('input'); | ||
inputElementWrapper.name = fieldName; | ||
|
||
addInputToForm(inputElementWrapper); | ||
|
||
const hiddenInput = document.querySelector(`input[name="${fieldName}"]`); | ||
|
||
const values = [validValue, invalidValue]; | ||
const events = ['input', 'change']; | ||
|
||
events.forEach(eventName => { | ||
const inputEvent = new Event(eventName); | ||
values.forEach(inputValue => { | ||
inputElementWrapper.value = inputElementWrapper.formElement.value = inputValue; | ||
inputElementWrapper.dispatchEvent(inputEvent); | ||
expect(hiddenInput.value, `${eventName} was unable to match values`).to.equal(inputElementWrapper.formElement.value); | ||
expect(hiddenInput.validationMessage, `${eventName} was unable to match validation messages`).to.equal(inputElementWrapper.formElement.validationMessage); | ||
}); | ||
}); | ||
}); | ||
|
||
it(`should add custom hidden element`, function () { | ||
const hiddenElementType = 'DIGGERING'; | ||
const fieldName = 'inputName'; | ||
|
||
addedElements = textToDomToParent(`<form><div><input></input></div></form>`); | ||
const formElement = addedElements[0]; | ||
|
||
const inputElementWrapper = formElement.children[0]; | ||
inputElementWrapper.formElement = inputElementWrapper.querySelector('input'); | ||
inputElementWrapper.name = fieldName; | ||
|
||
addInputToForm(inputElementWrapper, hiddenElementType); | ||
|
||
expect(formElement.querySelector(`[name="${fieldName}"]`).tagName).to.equal(hiddenElementType); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.