Skip to content

Commit eda200c

Browse files
asynclizcopybara-github
authored andcommitted
feat(labs): change form-submitter to a behavior mixin
PiperOrigin-RevId: 879198126
1 parent 38724b9 commit eda200c

File tree

5 files changed

+153
-201
lines changed

5 files changed

+153
-201
lines changed

button/internal/button.ts

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,6 @@ import {property, query, queryAssignedElements} from 'lit/decorators.js';
1212

1313
import {ARIAMixinStrict} from '../../internal/aria/aria.js';
1414
import {mixinDelegatesAria} from '../../internal/aria/delegate.js';
15-
import {
16-
FormSubmitter,
17-
setupFormSubmitter,
18-
type FormSubmitterType,
19-
} from '../../internal/controller/form-submitter.js';
2015
import {
2116
dispatchActivationClick,
2217
isActivationClick,
@@ -25,18 +20,17 @@ import {
2520
internals,
2621
mixinElementInternals,
2722
} from '../../labs/behaviors/element-internals.js';
23+
import {mixinFormSubmitter} from '../../labs/behaviors/form-submitter.js';
2824

2925
// Separate variable needed for closure.
30-
const buttonBaseClass = mixinDelegatesAria(mixinElementInternals(LitElement));
26+
const buttonBaseClass = mixinDelegatesAria(
27+
mixinFormSubmitter(mixinElementInternals(LitElement)),
28+
);
3129

3230
/**
3331
* A button component.
3432
*/
35-
export abstract class Button extends buttonBaseClass implements FormSubmitter {
36-
static {
37-
setupFormSubmitter(Button);
38-
}
39-
33+
export abstract class Button extends buttonBaseClass {
4034
/** @nocollapse */
4135
static readonly formAssociated = true;
4236

@@ -95,25 +89,6 @@ export abstract class Button extends buttonBaseClass implements FormSubmitter {
9589
@property({type: Boolean, attribute: 'has-icon', reflect: true}) hasIcon =
9690
false;
9791

98-
/**
99-
* The default behavior of the button. May be "button", "reset", or "submit"
100-
* (default).
101-
*/
102-
@property() type: FormSubmitterType = 'submit';
103-
104-
/**
105-
* The value added to a form with the button's name when the button submits a
106-
* form.
107-
*/
108-
@property({reflect: true}) value = '';
109-
110-
get name() {
111-
return this.getAttribute('name') ?? '';
112-
}
113-
set name(name: string) {
114-
this.setAttribute('name', name);
115-
}
116-
11792
/**
11893
* The associated form element with which this element's value will submit.
11994
*/

iconbutton/internal/icon-button.ts

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,6 @@ import {literal, html as staticHtml} from 'lit/static-html.js';
1414

1515
import {ARIAMixinStrict} from '../../internal/aria/aria.js';
1616
import {mixinDelegatesAria} from '../../internal/aria/delegate.js';
17-
import {
18-
FormSubmitter,
19-
setupFormSubmitter,
20-
type FormSubmitterType,
21-
} from '../../internal/controller/form-submitter.js';
2217
import {isRtl} from '../../internal/controller/is-rtl.js';
2318
import {
2419
afterDispatch,
@@ -28,12 +23,13 @@ import {
2823
internals,
2924
mixinElementInternals,
3025
} from '../../labs/behaviors/element-internals.js';
26+
import {mixinFormSubmitter} from '../../labs/behaviors/form-submitter.js';
3127

3228
type LinkTarget = '_blank' | '_parent' | '_self' | '_top';
3329

3430
// Separate variable needed for closure.
3531
const iconButtonBaseClass = mixinDelegatesAria(
36-
mixinElementInternals(LitElement),
32+
mixinFormSubmitter(mixinElementInternals(LitElement)),
3733
);
3834

3935
/**
@@ -43,11 +39,7 @@ const iconButtonBaseClass = mixinDelegatesAria(
4339
* --composed
4440
* @fires change {Event} Dispatched when a toggle button toggles --bubbles
4541
*/
46-
export class IconButton extends iconButtonBaseClass implements FormSubmitter {
47-
static {
48-
setupFormSubmitter(IconButton);
49-
}
50-
42+
export class IconButton extends iconButtonBaseClass {
5143
/** @nocollapse */
5244
static readonly formAssociated = true;
5345

@@ -113,25 +105,6 @@ export class IconButton extends iconButtonBaseClass implements FormSubmitter {
113105
*/
114106
@property({type: Boolean, reflect: true}) selected = false;
115107

116-
/**
117-
* The default behavior of the button. May be "button", "reset", or "submit"
118-
* (default).
119-
*/
120-
@property() type: FormSubmitterType = 'submit';
121-
122-
/**
123-
* The value added to a form with the button's name when the button submits a
124-
* form.
125-
*/
126-
@property({reflect: true}) value = '';
127-
128-
get name() {
129-
return this.getAttribute('name') ?? '';
130-
}
131-
set name(name: string) {
132-
this.setAttribute('name', name);
133-
}
134-
135108
/**
136109
* The associated form element with which this element's value will submit.
137110
*/

internal/controller/form-submitter.ts

Lines changed: 0 additions & 130 deletions
This file was deleted.

labs/behaviors/form-submitter.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/**
2+
* @license
3+
* Copyright 2023 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {isServer, LitElement} from 'lit';
8+
import {property} from 'lit/decorators.js';
9+
10+
import {
11+
afterDispatch,
12+
setupDispatchHooks,
13+
} from '../../internal/events/dispatch-hooks.js';
14+
import {internals, WithElementInternals} from './element-internals.js';
15+
import {MixinBase, MixinReturn} from './mixin.js';
16+
17+
/**
18+
* A string indicating the form submission behavior of the element.
19+
*
20+
* - submit: The element submits the form. This is the default value if the
21+
* attribute is not specified, or if it is dynamically changed to an empty or
22+
* invalid value.
23+
* - reset: The element resets the form.
24+
* - button: The element does nothing.
25+
*/
26+
export type FormSubmitterType = 'button' | 'submit' | 'reset';
27+
28+
/**
29+
* An element that can submit or reset a `<form>`, similar to
30+
* `<button type="submit">`.
31+
*/
32+
export interface FormSubmitter {
33+
/**
34+
* A string indicating the form submission behavior of the element.
35+
*
36+
* - submit: The element submits the form. This is the default value if the
37+
* attribute is not specified, or if it is dynamically changed to an empty or
38+
* invalid value.
39+
* - reset: The element resets the form.
40+
* - button: The element does nothing.
41+
*/
42+
type: FormSubmitterType;
43+
44+
/**
45+
* The HTML name to use in form submission. When combined with a `value`, the
46+
* submitting button's name/value will be added to the form.
47+
*
48+
* Names must reflect to a `name` attribute for form integration.
49+
*/
50+
name: string;
51+
52+
/**
53+
* The value of the button. When combined with a `name`, the submitting
54+
* button's name/value will be added to the form.
55+
*/
56+
value: string;
57+
}
58+
59+
/**
60+
* Mixes in form submitter behavior for a class.
61+
*
62+
* A click listener is added to each element instance. If the click is not
63+
* default prevented, it will submit the element's form, if any.
64+
*
65+
* @example
66+
* ```ts
67+
* const base = mixinFormSubmitter(mixinElementInternals(LitElement));
68+
* class MyButton extends base {
69+
* static formAssociated = true;
70+
* }
71+
* ```
72+
*
73+
* @param base The class to mix functionality into.
74+
* @return The provided class with `FormSubmitter` mixed in.
75+
*/
76+
export function mixinFormSubmitter<
77+
T extends MixinBase<LitElement & WithElementInternals>,
78+
>(base: T): MixinReturn<T, FormSubmitter> {
79+
abstract class FormSubmitterElement extends base implements FormSubmitter {
80+
@property() type: FormSubmitterType = 'submit';
81+
@property({reflect: true}) value = '';
82+
83+
// Name attribute must reflect synchronously for form integration.
84+
get name() {
85+
return this.getAttribute('name') ?? '';
86+
}
87+
set name(name: string) {
88+
this.setAttribute('name', name);
89+
}
90+
91+
// Mixins must have a constructor with `...args: any[]`
92+
// tslint:disable-next-line:no-any
93+
constructor(...args: any[]) {
94+
super(...args);
95+
if (isServer) return;
96+
setupDispatchHooks(this, 'click');
97+
this.addEventListener('click', async (event: Event) => {
98+
const isReset = this.type === 'reset';
99+
const isSubmit = this.type === 'submit';
100+
const elementInternals = this[internals];
101+
const {form} = elementInternals;
102+
if (!form || !(isSubmit || isReset)) {
103+
return;
104+
}
105+
106+
afterDispatch(event, () => {
107+
if (event.defaultPrevented) {
108+
return;
109+
}
110+
111+
if (isReset) {
112+
form.reset();
113+
return;
114+
}
115+
116+
// form.requestSubmit(submitter) does not work with form associated custom
117+
// elements. This patches the dispatched submit event to add the correct
118+
// `submitter`.
119+
// See https://github.com/WICG/webcomponents/issues/814
120+
form.addEventListener(
121+
'submit',
122+
(submitEvent: Event) => {
123+
Object.defineProperty(submitEvent, 'submitter', {
124+
configurable: true,
125+
enumerable: true,
126+
get: () => this,
127+
});
128+
},
129+
{capture: true, once: true},
130+
);
131+
132+
elementInternals.setFormValue(this.value);
133+
form.requestSubmit();
134+
});
135+
});
136+
}
137+
}
138+
139+
return FormSubmitterElement;
140+
}

0 commit comments

Comments
 (0)