Skip to content

Commit c9d8f2a

Browse files
authored
feat(ui5-button): introduce support for form attribute (#13321)
This PR introduces the form property to ui5-button, enabling buttons to submit forms even when placed outside the form element. This brings the component in line with native HTML button behavior. ## Usage: The form property in the ui5-button takes in an ID of a form as a string allowing the button to submit the associated form: ```ts <form id="externalForm"> <!-- form fields --> </form> <ui5-button form="externalForm" type="Submit">Submit</ui5-button> ``` Fixes: #7459
1 parent b38ce39 commit c9d8f2a

4 files changed

Lines changed: 192 additions & 3 deletions

File tree

packages/base/src/features/InputElementsFormSupport.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,22 @@ interface IFormInputElement extends UI5Element {
88
formElementAnchor?: () => HTMLElement | undefined | Promise<HTMLElement | undefined>;
99
}
1010

11+
/**
12+
* Gets the associated form for an element.
13+
* If the element has a `form` attribute, it looks up the form by ID.
14+
* Otherwise, it falls back to the form associated via ElementInternals.
15+
*/
16+
const getAssociatedForm = (element: UI5Element): HTMLFormElement | null => {
17+
const formAttribute = element.getAttribute("form");
18+
19+
if (formAttribute) {
20+
const form = document.getElementById(formAttribute);
21+
return form instanceof HTMLFormElement ? form : null;
22+
}
23+
24+
return element._internals?.form ?? null;
25+
};
26+
1127
const updateFormValue = (element: IFormInputElement | UI5Element) => {
1228
if (isInputElement(element)) {
1329
setFormValue(element);
@@ -46,15 +62,22 @@ const setFormValidity = async (element: IFormInputElement) => {
4662
};
4763

4864
const submitForm = async (element: UI5Element) => {
49-
const elements = [...(element._internals?.form?.elements ?? [])] as Array<IFormInputElement | UI5Element>;
65+
const form = getAssociatedForm(element);
66+
67+
if (!form) {
68+
return;
69+
}
70+
71+
const elements = [...form.elements] as Array<IFormInputElement | UI5Element>;
5072

5173
await Promise.all(elements.map(el => { return isInputElement(el) ? setFormValidity(el) : Promise.resolve(); }));
5274

53-
element._internals?.form?.requestSubmit();
75+
form.requestSubmit();
5476
};
5577

5678
const resetForm = (element: UI5Element) => {
57-
element._internals?.form?.reset();
79+
const form = getAssociatedForm(element);
80+
form?.reset();
5881
};
5982

6083
const isInputElement = (element: IFormInputElement | UI5Element): element is IFormInputElement => {

packages/main/cypress/specs/Button.cy.tsx

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,4 +638,128 @@ describe("Accessibility", () => {
638638
expect(info.description).to.include("Negative Action");
639639
});
640640
});
641+
});
642+
643+
describe("Button form attribute", () => {
644+
it("should submit an external form using form attribute", () => {
645+
const submitSpy = cy.spy().as("submitSpy");
646+
647+
cy.mount(
648+
<div>
649+
<form id="externalForm" onSubmit={e => {
650+
e.preventDefault();
651+
submitSpy();
652+
}}>
653+
<input name="test" defaultValue="value" />
654+
</form>
655+
<Button form="externalForm" type="Submit">Submit External Form</Button>
656+
</div>
657+
);
658+
659+
cy.get("[ui5-button]")
660+
.realClick();
661+
662+
cy.get("@submitSpy")
663+
.should("have.been.calledOnce");
664+
});
665+
666+
it("should reset an external form using form attribute", () => {
667+
cy.mount(
668+
<div>
669+
<form id="resetForm">
670+
<input name="test" id="testInput" defaultValue="initial" />
671+
</form>
672+
<Button form="resetForm" type="Reset">Reset External Form</Button>
673+
</div>
674+
);
675+
676+
// Change the input value
677+
cy.get("#testInput")
678+
.clear()
679+
.realType("changed");
680+
681+
cy.get("#testInput")
682+
.should("have.value", "changed");
683+
684+
// Click the reset button
685+
cy.get("[ui5-button]")
686+
.realClick();
687+
688+
// Verify the form was reset
689+
cy.get("#testInput")
690+
.should("have.value", "initial");
691+
});
692+
693+
it("should not submit when form attribute references non-existent form", () => {
694+
const submitSpy = cy.spy().as("submitSpy");
695+
696+
cy.mount(
697+
<div>
698+
<form id="realForm" onSubmit={e => {
699+
e.preventDefault();
700+
submitSpy();
701+
}}>
702+
<input name="test" defaultValue="value" />
703+
</form>
704+
<Button form="nonExistentForm" type="Submit">Submit</Button>
705+
</div>
706+
);
707+
708+
cy.get("[ui5-button]")
709+
.realClick();
710+
711+
cy.get("@submitSpy")
712+
.should("not.have.been.called");
713+
});
714+
715+
it("should prioritize form attribute over parent form", () => {
716+
const parentFormSubmitSpy = cy.spy().as("parentFormSubmit");
717+
const externalFormSubmitSpy = cy.spy().as("externalFormSubmit");
718+
719+
cy.mount(
720+
<div>
721+
<form id="externalForm" onSubmit={e => {
722+
e.preventDefault();
723+
externalFormSubmitSpy();
724+
}}>
725+
<input name="external" defaultValue="external" />
726+
</form>
727+
<form id="parentForm" onSubmit={e => {
728+
e.preventDefault();
729+
parentFormSubmitSpy();
730+
}}>
731+
<input name="parent" defaultValue="parent" />
732+
<Button form="externalForm" type="Submit">Submit External</Button>
733+
</form>
734+
</div>
735+
);
736+
737+
cy.get("[ui5-button]")
738+
.realClick();
739+
740+
cy.get("@externalFormSubmit")
741+
.should("have.been.calledOnce");
742+
cy.get("@parentFormSubmit")
743+
.should("not.have.been.called");
744+
});
745+
746+
it("should fall back to parent form when form attribute is not set", () => {
747+
const submitSpy = cy.spy().as("submitSpy");
748+
749+
cy.mount(
750+
<form onSubmit={e => {
751+
e.preventDefault();
752+
submitSpy();
753+
}}>
754+
<input name="test" defaultValue="value" />
755+
<Button type="Submit">Submit Parent Form</Button>
756+
</form>
757+
);
758+
759+
cy.get("[ui5-button]")
760+
.realClick();
761+
762+
cy.get("@submitSpy")
763+
.should("have.been.calledOnce");
764+
});
641765
});

