From 19ebd4e5f694738d2687f2a9d2a24ef2fa9beb4b Mon Sep 17 00:00:00 2001 From: Yonatan Kra Date: Tue, 8 Sep 2020 15:29:51 +0300 Subject: [PATCH] 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 --- __snapshots__/vwc-textfield.md | 20 ++ common/foundation/src/form-association.ts | 55 +++++ .../foundation/test/form-association.test.js | 148 +++++++++++ components/select/package.json | 2 +- components/select/src/vwc-select.ts | 59 +---- components/textarea/package.json | 1 + components/textarea/src/vwc-textarea.ts | 7 +- components/textarea/test/textarea.test.js | 232 +++++++++++++++++- components/textfield/package.json | 1 + components/textfield/src/vwc-textfield.ts | 57 +---- components/textfield/test/textfield.test.js | 31 ++- 11 files changed, 476 insertions(+), 137 deletions(-) create mode 100644 __snapshots__/vwc-textfield.md create mode 100644 common/foundation/src/form-association.ts create mode 100644 common/foundation/test/form-association.test.js diff --git a/__snapshots__/vwc-textfield.md b/__snapshots__/vwc-textfield.md new file mode 100644 index 000000000..b3a47fd62 --- /dev/null +++ b/__snapshots__/vwc-textfield.md @@ -0,0 +1,20 @@ +# `vwc-textfield` + +#### `should have internal contents` + +```html + + +``` + diff --git a/common/foundation/src/form-association.ts b/common/foundation/src/form-association.ts new file mode 100644 index 000000000..1bfcec840 --- /dev/null +++ b/common/foundation/src/form-association.ts @@ -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); + }); +} \ No newline at end of file diff --git a/common/foundation/test/form-association.test.js b/common/foundation/test/form-association.test.js new file mode 100644 index 000000000..c500b477e --- /dev/null +++ b/common/foundation/test/form-association.test.js @@ -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(`
`); + 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(` +
+
+ `); + 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(`
`); + 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(`
`); + 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(`
`); + 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(`
`); + 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); + }); + }); +}); \ No newline at end of file diff --git a/components/select/package.json b/components/select/package.json index b66535383..c1f7dac18 100644 --- a/components/select/package.json +++ b/components/select/package.json @@ -26,6 +26,7 @@ }, "dependencies": { "@material/mwc-select": "^0.18.0", + "@vonage/vvd-foundation": "^0.5.0", "@vonage/vvd-style-coupling": "^0.5.0", "@vonage/vwc-icon": "^0.5.0", "@vonage/vwc-notched-outline": "^0.5.0", @@ -33,7 +34,6 @@ "tslib": "^2.0.1" }, "devDependencies": { - "@vonage/vvd-foundation": "^0.5.0", "@vonage/vvd-typography": "^0.5.0" } } \ No newline at end of file diff --git a/components/select/src/vwc-select.ts b/components/select/src/vwc-select.ts index 17c8349b1..e01a100ef 100644 --- a/components/select/src/vwc-select.ts +++ b/components/select/src/vwc-select.ts @@ -5,6 +5,7 @@ import { Select as MWCSelect } from '@material/mwc-select'; import { style as styleCoupling } from '@vonage/vvd-style-coupling/vvd-style-coupling.css.js'; import { style as vwcSelectStyle } from './vwc-select.css'; import { style as mwcSelectStyle } from '@material/mwc-select/mwc-select-css.js'; +import { addInputToForm } from '@vonage/vvd-foundation/form-association'; declare global { interface HTMLElementTagNameMap { @@ -16,32 +17,6 @@ declare global { // @ts-ignore MWCSelect.styles = [styleCoupling, mwcSelectStyle, vwcSelectStyle]; -function getFormByIdOrClosest(element: VWCSelect): HTMLFormElement | null { - const formId = element.form; - const formElement = formId ? document.getElementById(formId) : element.closest('form'); - return formElement instanceof HTMLFormElement ? formElement : null; -} - -function addHiddenInput(hostingForm: HTMLElement, { name, value }: { name: string | undefined, value: string }) { - const hiddenInput = document.createElement('input'); - hiddenInput.style.display = 'none'; - if (name !== undefined) { - 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); -} - /** * This component is an extension of [](https://github.com/material-components/material-components-web-components/tree/master/packages/select) */ @@ -60,7 +35,7 @@ export class VWCSelect extends MWCSelect { await super.firstUpdated(); this.shadowRoot?.querySelector('.mdc-notched-outline')?.shadowRoot?.querySelector('.mdc-notched-outline')?.classList.add('vvd-notch'); this.replaceIcon(); - this.addSelectToForm(); + addInputToForm(this); } private replaceIcon(): void { @@ -70,34 +45,4 @@ export class VWCSelect extends MWCSelect { chevronIcon.setAttribute('type', 'down'); this.shadowRoot?.querySelector(`.${ddIconClass}`)?.replaceWith(chevronIcon); } - - protected addSelectToForm(): void { - const hostingForm = getFormByIdOrClosest(this); - - if (!hostingForm) { - return; - } - - this.hiddenInput = addHiddenInput(hostingForm, this); - setValueAndValidity(this.hiddenInput, this.value, this.formElement.validationMessage); - - hostingForm.addEventListener('reset', () => { - this.value = this.formElement.value = this.hiddenInput?.defaultValue ?? ''; - setValueAndValidity(this.hiddenInput, this.value, this.formElement.validationMessage); - }); - - this.hiddenInput.addEventListener('invalid', (event) => { - event.stopPropagation(); - event.preventDefault(); - }); - - this.addEventListener('change', () => { - setValueAndValidity(this.hiddenInput, this.value, this.formElement.validationMessage); - }); - - this.addEventListener('input', () => { - setValueAndValidity(this.hiddenInput, this.value, this.formElement.validationMessage); - }); - - } } diff --git a/components/textarea/package.json b/components/textarea/package.json index a0389e222..502299296 100644 --- a/components/textarea/package.json +++ b/components/textarea/package.json @@ -25,6 +25,7 @@ "dependencies": { "@material/mwc-textarea": "^0.18.0", "@material/mwc-textfield": "^0.18.0", + "@vonage/vvd-foundation": "^0.5.0", "@vonage/vvd-style-coupling": "^0.5.0", "@vonage/vwc-notched-outline": "^0.5.0", "lit-element": "^2.4.0", diff --git a/components/textarea/src/vwc-textarea.ts b/components/textarea/src/vwc-textarea.ts index de7ae5c1d..644cb1dd8 100644 --- a/components/textarea/src/vwc-textarea.ts +++ b/components/textarea/src/vwc-textarea.ts @@ -1,9 +1,10 @@ -import { customElement } from 'lit-element'; +import { customElement, property } from 'lit-element'; import '@vonage/vwc-notched-outline'; import { TextArea as MWCTextArea } from '@material/mwc-textarea'; import { style as styleCoupling } from '@vonage/vvd-style-coupling/vvd-style-coupling.css.js'; import { style as vwcTextareaStyle } from './vwc-textarea.css'; import { style as mwcTextareaStyle } from '@material/mwc-textarea/mwc-textarea-css.js'; +import { addInputToForm } from '@vonage/vvd-foundation/form-association'; export { TextFieldType } from '@material/mwc-textfield'; @@ -22,8 +23,12 @@ MWCTextArea.styles = [styleCoupling, mwcTextareaStyle, vwcTextareaStyle]; */ @customElement('vwc-textarea') export class VWCTextArea extends MWCTextArea { + @property({ type: String, reflect: true }) + form: string | undefined; + async firstUpdated(): Promise { await super.firstUpdated(); this.shadowRoot?.querySelector('.mdc-notched-outline')?.shadowRoot?.querySelector('.mdc-notched-outline')?.classList.add('vvd-notch'); + addInputToForm(this, 'textarea'); } } diff --git a/components/textarea/test/textarea.test.js b/components/textarea/test/textarea.test.js index 9307e731b..a49a0d5cd 100644 --- a/components/textarea/test/textarea.test.js +++ b/components/textarea/test/textarea.test.js @@ -4,25 +4,243 @@ import { chaiDomDiff } from '@open-wc/semantic-dom-diff'; chai.use(chaiDomDiff); -const VWC_TEXTAREA = 'vwc-textarea'; +const COMPONENT_NAME = 'vwc-textarea'; + +function listenToSubmission(formElement) { + return new Promise(res => { + formElement.addEventListener('submit', () => { + const formData = new FormData(formElement); + res(formData); + }); + }); +} + +async function changeFieldValue(actualElement, value, eventName = 'change') { + actualElement.value = value; + await waitNextTask(); + + let evt = new Event(eventName); + actualElement.dispatchEvent(evt); +} describe('textarea', () => { + let addedElements = []; + + afterEach(() => { + while (addedElements.length) { + addedElements.pop().remove(); + } + }); + it('should be defined as a custom element', async () => { - assert.exists(customElements.get(VWC_TEXTAREA, 'vwc-textarea element is not defined')); + assert.exists(customElements.get(COMPONENT_NAME, 'vwc-textarea element is not defined')); }); it('should have internal contents', async () => { - const addedElements = textToDomToParent(`<${VWC_TEXTAREA}>`); + addedElements = textToDomToParent(`<${COMPONENT_NAME}>`); const actualElement = addedElements[0]; await waitNextTask(); expect(actualElement.shadowRoot.innerHTML).to.equalSnapshot(); }); + describe(`form association`, function() { + + it(`should attach to closest form`, async function() { + const fieldValue = ` + ${Math.random().toString()} + ${Math.random().toString()} + `; + const fieldName = 'test-field'; + addedElements = textToDomToParent(`
<${COMPONENT_NAME} name="${fieldName}" value="${fieldValue}">Button Text
`); + const formElement = addedElements[0]; + await waitNextTask(); + + const submitPromise = listenToSubmission(formElement); + + formElement.requestSubmit(); + + for (let pair of (await submitPromise).entries()) { + expect(pair[0]).to.equal(fieldName); + expect(pair[1].split('\r').join('')).to.equal(fieldValue); + } + + expect(formElement.querySelectorAll(`textarea[name="${fieldName}"`).length).to.equal(1); + }); + + it(`should attach to form when given form id`, async function () { + const fieldValue = Math.random().toString(); + const fieldName = 'test-field'; + const externalFormID = 'externalForm'; + + addedElements = textToDomToParent(` +
+ <${COMPONENT_NAME} name="${fieldName}" value="${fieldValue}" form="${externalFormID}">Button Text + +
+
`); + + await waitNextTask(); + + const formElement = addedElements[0]; + const externalForm = addedElements[1]; + + const submitPromise = listenToSubmission(externalForm); + + externalForm.requestSubmit(); + + for (let pair of (await submitPromise).entries()) { + expect(pair[0]).to.equal(fieldName); + expect(pair[1]).to.equal(fieldValue); + } + + expect(formElement.querySelector(`textarea[name="${fieldName}"`)).to.equal(null); + expect(externalForm.querySelectorAll(`textarea[name="${fieldName}"`).length).to.equal(1); + }); + + it(`should do nothing if form value resolves to a non form element`, async function () { + const fieldValue = Math.random().toString(); + const fieldName = 'test-field'; + const formId = 'testForm'; + addedElements = textToDomToParent(`
<${COMPONENT_NAME} name="${fieldName}" value="${fieldValue}" form="${formId}">Button Text
`); + const formElement = addedElements[0]; + await waitNextTask(); + + expect(formElement.querySelector('textarea')).to.equal(null); + }); + + describe(`value binding`, function () { + + it(`should reset the value of the custom element to default on form reset`, async function () { + const fieldValue = Math.random().toString(); + const fieldName = 'test-field'; + addedElements = textToDomToParent(`
<${COMPONENT_NAME} name="${fieldName}" value="${fieldValue}">Button Text
`); + const formElement = addedElements[0]; + const actualElement = formElement.firstChild; + await waitNextTask(); + actualElement.value = '5'; + await waitNextTask(); + formElement.reset(); + + expect(actualElement.value).to.equal(fieldValue); + }); + + it(`should change the value of the mock input on internal input change`, async function () { + const fieldValue = Math.random().toString(); + const fieldName = 'test-field'; + addedElements = textToDomToParent(`
<${COMPONENT_NAME} name="${fieldName}">Button Text
`); + const formElement = addedElements[0]; + const actualElement = formElement.firstChild; + await waitNextTask(); + + await changeFieldValue(actualElement, fieldValue, 'change'); + + expect(actualElement.hiddenInput.value).to.equal(fieldValue); + }); + }); + + describe(`validation`, function () { + it(`should get validity from the element's validationMessage`, async function () { + const fieldName = 'test-field'; + addedElements = textToDomToParent(`
<${COMPONENT_NAME} required name="${fieldName}">Button Text
`); + const formElement = addedElements[0]; + const actualElement = formElement.firstChild; + await waitNextTask(); + + const invalidity = formElement.checkValidity(); + + await changeFieldValue(actualElement, 'abc', 'input'); + + expect(invalidity).to.equal(false); + expect(formElement.checkValidity()).to.equal(true); + }); + + it(`should validate on reset`, async function () { + const fieldValue = Math.random().toString(); + const fieldName = 'test-field'; + addedElements = textToDomToParent(`
<${COMPONENT_NAME} required value="${fieldValue}" name="${fieldName}">Button Text
`); + const formElement = addedElements[0]; + const actualElement = formElement.firstChild; + await waitNextTask(); + + const validInput = formElement.checkValidity(); + await changeFieldValue(actualElement, '', 'change'); + const invalidInput = formElement.checkValidity(); + + formElement.reset(); + + expect(validInput).to.equal(true); + expect(invalidInput).to.equal(false); + expect(formElement.checkValidity()).to.equal(true); + }); + + it(`should not submit an invalid form`, async function () { + let submitted = false; + const fieldName = 'test-field'; + addedElements = textToDomToParent(`
<${COMPONENT_NAME} required value="val" name="${fieldName}">Button Text
`); + const formElement = addedElements[0]; + const actualElement = formElement.firstChild; + await waitNextTask(); + + const invalidity = formElement.checkValidity(); + + await waitNextTask(); + + formElement.addEventListener('submit', () => { + submitted = true; + }); + + formElement.requestSubmit(); + + const submitValidForm = submitted; + + submitted = false; + + await changeFieldValue(actualElement, '', 'change'); + formElement.requestSubmit(); + + expect(invalidity).to.equal(true); + expect(submitValidForm).to.equal(true); + expect(submitted).to.equal(false); + }); + }); + + it(`should work under multiple shadow layers`, async function () { + const fieldValue = Math.random().toString(); + const fieldName = 'test-field'; + addedElements = textToDomToParent(` +
+ + <${COMPONENT_NAME} required value="${fieldValue}" name="${fieldName}">Button Text + +
`); + const formElement = addedElements[0]; + const actualElement = formElement.children[0].children[0]; + await waitNextTask(); + + const validInput = formElement.checkValidity(); + + const submitPromise = listenToSubmission(formElement); + + formElement.requestSubmit(); + + for (let pair of (await submitPromise).entries()) { + expect(pair[0]).to.equal(fieldName); + expect(pair[1]).to.equal(fieldValue); + } + + await changeFieldValue(actualElement, '', 'change'); + + expect(formElement.querySelectorAll(`textarea[name="${fieldName}"`).length).to.equal(1); + expect(validInput).to.equal(true); + expect(formElement.checkValidity()).to.equal(false); + }); + }); + describe('typography', () => { it('should have set typography for a label', async () => { - const actualElements = textToDomToParent(`<${VWC_TEXTAREA} outlined label="Vwc textarea">`); + addedElements = textToDomToParent(`<${COMPONENT_NAME} outlined label="Vwc textarea">`); await waitNextTask(); - const labelElement = actualElements[0].shadowRoot.querySelector('.mdc-notched-outline').querySelector('#label'); + const labelElement = addedElements[0].shadowRoot.querySelector('.mdc-notched-outline').querySelector('#label'); expect(labelElement).to.exist; assertComputedStyle(labelElement, { fontFamily: 'SpeziaWebVariable', @@ -36,9 +254,9 @@ describe('textarea', () => { }); it('should have set typography for an input', async () => { - const actualElements = textToDomToParent(`<${VWC_TEXTAREA} outlined disabled label="Vwc textarea">`); + addedElements = textToDomToParent(`<${COMPONENT_NAME} outlined disabled label="Vwc textarea">`); await waitNextTask(); - const inputElement = actualElements[0].shadowRoot.querySelector('.mdc-text-field__input'); + const inputElement = addedElements[0].shadowRoot.querySelector('.mdc-text-field__input'); expect(inputElement).to.exist; assertComputedStyle(inputElement, { fontFamily: 'SpeziaWebVariable', diff --git a/components/textfield/package.json b/components/textfield/package.json index 82c5a3053..6b87b942f 100644 --- a/components/textfield/package.json +++ b/components/textfield/package.json @@ -25,6 +25,7 @@ }, "dependencies": { "@material/mwc-textfield": "^0.18.0", + "@vonage/vvd-foundation": "^0.5.0", "@vonage/vvd-style-coupling": "^0.5.0", "@vonage/vwc-notched-outline": "^0.5.0", "lit-element": "^2.4.0", diff --git a/components/textfield/src/vwc-textfield.ts b/components/textfield/src/vwc-textfield.ts index 19f8caab2..eb435fe2e 100644 --- a/components/textfield/src/vwc-textfield.ts +++ b/components/textfield/src/vwc-textfield.ts @@ -4,7 +4,7 @@ import { TextField as MWCTextField } from '@material/mwc-textfield'; import { style as styleCoupling } from '@vonage/vvd-style-coupling/vvd-style-coupling.css.js'; import { style as vwcTextFieldStyle } from './vwc-textfield.css'; import { style as mwcTextFieldStyle } from '@material/mwc-textfield/mwc-textfield-css.js'; - +import { addInputToForm } from '@vonage/vvd-foundation/form-association'; export { TextFieldType } from '@material/mwc-textfield'; declare global { @@ -17,30 +17,6 @@ declare global { // @ts-ignore MWCTextField.styles = [styleCoupling, mwcTextFieldStyle, vwcTextFieldStyle]; -function getFormByIdOrClosest(element: VWCTextField): HTMLFormElement | null { - const formId = element.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 }) { - const hiddenInput = document.createElement('input'); - 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); -} - @customElement('vwc-textfield') export class VWCTextField extends MWCTextField { @property({ type: HTMLInputElement, reflect: false }) @@ -52,35 +28,6 @@ export class VWCTextField extends MWCTextField { async firstUpdated(): Promise { await super.firstUpdated(); this.shadowRoot?.querySelector('.mdc-notched-outline')?.shadowRoot?.querySelector('.mdc-notched-outline')?.classList.add('vvd-notch'); - this.addInputToForm(); - } - - protected addInputToForm(): void { - const hostingForm = getFormByIdOrClosest(this); - - if (!hostingForm) { - return; - } - - this.hiddenInput = addHiddenInput(hostingForm, this); - setValueAndValidity(this.hiddenInput, this.value, this.formElement.validationMessage); - - hostingForm.addEventListener('reset', () => { - this.value = this.formElement.value = this.hiddenInput?.defaultValue ?? ''; - setValueAndValidity(this.hiddenInput, this.value, this.formElement.validationMessage); - }); - - this.hiddenInput.addEventListener('invalid', (event) => { - event.stopPropagation(); - event.preventDefault(); - }); - - this.addEventListener('change', () => { - setValueAndValidity(this.hiddenInput, this.value, this.formElement.validationMessage); - }); - - this.addEventListener('input', () => { - setValueAndValidity(this.hiddenInput, this.value, this.formElement.validationMessage); - }); + addInputToForm(this); } } diff --git a/components/textfield/test/textfield.test.js b/components/textfield/test/textfield.test.js index 64bf53588..456752980 100644 --- a/components/textfield/test/textfield.test.js +++ b/components/textfield/test/textfield.test.js @@ -5,7 +5,7 @@ import { chaiDomDiff } from '@open-wc/semantic-dom-diff'; chai.use(chaiDomDiff); -const VWC_TEXTFIELD = 'vwc-textfield'; +const COMPONENT_NAME = 'vwc-textfield'; function listenToSubmission(formElement) { return new Promise(res => { @@ -34,11 +34,11 @@ describe('textfield', () => { }); it('should be defined as a custom element', async () => { - expect(Boolean(customElements.get(VWC_TEXTFIELD))).to.equal(true); + expect(Boolean(customElements.get(COMPONENT_NAME))).to.equal(true); }); it('should have internal contents', async () => { - addedElements = textToDomToParent(`<${VWC_TEXTFIELD}>`); + addedElements = textToDomToParent(`<${COMPONENT_NAME}>`); const actualElement = addedElements[0]; await waitNextTask(); expect(actualElement.shadowRoot.innerHTML).to.equalSnapshot(); @@ -46,7 +46,7 @@ describe('textfield', () => { describe('typography', () => { it('should have set typography for a label', async () => { - addedElements = textToDomToParent(`<${VWC_TEXTFIELD} outlined label="Vwc textarea">`); + addedElements = textToDomToParent(`<${COMPONENT_NAME} outlined label="Vwc textarea">`); await waitNextTask(); const labelElement = addedElements[0].shadowRoot.querySelector('.mdc-notched-outline').querySelector('#label'); expect(labelElement).to.exist; @@ -62,7 +62,7 @@ describe('textfield', () => { }); it('should have set typography for an input', async () => { - addedElements = textToDomToParent(`<${VWC_TEXTFIELD} outlined disabled label="Vwc textarea">`); + addedElements = textToDomToParent(`<${COMPONENT_NAME} outlined disabled label="Vwc textarea">`); await waitNextTask(); const inputElement = addedElements[0].shadowRoot.querySelector('.mdc-text-field__input'); expect(inputElement).to.exist; @@ -83,7 +83,7 @@ describe('textfield', () => { it(`should attach to closest form`, async function () { const fieldValue = Math.random().toString(); const fieldName = 'test-field'; - addedElements = textToDomToParent(`
<${VWC_TEXTFIELD} name="${fieldName}" value="${fieldValue}">Button Text
`); + addedElements = textToDomToParent(`
<${COMPONENT_NAME} name="${fieldName}" value="${fieldValue}">Button Text
`); const formElement = addedElements[0]; await waitNextTask(); @@ -106,8 +106,8 @@ describe('textfield', () => { addedElements = textToDomToParent(`
- <${VWC_TEXTFIELD} name="${fieldName}" value="${fieldValue}" form="${externalFormID}">Button Text - + <${COMPONENT_NAME} name="${fieldName}" value="${fieldValue}" form="${externalFormID}">Button Text +
`); @@ -133,9 +133,8 @@ describe('textfield', () => { const fieldValue = Math.random().toString(); const fieldName = 'test-field'; const formId = 'testForm'; - addedElements = textToDomToParent(`
<${VWC_TEXTFIELD} name="${fieldName}" value="${fieldValue}" form="${formId}">Button Text
`); + addedElements = textToDomToParent(`
<${COMPONENT_NAME} name="${fieldName}" value="${fieldValue}" form="${formId}">Button Text
`); const formElement = addedElements[0]; - const actualElement = formElement.firstChild; await waitNextTask(); expect(formElement.querySelector('input')).to.equal(null); @@ -146,7 +145,7 @@ describe('textfield', () => { it(`should reset the value of the custom element to default on form reset`, async function () { const fieldValue = Math.random().toString(); const fieldName = 'test-field'; - addedElements = textToDomToParent(`
<${VWC_TEXTFIELD} name="${fieldName}" value="${fieldValue}">Button Text
`); + addedElements = textToDomToParent(`
<${COMPONENT_NAME} name="${fieldName}" value="${fieldValue}">Button Text
`); const formElement = addedElements[0]; const actualElement = formElement.firstChild; await waitNextTask(); @@ -160,7 +159,7 @@ describe('textfield', () => { it(`should change the value of the mock input on internal input change`, async function () { const fieldValue = Math.random().toString(); const fieldName = 'test-field'; - addedElements = textToDomToParent(`
<${VWC_TEXTFIELD} name="${fieldName}">Button Text
`); + addedElements = textToDomToParent(`
<${COMPONENT_NAME} name="${fieldName}">Button Text
`); const formElement = addedElements[0]; const actualElement = formElement.firstChild; await waitNextTask(); @@ -174,7 +173,7 @@ describe('textfield', () => { describe(`validation`, function () { it(`should get validity from the element's validationMessage`, async function () { const fieldName = 'test-field'; - addedElements = textToDomToParent(`
<${VWC_TEXTFIELD} required name="${fieldName}">Button Text
`); + addedElements = textToDomToParent(`
<${COMPONENT_NAME} required name="${fieldName}">Button Text
`); const formElement = addedElements[0]; const actualElement = formElement.firstChild; await waitNextTask(); @@ -190,7 +189,7 @@ describe('textfield', () => { it(`should validate on reset`, async function () { const fieldValue = Math.random().toString(); const fieldName = 'test-field'; - addedElements = textToDomToParent(`
<${VWC_TEXTFIELD} required value="${fieldValue}" name="${fieldName}">Button Text
`); + addedElements = textToDomToParent(`
<${COMPONENT_NAME} required value="${fieldValue}" name="${fieldName}">Button Text
`); const formElement = addedElements[0]; const actualElement = formElement.firstChild; await waitNextTask(); @@ -209,7 +208,7 @@ describe('textfield', () => { it(`should not submit an invalid form`, async function () { let submitted = false; const fieldName = 'test-field'; - addedElements = textToDomToParent(`
<${VWC_TEXTFIELD} required value="val" name="${fieldName}">Button Text
`); + addedElements = textToDomToParent(`
<${COMPONENT_NAME} required value="val" name="${fieldName}">Button Text
`); const formElement = addedElements[0]; const actualElement = formElement.firstChild; await waitNextTask(); @@ -243,7 +242,7 @@ describe('textfield', () => { addedElements = textToDomToParent(`
- <${VWC_TEXTFIELD} required value="${fieldValue}" name="${fieldName}">Button Text + <${COMPONENT_NAME} required value="${fieldValue}" name="${fieldName}">Button Text
`); const formElement = addedElements[0];