Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(textarea): textarea form association #300

Merged
merged 16 commits into from
Sep 8, 2020
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 20 additions & 0 deletions __snapshots__/vwc-textfield.md
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>

```

55 changes: 55 additions & 0 deletions common/foundation/src/form-association.ts
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: any): HTMLFormElement | null {
gullerya marked this conversation as resolved.
Show resolved Hide resolved
const formId = element.form;
gullerya marked this conversation as resolved.
Show resolved Hide resolved
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]) {
YonatanKra marked this conversation as resolved.
Show resolved Hide resolved
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) {
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);
});
}
136 changes: 136 additions & 0 deletions common/foundation/test/form-association.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { addInputToForm } from '../form-association';
import { textToDomToParent } from '../../../test/test-helpers';

describe(`Form Association Foundation`, function() {
let addedElements = [];

afterEach(function() {
addedElements.forEach(elm => elm.remove());
});

describe(`addInputToForm`, function() {
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 = 'otherForm';
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.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 = 'otherForm';
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.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 = 'otherForm';
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.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 = 'otherForm';
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;

HTMLElement.prototype.setCustomValidity = function() {
return 5;
};
addInputToForm(inputElementWrapper, hiddenElementType);

expect(formElement.querySelector(`[name="${fieldName}"]`).tagName).to.equal(hiddenElementType);
});
});
});
2 changes: 1 addition & 1 deletion components/select/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@
},
"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",
"lit-element": "^2.4.0",
"tslib": "^2.0.1"
},
"devDependencies": {
"@vonage/vvd-foundation": "^0.5.0",
"@vonage/vvd-typography": "^0.5.0"
}
}
59 changes: 2 additions & 57 deletions components/select/src/vwc-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 [<mwc-select>](https://github.com/material-components/material-components-web-components/tree/master/packages/select)
*/
Expand All @@ -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 {
Expand All @@ -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);
});

}
}
1 change: 1 addition & 0 deletions components/textarea/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 6 additions & 1 deletion components/textarea/src/vwc-textarea.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<void> {
await super.firstUpdated();
this.shadowRoot?.querySelector('.mdc-notched-outline')?.shadowRoot?.querySelector('.mdc-notched-outline')?.classList.add('vvd-notch');
addInputToForm(this, 'textarea');
}
}