Skip to content

Commit a61f79c

Browse files
asynclizcopybara-github
authored andcommitted
feat(checkbox): add full form association support
PiperOrigin-RevId: 532621912
1 parent 57f7ae2 commit a61f79c

File tree

3 files changed

+497
-15
lines changed

3 files changed

+497
-15
lines changed

checkbox/checkbox_test.ts

Lines changed: 137 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 {MdCheckbox} from './checkbox.js';
@@ -14,4 +17,138 @@ describe('<md-checkbox>', () => {
1417
describe('.styles', () => {
1518
createTokenTests(MdCheckbox.styles);
1619
});
20+
21+
describe('forms', () => {
22+
createFormTests({
23+
queryControl: root => root.querySelector('md-checkbox'),
24+
valueTests: [
25+
{
26+
name: 'unnamed',
27+
render: () => html`<md-checkbox checked></md-checkbox>`,
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: 'unchecked',
36+
render: () => html`<md-checkbox name="checkbox"></md-checkbox>`,
37+
assertValue(formData) {
38+
expect(formData)
39+
.withContext('should not add anything to form when unchecked')
40+
.toHaveSize(0);
41+
}
42+
},
43+
{
44+
name: 'checked default value',
45+
render: () =>
46+
html`<md-checkbox name="checkbox" checked></md-checkbox>`,
47+
assertValue(formData) {
48+
expect(formData.get('checkbox')).toBe('on');
49+
}
50+
},
51+
{
52+
name: 'checked custom value',
53+
render: () =>
54+
html`<md-checkbox name="checkbox" checked value="Custom value"></md-checkbox>`,
55+
assertValue(formData) {
56+
expect(formData.get('checkbox')).toBe('Custom value');
57+
}
58+
},
59+
{
60+
name: 'indeterminate',
61+
render: () =>
62+
html`<md-checkbox name="checkbox" checked indeterminate></md-checkbox>`,
63+
assertValue(formData) {
64+
expect(formData)
65+
.withContext(
66+
'should not add anything to form when indeterminate')
67+
.toHaveSize(0);
68+
}
69+
},
70+
{
71+
name: 'disabled',
72+
render: () =>
73+
html`<md-checkbox name="checkbox" checked disabled></md-checkbox>`,
74+
assertValue(formData) {
75+
expect(formData)
76+
.withContext('should not add anything to form when disabled')
77+
.toHaveSize(0);
78+
}
79+
}
80+
],
81+
resetTests: [
82+
{
83+
name: 'reset to unchecked',
84+
render: () => html`<md-checkbox name="checkbox"></md-checkbox>`,
85+
change(checkbox) {
86+
checkbox.checked = true;
87+
},
88+
assertReset(checkbox) {
89+
expect(checkbox.checked)
90+
.withContext('checkbox.checked after reset')
91+
.toBeFalse();
92+
}
93+
},
94+
{
95+
name: 'reset to checked',
96+
render: () =>
97+
html`<md-checkbox name="checkbox" checked></md-checkbox>`,
98+
change(checkbox) {
99+
checkbox.checked = false;
100+
},
101+
assertReset(checkbox) {
102+
expect(checkbox.checked)
103+
.withContext('checkbox.checked after reset')
104+
.toBeTrue();
105+
}
106+
},
107+
{
108+
name: 'reset to indeterminate',
109+
render: () =>
110+
html`<md-checkbox name="checkbox" indeterminate></md-checkbox>`,
111+
change(checkbox) {
112+
checkbox.indeterminate = false;
113+
},
114+
assertReset(checkbox) {
115+
expect(checkbox.indeterminate)
116+
.withContext('checkbox.indeterminate should not be reset')
117+
.toBeFalse();
118+
}
119+
}
120+
],
121+
restoreTests: [
122+
{
123+
name: 'restore unchecked',
124+
render: () => html`<md-checkbox name="checkbox"></md-checkbox>`,
125+
assertRestored(checkbox) {
126+
expect(checkbox.checked)
127+
.withContext('checkbox.checked after restore')
128+
.toBeFalse();
129+
}
130+
},
131+
{
132+
name: 'restore checked',
133+
render: () =>
134+
html`<md-checkbox name="checkbox" checked></md-checkbox>`,
135+
assertRestored(checkbox) {
136+
expect(checkbox.checked)
137+
.withContext('checkbox.checked after restore')
138+
.toBeTrue();
139+
}
140+
},
141+
{
142+
name: 'restore indeterminate',
143+
render: () =>
144+
html`<md-checkbox name="checkbox" indeterminate></md-checkbox>`,
145+
assertRestored(checkbox) {
146+
expect(checkbox.indeterminate)
147+
.withContext('checkbox.indeterminate should not be restored')
148+
.toBeFalse();
149+
}
150+
}
151+
]
152+
});
153+
});
17154
});

