diff --git a/docs/getting-started/form-controls.md b/docs/getting-started/form-controls.md index 73aff208..9f34d553 100644 --- a/docs/getting-started/form-controls.md +++ b/docs/getting-started/form-controls.md @@ -355,3 +355,18 @@ This example demonstrates custom validation styles using `data-user-invalid` and } ``` + +## Getting Associated Form Controls + +At this time, using [`HTMLFormElement.elements`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/elements) will not return Shoelace form controls because the browser is unaware of their status as custom element form controls. Fortunately, Shoelace provides an `elements()` function that does something very similar. However, instead of returning an [`HTMLFormControlsCollection`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormControlsCollection), it returns an array of HTML and Shoelace form controls in the order they appear in the DOM. + +```js +import { getFormControls } from '@shoelace-style/shoelace/dist/utilities/form.js'; + +const form = document.querySelector('#my-form'); +const formControls = getFormControls(form); + +console.log(formControls); // e.g. [input, sl-input, ...] +``` + +?> You probably don't need this function! If you're gathering form data for submission, you probably want to look at the [Data Serialization](#data-serializing) instead. diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index bd4e9715..eabbdadf 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -15,6 +15,7 @@ New versions of Shoelace are released as-needed and generally occur when a criti - Added `sl-hover` event to `` [#1125](https://github.com/shoelace-style/shoelace/issues/1125) - Added the `@documentation` tag with a link to the docs for each component - Added the `form` attribute to all form controls to allow placing them outside of a `
` element [#1130](https://github.com/shoelace-style/shoelace/issues/1130) +- Added the `getFormControls()` function as an alternative to `HTMLFormElement.elements` - Fixed a bug in `` that prevented placeholders from showing when `multiple` was used [#1109](https://github.com/shoelace-style/shoelace/issues/1109) - Fixed a bug in `` that caused tags to not be rounded when using the `pill` attribute [#1117](https://github.com/shoelace-style/shoelace/issues/1117) - Fixed a bug in `` where the `sl-change` and `sl-input` events didn't weren't emitted when removing tags [#1119](https://github.com/shoelace-style/shoelace/issues/1119) diff --git a/src/components/input/input.test.ts b/src/components/input/input.test.ts index 96b9d6b0..5055ae97 100644 --- a/src/components/input/input.test.ts +++ b/src/components/input/input.test.ts @@ -1,7 +1,8 @@ import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing'; import { sendKeys } from '@web/test-runner-commands'; import sinon from 'sinon'; -import { serialize } from '../../utilities/form'; +// @ts-expect-error - The getFormControls() function must come from the same dist +import { getFormControls, serialize } from '../../../dist/utilities/form.js'; import type SlInput from './input'; describe('', () => { @@ -235,33 +236,6 @@ describe('', () => { expect(formData.get('a')).to.equal('1'); }); - - it('should submit with the correct form when the form attribute changes (FormControlController)', async () => { - const el = await fixture(html` -
- - - Submit - -
- - Submit -
- -
- `); - const form = el.querySelector('#f2')!; - const input = document.querySelector('sl-input')!; - - input.form = 'f2'; - await input.updateComplete; - - const formData = new FormData(form); - - expect(formData.get('a')).to.equal('1'); - expect(formData.get('b')).to.be.null; - expect(formData.get('c')).to.equal('3'); - }); }); describe('when resetting a form', () => { @@ -448,4 +422,59 @@ describe('', () => { expect(input.spellcheck).to.be.false; }); }); + + describe('when using FormControlController', () => { + it('should submit with the correct form when the form attribute changes', async () => { + const el = await fixture(html` +
+
+ + Submit +
+
+ + Submit +
+ +
+ `); + const form = el.querySelector('#f2')!; + const input = document.querySelector('sl-input')!; + + input.form = 'f2'; + await input.updateComplete; + + const formData = new FormData(form); + + expect(formData.get('a')).to.equal('1'); + expect(formData.get('b')).to.be.null; + expect(formData.get('c')).to.equal('3'); + }); + }); + + describe('when using the getFormControls() function', () => { + it('should return both native and Shoelace form controls in the correct DOM order', async () => { + const el = await fixture(html` +
+ + +
+ + + + + + +
+ + +
+ `); + const form = el.querySelector('form')!; + const formControls = getFormControls(form); + + expect(formControls.length).to.equal(10); + expect(formControls.map((fc: HTMLInputElement) => fc.value).join('')).to.equal('12345678910'); + }); + }); }); diff --git a/src/internal/form.ts b/src/internal/form.ts index d9003541..f00ba3dc 100644 --- a/src/internal/form.ts +++ b/src/internal/form.ts @@ -7,7 +7,7 @@ import type { ReactiveController, ReactiveControllerHost } from 'lit'; // elements connect and disconnect to/from the DOM, their containing form is used as the key and the form control is // added and removed from the form's set, respectively. // -const formCollections: WeakMap> = new WeakMap(); +export const formCollections: WeakMap> = new WeakMap(); // // We store a WeakMap of controls that users have interacted with. This allows us to determine the interaction state diff --git a/src/utilities/form.ts b/src/utilities/form.ts index b27cbd3a..b2d79866 100644 --- a/src/utilities/form.ts +++ b/src/utilities/form.ts @@ -1,3 +1,5 @@ +import { formCollections } from '../internal/form'; + /** * Serializes a form and returns a plain object. If a form control with the same name appears more than once, the * property will be converted to an array. @@ -21,3 +23,23 @@ export function serialize(form: HTMLFormElement) { return object; } + +/** + * Returns all form controls that are associated with the specified form. Includes both native and Shoelace form + * controls. Use this function in lieu of the `HTMLFormElement.elements` property, which doesn't recognize Shoelace + * form controls. + */ +export function getFormControls(form: HTMLFormElement) { + const rootNode = form.getRootNode() as Document | ShadowRoot; + const allNodes = [...rootNode.querySelectorAll('*')]; + const formControls = [...form.elements]; + const collection = formCollections.get(form); + const shoelaceFormControls = collection ? Array.from(collection) : []; + + // To return form controls in the right order, we sort by DOM index + return [...formControls, ...shoelaceFormControls].sort((a: Element, b: Element) => { + if (allNodes.indexOf(a) < allNodes.indexOf(b)) return -1; + if (allNodes.indexOf(a) > allNodes.indexOf(b)) return 1; + return 0; + }); +}