Skip to content

Commit 921a905

Browse files
asynclizcopybara-github
authored andcommitted
feat(switch): add full form association support
PiperOrigin-RevId: 532627572
1 parent a61f79c commit 921a905

File tree

2 files changed

+140
-13
lines changed

2 files changed

+140
-13
lines changed

switch/lib/switch.ts

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,13 @@
77
import '../../focus/focus-ring.js';
88
import '../../ripple/ripple.js';
99

10-
import {html, isServer, LitElement, nothing, TemplateResult} from 'lit';
10+
import {html, isServer, LitElement, nothing, PropertyValues, TemplateResult} from 'lit';
1111
import {property, query, queryAsync, state} from 'lit/decorators.js';
1212
import {ClassInfo, classMap} from 'lit/directives/class-map.js';
1313
import {when} from 'lit/directives/when.js';
1414

1515
import {requestUpdateOnAriaChange} from '../../aria/delegate.js';
1616
import {dispatchActivationClick, isActivationClick} from '../../controller/events.js';
17-
import {FormController, getFormValue} from '../../controller/form-controller.js';
1817
import {ripple} from '../../ripple/directive.js';
1918
import {MdRipple} from '../../ripple/ripple.js';
2019

@@ -68,30 +67,40 @@ export class Switch extends LitElement {
6867
@query('button') private readonly button!: HTMLButtonElement|null;
6968

7069
/**
71-
* The associated form element with which this element's value will submit.
70+
* The value associated with this switch on form submission. `null` is
71+
* submitted when `selected` is `false`.
7272
*/
73-
get form() {
74-
return this.closest('form');
75-
}
73+
@property() value = 'on';
7674

7775
/**
7876
* The HTML name to use in form submission.
7977
*/
80-
@property({reflect: true}) name = '';
78+
get name() {
79+
return this.getAttribute('name') ?? '';
80+
}
81+
set name(name: string) {
82+
this.setAttribute('name', name);
83+
}
8184

8285
/**
83-
* The value associated with this switch on form submission. `null` is
84-
* submitted when `selected` is `false`.
86+
* The associated form element with which this element's value will submit.
8587
*/
86-
@property() value = 'on';
88+
get form() {
89+
return this.internals.form;
90+
}
8791

88-
[getFormValue]() {
89-
return this.selected ? this.value : null;
92+
/**
93+
* The labels this element is associated with.
94+
*/
95+
get labels() {
96+
return this.internals.labels;
9097
}
9198

99+
private readonly internals =
100+
(this as HTMLElement /* needed for closure */).attachInternals();
101+
92102
constructor() {
93103
super();
94-
this.addController(new FormController(this));
95104
if (!isServer) {
96105
this.addEventListener('click', (event: MouseEvent) => {
97106
if (!isActivationClick(event)) {
@@ -106,6 +115,12 @@ export class Switch extends LitElement {
106115
}
107116
}
108117

118+
protected override update(changed: PropertyValues<Switch>) {
119+
const state = String(this.selected);
120+
this.internals.setFormValue(this.selected ? this.value : null, state);
121+
super.update(changed);
122+
}
123+
109124
protected override render(): TemplateResult {
110125
// NOTE: buttons must use only [phrasing
111126
// content](https://html.spec.whatwg.org/multipage/dom.html#phrasing-content)
@@ -218,4 +233,16 @@ export class Switch extends LitElement {
218233
// Additionally, native change event is not an InputEvent.
219234
this.dispatchEvent(new Event('change', {bubbles: true}));
220235
}
236+
237+
/** @private */
238+
formResetCallback() {
239+
// The selected property does not reflect, so the original attribute set by
240+
// the user is used to determine the default value.
241+
this.selected = this.hasAttribute('selected');
242+
}
243+
244+
/** @private */
245+
formStateRestoreCallback(state: string) {
246+
this.selected = state === 'true';
247+
}
221248
}

switch/switch_test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
// import 'jasmine'; (google3-only)
88

9+
import {html} from 'lit';
10+
11+
import {createFormTests} from '../testing/forms.js';
912
import {createTokenTests} from '../testing/tokens.js';
1013

1114
import {MdSwitch} from './switch.js';
@@ -14,4 +17,101 @@ describe('<md-switch>', () => {
1417
describe('.styles', () => {
1518
createTokenTests(MdSwitch.styles);
1619
});
20+
21+
describe('forms', () => {
22+
createFormTests({
23+
queryControl: root => root.querySelector('md-switch'),
24+
valueTests: [
25+
{
26+
name: 'unnamed',
27+
render: () => html`<md-switch selected></md-switch>`,
28+
assertValue(formData) {
29+
expect(formData)
30+
.withContext('should not add anything to form without a name')
31+
.toHaveSize(0);
32+
}
33+
},
34+
{
35+
name: 'unselected',
36+
render: () => html`<md-switch name="switch"></md-switch>`,
37+
assertValue(formData) {
38+
expect(formData)
39+
.withContext('should not add anything to form when unselected')
40+
.toHaveSize(0);
41+
}
42+
},
43+
{
44+
name: 'selected default value',
45+
render: () => html`<md-switch name="switch" selected></md-switch>`,
46+
assertValue(formData) {
47+
expect(formData.get('switch')).toBe('on');
48+
}
49+
},
50+
{
51+
name: 'selected custom value',
52+
render: () =>
53+
html`<md-switch name="switch" selected value="Custom value"></md-switch>`,
54+
assertValue(formData) {
55+
expect(formData.get('switch')).toBe('Custom value');
56+
}
57+
},
58+
{
59+
name: 'disabled',
60+
render: () =>
61+
html`<md-switch name="switch" selected disabled></md-switch>`,
62+
assertValue(formData) {
63+
expect(formData)
64+
.withContext('should not add anything to form when disabled')
65+
.toHaveSize(0);
66+
}
67+
}
68+
],
69+
resetTests: [
70+
{
71+
name: 'reset to unselected',
72+
render: () => html`<md-switch name="switch"></md-switch>`,
73+
change(control) {
74+
control.selected = true;
75+
},
76+
assertReset(control) {
77+
expect(control.selected)
78+
.withContext('control.selected after reset')
79+
.toBeFalse();
80+
}
81+
},
82+
{
83+
name: 'reset to selected',
84+
render: () => html`<md-switch name="switch" selected></md-switch>`,
85+
change(control) {
86+
control.selected = false;
87+
},
88+
assertReset(control) {
89+
expect(control.selected)
90+
.withContext('control.selected after reset')
91+
.toBeTrue();
92+
}
93+
},
94+
],
95+
restoreTests: [
96+
{
97+
name: 'restore unselected',
98+
render: () => html`<md-switch name="switch"></md-switch>`,
99+
assertRestored(control) {
100+
expect(control.selected)
101+
.withContext('control.selected after restore')
102+
.toBeFalse();
103+
}
104+
},
105+
{
106+
name: 'restore selected',
107+
render: () => html`<md-switch name="switch" selected></md-switch>`,
108+
assertRestored(control) {
109+
expect(control.selected)
110+
.withContext('control.selected after restore')
111+
.toBeTrue();
112+
}
113+
},
114+
]
115+
});
116+
});
17117
});

0 commit comments

Comments
 (0)