mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-20 20:33:32 +08:00
feat(button): submit from outside of form (#25913)
Resolves #21194 Co-authored-by: Sean Perkins <sean@ionic.io>
This commit is contained in:
@ -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 <form>
|
||||
// and if so, then use JS to submit it
|
||||
const form = this.el.closest('form');
|
||||
if (form) {
|
||||
let formEl = this.findForm();
|
||||
const { form } = this;
|
||||
|
||||
if (!formEl && form !== undefined) {
|
||||
/**
|
||||
* The developer specified a form selector for
|
||||
* the button to submit, but it was not found.
|
||||
*/
|
||||
if (typeof form === 'string') {
|
||||
printIonWarning(
|
||||
`Form with selector: "#${form}" could not be found. Verify that the id is correct and the form is rendered in the DOM.`,
|
||||
el
|
||||
);
|
||||
} else {
|
||||
printIonWarning(
|
||||
`The provided "form" element is invalid. Verify that the form is a HTMLFormElement and rendered in the DOM.`,
|
||||
el
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formEl) {
|
||||
/**
|
||||
* If the form element is not set, the button may be inside
|
||||
* of a form element. Query the closest form element to the button.
|
||||
*/
|
||||
formEl = el.closest('form');
|
||||
}
|
||||
|
||||
if (formEl) {
|
||||
ev.preventDefault();
|
||||
|
||||
const fakeButton = document.createElement('button');
|
||||
fakeButton.type = this.type;
|
||||
fakeButton.style.display = 'none';
|
||||
form.appendChild(fakeButton);
|
||||
formEl.appendChild(fakeButton);
|
||||
fakeButton.click();
|
||||
fakeButton.remove();
|
||||
}
|
||||
@ -217,7 +271,6 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf
|
||||
rel,
|
||||
target,
|
||||
};
|
||||
|
||||
let fill = this.fill;
|
||||
if (fill === undefined) {
|
||||
fill = this.inToolbar || this.inListHeader ? 'clear' : 'solid';
|
||||
|
93
core/src/components/button/test/form-reference/button.e2e.ts
Normal file
93
core/src/components/button/test/form-reference/button.e2e.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '@utils/test/playwright';
|
||||
|
||||
test.describe('button: form', () => {
|
||||
test('should submit the form by id', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<form id="myForm"></form>
|
||||
<ion-button form="myForm" type="submit">Submit</ion-button>
|
||||
`);
|
||||
|
||||
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(`
|
||||
<form></form>
|
||||
<ion-button type="submit">Submit</ion-button>
|
||||
<script>
|
||||
const form = document.querySelector('form');
|
||||
const button = document.querySelector('ion-button');
|
||||
button.form = form;
|
||||
</script>
|
||||
`);
|
||||
|
||||
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(`
|
||||
<form>
|
||||
<ion-button type="submit">Submit</ion-button>
|
||||
</form>
|
||||
`);
|
||||
|
||||
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(`<ion-button type="submit" form="missingForm">Submit</ion-button>`);
|
||||
|
||||
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(`
|
||||
<ion-button type="submit">Submit</ion-button>
|
||||
<script>
|
||||
const form = document.querySelector('form');
|
||||
const button = document.querySelector('ion-button');
|
||||
|
||||
button.form = form;
|
||||
</script>
|
||||
`);
|
||||
|
||||
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.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
51
core/src/components/button/test/form-reference/index.html
Normal file
51
core/src/components/button/test/form-reference/index.html
Normal file
@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Button - Form</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Outside button - Form Submit</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding ion-text-center" id="content">
|
||||
<form id="outside-submit" onsubmit="return validate(event)" action="http://httpbin.org/get" method="GET">
|
||||
<div>
|
||||
<input name="name" required />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ion-button type="submit" form="outside-submit"> Submit Form </ion-button>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
|
||||
<script>
|
||||
document.querySelector('form').addEventListener('submit', (event) => {
|
||||
console.log('SUBMIT from event', event);
|
||||
});
|
||||
function validate(event) {
|
||||
console.log('SUBMIT from attribute', event);
|
||||
if (event.target.elements[0].value === 'admin') {
|
||||
return true;
|
||||
} else {
|
||||
console.log('INCORRECT USER, use "admin"');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Reference in New Issue
Block a user