diff --git a/angular/src/directives/proxies.ts b/angular/src/directives/proxies.ts
index 94efa9acff..e0a36eb59a 100644
--- a/angular/src/directives/proxies.ts
+++ b/angular/src/directives/proxies.ts
@@ -239,13 +239,13 @@ export declare interface IonButton extends Components.IonButton {
@ProxyCmp({
defineCustomElementFn: undefined,
- inputs: ['buttonType', 'color', 'disabled', 'download', 'expand', 'fill', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'size', 'strong', 'target', 'type']
+ inputs: ['buttonType', 'color', 'disabled', 'download', 'expand', 'fill', 'form', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'size', 'strong', 'target', 'type']
})
@Component({
selector: 'ion-button',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '',
- inputs: ['buttonType', 'color', 'disabled', 'download', 'expand', 'fill', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'size', 'strong', 'target', 'type']
+ inputs: ['buttonType', 'color', 'disabled', 'download', 'expand', 'fill', 'form', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'size', 'strong', 'target', 'type']
})
export class IonButton {
protected el: HTMLElement;
diff --git a/core/api.txt b/core/api.txt
index 7d56c5064d..56db72b84a 100644
--- a/core/api.txt
+++ b/core/api.txt
@@ -203,6 +203,7 @@ ion-button,prop,disabled,boolean,false,false,true
ion-button,prop,download,string | undefined,undefined,false,false
ion-button,prop,expand,"block" | "full" | undefined,undefined,false,true
ion-button,prop,fill,"clear" | "default" | "outline" | "solid" | undefined,undefined,false,true
+ion-button,prop,form,HTMLFormElement | string | undefined,undefined,false,false
ion-button,prop,href,string | undefined,undefined,false,false
ion-button,prop,mode,"ios" | "md",undefined,false,false
ion-button,prop,rel,string | undefined,undefined,false,false
diff --git a/core/src/components.d.ts b/core/src/components.d.ts
index 74d69eeaa2..dcb41e77fa 100644
--- a/core/src/components.d.ts
+++ b/core/src/components.d.ts
@@ -379,6 +379,10 @@ export namespace Components {
* Set to `"clear"` for a transparent button that resembles a flat button, to `"outline"` for a transparent button with a border, or to `"solid"` for a button with a filled background. The default fill is `"solid"` except inside of a toolbar, where the default is `"clear"`.
*/
"fill"?: 'clear' | 'outline' | 'solid' | 'default';
+ /**
+ * The HTML form element or form element id. Used to submit a form when the button is not a child of the form.
+ */
+ "form"?: string | HTMLFormElement;
/**
* Contains a URL or a URL fragment that the hyperlink points to. If this property is set, an anchor tag will be rendered.
*/
@@ -4343,6 +4347,10 @@ declare namespace LocalJSX {
* Set to `"clear"` for a transparent button that resembles a flat button, to `"outline"` for a transparent button with a border, or to `"solid"` for a button with a filled background. The default fill is `"solid"` except inside of a toolbar, where the default is `"clear"`.
*/
"fill"?: 'clear' | 'outline' | 'solid' | 'default';
+ /**
+ * The HTML form element or form element id. Used to submit a form when the button is not a child of the form.
+ */
+ "form"?: string | HTMLFormElement;
/**
* Contains a URL or a URL fragment that the hyperlink points to. If this property is set, an anchor tag will be rendered.
*/
diff --git a/core/src/components/button/button.tsx b/core/src/components/button/button.tsx
index d131adb857..060d9002ee 100644
--- a/core/src/components/button/button.tsx
+++ b/core/src/components/button/button.tsx
@@ -6,6 +6,7 @@ import type { AnimationBuilder, Color, RouterDirection } from '../../interface';
import type { AnchorInterface, ButtonInterface } from '../../utils/element-interface';
import type { Attributes } from '../../utils/helpers';
import { inheritAriaAttributes, hasShadowDom } from '../../utils/helpers';
+import { printIonWarning } from '../../utils/logging';
import { createColorClasses, hostContext, openURL } from '../../utils/theme';
/**
@@ -127,6 +128,11 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf
*/
@Prop() type: 'submit' | 'reset' | 'button' = 'button';
+ /**
+ * The HTML form element or form element id. Used to submit a form when the button is not a child of the form.
+ */
+ @Prop() form?: string | HTMLFormElement;
+
/**
* Emitted when the button has focus.
*/
@@ -160,21 +166,69 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf
return 'bounded';
}
+ /**
+ * Finds the form element based on the provided `form` selector
+ * or element reference provided.
+ */
+ private findForm(): HTMLFormElement | null {
+ const { form } = this;
+ if (form instanceof HTMLFormElement) {
+ return form;
+ }
+ if (typeof form === 'string') {
+ const el = document.getElementById(form);
+ if (el instanceof HTMLFormElement) {
+ return el;
+ }
+ }
+ return null;
+ }
+
private handleClick = (ev: Event) => {
+ const { el } = this;
if (this.type === 'button') {
openURL(this.href, ev, this.routerDirection, this.routerAnimation);
- } else if (hasShadowDom(this.el)) {
+ } else if (hasShadowDom(el)) {
// this button wants to specifically submit a form
// climb up the dom to see if we're in a
+ Submit
+ `);
+
+ const submitEvent = await page.spyOnEvent('submit');
+
+ await page.click('ion-button');
+
+ expect(submitEvent).toHaveReceivedEvent();
+ });
+
+ test('should submit the form by reference', async ({ page }) => {
+ await page.setContent(`
+
+ Submit
+
+ `);
+
+ const submitEvent = await page.spyOnEvent('submit');
+
+ await page.click('ion-button');
+
+ expect(submitEvent).toHaveReceivedEvent();
+ });
+
+ test('should submit the closest form', async ({ page }) => {
+ await page.setContent(`
+
+ `);
+
+ const submitEvent = await page.spyOnEvent('submit');
+
+ await page.click('ion-button');
+
+ expect(submitEvent).toHaveReceivedEvent();
+ });
+
+ test.describe('should throw a warning if the form cannot be found', () => {
+ test('form is a string selector', async ({ page }) => {
+ await page.setContent(`Submit`);
+
+ const logs: string[] = [];
+
+ page.on('console', (msg) => {
+ logs.push(msg.text());
+ });
+
+ await page.click('ion-button');
+
+ expect(logs.length).toBe(1);
+ expect(logs[0]).toContain(
+ '[Ionic Warning]: Form with selector: "#missingForm" could not be found. Verify that the id is correct and the form is rendered in the DOM.'
+ );
+ });
+
+ test('form is an element reference', async ({ page }) => {
+ await page.setContent(`
+ Submit
+
+ `);
+
+ const logs: string[] = [];
+
+ page.on('console', (msg) => {
+ logs.push(msg.text());
+ });
+
+ await page.click('ion-button');
+
+ expect(logs.length).toBe(1);
+ expect(logs[0]).toContain(
+ '[Ionic Warning]: The provided "form" element is invalid. Verify that the form is a HTMLFormElement and rendered in the DOM.'
+ );
+ });
+ });
+});
diff --git a/core/src/components/button/test/form-reference/index.html b/core/src/components/button/test/form-reference/index.html
new file mode 100644
index 0000000000..5cfdf1aea6
--- /dev/null
+++ b/core/src/components/button/test/form-reference/index.html
@@ -0,0 +1,51 @@
+
+
+
+
+ Button - Form
+
+
+
+
+
+
+
+
+
+
+
+
+ Outside button - Form Submit
+
+
+
+
+
+
+ Submit Form
+
+
+
+
+
+
diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts
index 9b683a7dc2..efd8d274f4 100644
--- a/packages/vue/src/proxies.ts
+++ b/packages/vue/src/proxies.ts
@@ -161,6 +161,7 @@ export const IonButton = /*@__PURE__*/ defineContainer('ion-butto
'strong',
'target',
'type',
+ 'form',
'ionFocus',
'ionBlur'
]);