Skip to content

Commit

Permalink
feat(textfield): add form association support
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 537893731
  • Loading branch information
asyncLiz authored and copybara-github committed Jun 5, 2023
1 parent 2d02404 commit e842f79
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 37 deletions.
44 changes: 37 additions & 7 deletions textfield/lib/text-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {html as staticHtml, StaticValue} from 'lit/static-html.js';
import {ARIAMixinStrict} from '../../internal/aria/aria.js';
import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js';
import {redispatchEvent} from '../../internal/controller/events.js';
import {FormController, getFormValue} from '../../internal/controller/form-controller.js';
import {stringConverter} from '../../internal/controller/string-converter.js';

/**
Expand Down Expand Up @@ -46,6 +45,12 @@ export abstract class TextField extends LitElement {
static override shadowRootOptions:
ShadowRootInit = {...LitElement.shadowRootOptions, delegatesFocus: true};

/**
* @nocollapse
* @export
*/
static formAssociated = true;

@property({type: Boolean, reflect: true}) disabled = false;
/**
* Gets or sets whether or not the text field is in a visually invalid state.
Expand Down Expand Up @@ -95,15 +100,28 @@ export abstract class TextField extends LitElement {
*/
@property() textDirection = '';

// FormElement
/**
* The associated form element with which this element's value will submit.
*/
get form() {
return this.closest('form');
return this.internals.form;
}

@property({reflect: true, converter: stringConverter}) name = '';
/**
* The labels this element is associated with.
*/
get labels() {
return this.internals.labels;
}

[getFormValue]() {
return this.value;
/**
* The HTML name to use in form submission.
*/
get name() {
return this.getAttribute('name') ?? '';
}
set name(name: string) {
this.setAttribute('name', name);
}

// <input> properties
Expand Down Expand Up @@ -276,10 +294,11 @@ export abstract class TextField extends LitElement {
private readonly leadingIcons!: Element[];
@queryAssignedElements({slot: 'trailingicon'})
private readonly trailingIcons!: Element[];
private readonly internals =
(this as HTMLElement /* needed for closure */).attachInternals();

constructor() {
super();
this.addController(new FormController(this));
if (!isServer) {
this.addEventListener('click', this.focus);
this.addEventListener('focusin', this.handleFocusin);
Expand Down Expand Up @@ -474,6 +493,7 @@ export abstract class TextField extends LitElement {
// If a property such as `type` changes and causes the internal <input>
// value to change without dispatching an event, re-sync it.
const value = this.getInput().value;
this.internals.setFormValue(value);
if (this.value !== value) {
// Note this is typically inefficient in updated() since it schedules
// another update. However, it is needed for the <input> to fully render
Expand Down Expand Up @@ -699,4 +719,14 @@ export abstract class TextField extends LitElement {
this.hasLeadingIcon = this.leadingIcons.length > 0;
this.hasTrailingIcon = this.trailingIcons.length > 0;
}

/** @private */
formResetCallback() {
this.reset();
}

/** @private */
formStateRestoreCallback(state: string) {
this.value = state;
}
}
112 changes: 82 additions & 30 deletions textfield/lib/text-field_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {customElement} from 'lit/decorators.js';
import {literal} from 'lit/static-html.js';

import {Environment} from '../../testing/environment.js';
import {createFormTests} from '../../testing/forms.js';
import {Harness} from '../../testing/harness.js';
import {TextFieldHarness} from '../harness.js';

Expand Down Expand Up @@ -634,36 +635,87 @@ describe('TextField', () => {
});
});

describe('form submission', () => {
async function setupFormTest(propsInit: Partial<TestTextField> = {}) {
const template = html`
<form>
<md-test-text-field
?disabled=${propsInit.disabled === true}
.name=${propsInit.name ?? ''}
.value=${propsInit.value ?? ''}>
</md-test-text-field>
</form>`;
return setupTest(template);
}

it('does not submit if disabled', async () => {
const {harness} = await setupFormTest({name: 'foo', disabled: true});
const formData = await harness.submitForm();
expect(formData.get('foo')).toBeNull();
});

it('does not submit if name is not provided', async () => {
const {harness} = await setupFormTest();
const formData = await harness.submitForm();
const keys = Array.from(formData.keys());
expect(keys.length).toEqual(0);
});

it('submits under correct conditions', async () => {
const {harness} = await setupFormTest({name: 'foo', value: 'bar'});
const formData = await harness.submitForm();
expect(formData.get('foo')).toEqual('bar');
describe('forms', () => {
createFormTests({
queryControl: root => root.querySelector('md-test-text-field'),
valueTests: [
{
name: 'unnamed',
render: () =>
html`<md-test-text-field value="Value"></md-test-text-field>`,
assertValue(formData) {
expect(formData)
.withContext('should not add anything to form without a name')
.toHaveSize(0);
}
},
{
name: 'should add empty value',
render: () =>
html`<md-test-text-field name="input"></md-test-text-field>`,
assertValue(formData) {
expect(formData.get('input')).toBe('');
}
},
{
name: 'with value',
render: () =>
html`<md-test-text-field name="input" value="Value"></md-test-text-field>`,
assertValue(formData) {
expect(formData.get('input')).toBe('Value');
}
},
{
name: 'disabled',
render: () =>
html`<md-test-text-field name="input" value="Value" disabled></md-test-text-field>`,
assertValue(formData) {
expect(formData)
.withContext('should not add anything to form when disabled')
.toHaveSize(0);
}
}
],
resetTests: [
{
name: 'reset to empty value',
render: () =>
html`<md-test-text-field name="input"></md-test-text-field>`,
change(textField) {
textField.value = 'Value';
},
assertReset(textField) {
expect(textField.value)
.withContext('textField.value after reset')
.toBe('');
}
},
{
name: 'reset value',
render: () =>
html`<md-test-text-field name="input" value="First"></md-test-text-field>`,
change(textField) {
textField.value = 'Second';
},
assertReset(textField) {
expect(textField.value)
.withContext('textField.value after reset')
.toBe('First');
}
},
],
restoreTests: [
{
name: 'restore value',
render: () =>
html`<md-test-text-field name="input" value="Value"></md-test-text-field>`,
assertRestored(textField) {
expect(textField.value)
.withContext('textField.value after restore')
.toBe('Value');
}
},
]
});
});
});

0 comments on commit e842f79

Please sign in to comment.