packages/main/src/Button.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,19 @@ class Button extends UI5Element implements IButton {
204204
@property({ type: Boolean })
205205
submits = false;
206206

207+
/**
208+
* Associates the button with a form element by the form's `id` attribute.
209+
* When set, the button can submit or reset the specified form even if the button
210+
* is not a descendant of that form.
211+
*
212+
* **Note:** This property takes effect only when the button's "type" property is set to "Submit" or "Reset".
213+
* @default undefined
214+
* @public
215+
* @since 2.21.0
216+
*/
217+
@property()
218+
form?: string;
219+
207220
/**
208221
* Defines the tooltip of the component.
209222
*

packages/main/test/pages/Button.html

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,22 @@
309309
<ui5-button type="Reset">Reset</ui5-button>
310310
</form>
311311

312+
<br />
313+
<br />
314+
<ui5-title>Buttons with form attribute (outside form)</ui5-title>
315+
<br />
316+
<form id="externalForm" onsubmit="handleExternalFormSubmit(event)">
317+
<ui5-label>External Form:</ui5-label>
318+
<ui5-input id="externalFormInput" name="externalInput" value="External form input"></ui5-input>
319+
</form>
320+
<br />
321+
<p>These buttons are outside the form but control it via the <code>form</code> attribute:</p>
322+
<ui5-button form="externalForm" type="Submit" design="Emphasized">Submit External Form</ui5-button>
323+
<ui5-button form="externalForm" type="Reset">Reset External Form</ui5-button>
324+
<span id="externalFormMessage" style="color: green; display: none; margin-left: 10px;">Form submitted!</span>
325+
<br />
326+
<br />
327+
312328
<ui5-button id="openDialogButton" design="Emphasized">Show Registration Dialog</ui5-button>
313329

314330
<ui5-dialog id="registration-dialog" header-text="Register Form">
@@ -413,6 +429,19 @@
413429
btnInLink.addEventListener("ui5-click", (e) => {
414430
e.preventDefault();
415431
});
432+
433+
function handleExternalFormSubmit(event) {
434+
event.preventDefault();
435+
const formData = new FormData(event.target);
436+
const values = Object.fromEntries(formData.entries());
437+
const message = document.getElementById('externalFormMessage');
438+
message.textContent = 'Submitted: ' + JSON.stringify(values);
439+
message.style.display = 'inline';
440+
setTimeout(() => {
441+
message.style.display = 'none';
442+
}, 5000);
443+
}
444+
window.handleExternalFormSubmit = handleExternalFormSubmit;
416445
</script>
417446

418447
</body>

0 commit comments

Comments
 (0)