checkbox/lib/checkbox.ts

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ import {when} from 'lit/directives/when.js';
1515
import {ARIAMixinStrict} from '../../aria/aria.js';
1616
import {requestUpdateOnAriaChange} from '../../aria/delegate.js';
1717
import {dispatchActivationClick, isActivationClick, redispatchEvent} from '../../controller/events.js';
18-
import {FormController, getFormValue} from '../../controller/form-controller.js';
19-
import {stringConverter} from '../../controller/string-converter.js';
2018
import {ripple} from '../../ripple/directive.js';
2119
import {MdRipple} from '../../ripple/ripple.js';
2220

@@ -28,15 +26,13 @@ export class Checkbox extends LitElement {
2826
requestUpdateOnAriaChange(this);
2927
}
3028

31-
/**
32-
* @nocollapse
33-
*/
29+
/** @nocollapse */
3430
static formAssociated = true;
3531

3632
/**
3733
* Whether or not the checkbox is selected.
3834
*/
39-
@property({type: Boolean, reflect: true}) checked = false;
35+
@property({type: Boolean}) checked = false;
4036

4137
/**
4238
* Whether or not the checkbox is disabled.
@@ -46,14 +42,14 @@ export class Checkbox extends LitElement {
4642
/**
4743
* Whether or not the checkbox is invalid.
4844
*/
49-
@property({type: Boolean, reflect: true}) error = false;
45+
@property({type: Boolean}) error = false;
5046

5147
/**
5248
* Whether or not the checkbox is indeterminate.
5349
*
5450
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#indeterminate_state_checkboxes
5551
*/
56-
@property({type: Boolean, reflect: true}) indeterminate = false;
52+
@property({type: Boolean}) indeterminate = false;
5753

5854
/**
5955
* The value of the checkbox that is submitted with a form when selected.
@@ -65,13 +61,25 @@ export class Checkbox extends LitElement {
6561
/**
6662
* The HTML name to use in form submission.
6763
*/
68-
@property({reflect: true, converter: stringConverter}) name = '';
64+
get name() {
65+
return this.getAttribute('name') ?? '';
66+
}
67+
set name(name: string) {
68+
this.setAttribute('name', name);
69+
}
6970

7071
/**
7172
* The associated form element with which this element's value will submit.
7273
*/
7374
get form() {
74-
return this.closest('form');
75+
return this.internals.form;
76+
}
77+
78+
/**
79+
* The labels this element is associated with.
80+
*/
81+
get labels() {
82+
return this.internals.labels;
7583
}
7684

7785
@state() private prevChecked = false;
@@ -80,10 +88,11 @@ export class Checkbox extends LitElement {
8088
@queryAsync('md-ripple') private readonly ripple!: Promise<MdRipple|null>;
8189
@query('input') private readonly input!: HTMLInputElement|null;
8290
@state() private showRipple = false;
91+
private readonly internals =
92+
(this as HTMLElement /* needed for closure */).attachInternals();
8393

8494
constructor() {
8595
super();
86-
this.addController(new FormController(this));
8796
if (!isServer) {
8897
this.addEventListener('click', (event: MouseEvent) => {
8998
if (!isActivationClick(event)) {
@@ -99,10 +108,6 @@ export class Checkbox extends LitElement {
99108
this.input?.focus();
100109
}
101110

102-
[getFormValue]() {
103-
return this.checked ? this.value : null;
104-
}
105-
106111
protected override update(changed: PropertyValues<Checkbox>) {
107112
if (changed.has('checked') || changed.has('disabled') ||
108113
changed.has('indeterminate')) {
@@ -112,6 +117,9 @@ export class Checkbox extends LitElement {
112117
changed.get('indeterminate') ?? this.indeterminate;
113118
}
114119

120+
const shouldAddFormValue = this.checked && !this.indeterminate;
121+
const state = String(this.checked);
122+
this.internals.setFormValue(shouldAddFormValue ? this.value : null, state);
115123
super.update(changed);
116124
}
117125

@@ -176,4 +184,16 @@ export class Checkbox extends LitElement {
176184
private readonly renderRipple = () => { // bind to this
177185
return html`<md-ripple ?disabled=${this.disabled} unbounded></md-ripple>`;
178186
};
187+
188+
/** @private */
189+
formResetCallback() {
190+
// The checked property does not reflect, so the original attribute set by
191+
// the user is used to determine the default value.
192+
this.checked = this.hasAttribute('checked');
193+
}
194+
195+
/** @private */
196+
formStateRestoreCallback(state: string) {
197+
this.checked = state === 'true';
198+
}
179199
}

0 commit comments

Comments
 (0)