Skip to content

Commit a0b2890

Browse files
authored
feat(setFormValue): Add support for FormData to allow settings multiple form values
* feature(setFormValue): Added support to set FormData to allow settings multiple form values * fix(setFormValue): make sure FormData values are of string type, File and other Blob-like objects are not supported at the moment * fix(setFormValue): added support for multi-value keys and skip form elements without a name * fix(types): Added missing anchor argument to IElementInternals.setValidity interface
1 parent b074759 commit a0b2890

File tree

6 files changed

+120
-24
lines changed

6 files changed

+120
-24
lines changed

Diff for: src/element-internals.ts

+32-9
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
hiddenInputMap
1010
} from './maps';
1111
import { initAom } from './aom';
12-
import { getHostRoot, initRef, initLabels, initForm, findParentForm } from './utils';
12+
import { getHostRoot, initRef, initLabels, initForm, findParentForm, createHiddenInput, removeHiddenInputs } from './utils';
1313
import { ValidityState, reconcileValidty, setValid } from './ValidityState';
1414
import { observerCallback, observerConfig } from './mutation-observers';
1515
import { IElementInternals, ICustomElement, LabelsList } from './types';
@@ -117,15 +117,26 @@ export class ElementInternals implements IElementInternals {
117117
}
118118

119119
/** Sets the element's value within the form */
120-
setFormValue(value: string): void {
121-
const hiddenInput = hiddenInputMap.get(this);
122-
if (hiddenInput) {
123-
hiddenInput.value = value;
124-
}
125-
if (!this.form) {
120+
setFormValue(value: string | FormData): void {
121+
const ref = refMap.get(this);
122+
if (!this.form || !ref.constructor['formAssociated']) {
126123
return undefined;
127124
}
128-
const ref = refMap.get(this);
125+
removeHiddenInputs(this);
126+
if (typeof value === 'string') {
127+
if (ref.getAttribute('name')) {
128+
const hiddenInput = createHiddenInput(ref, this);
129+
hiddenInput.value = value;
130+
}
131+
} else if (value != null) {
132+
value.forEach((formDataValue, formDataKey) => {
133+
if (typeof formDataValue === 'string') {
134+
const hiddenInput = createHiddenInput(ref, this);
135+
hiddenInput.name = formDataKey;
136+
hiddenInput.value = formDataValue;
137+
}
138+
});
139+
}
129140
refValueMap.set(ref, value);
130141
}
131142

@@ -226,7 +237,19 @@ if (!window.ElementInternals) {
226237
refs.forEach(ref => {
227238
if (ref.getAttribute('name')) {
228239
const value = refValueMap.get(ref);
229-
data.set(ref.getAttribute('name'), value);
240+
if (typeof value === 'string') {
241+
data.set(ref.getAttribute('name'), value);
242+
} else if (value != null) {
243+
const keysAdded = [];
244+
value.forEach((formDataValue, formDataKey) => {
245+
if (keysAdded.includes(formDataKey)) {
246+
data.append(formDataKey, formDataValue);
247+
} else {
248+
data.set(formDataKey, formDataValue);
249+
keysAdded.push(formDataKey);
250+
}
251+
})
252+
}
230253
}
231254
});
232255
}

Diff for: src/maps.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const refMap = new WeakMap<IElementInternals, ICustomElement>();
1212
export const validityMap = new WeakMap<IElementInternals, ValidityState>();
1313

1414
/** Use an ElementInternals instance to get its attached input[type="hidden"] */
15-
export const hiddenInputMap = new WeakMap<IElementInternals, HTMLInputElement>();
15+
export const hiddenInputMap = new WeakMap<IElementInternals, HTMLInputElement[]>();
1616

1717
/** Use a custom element to get its attached ElementInternals instance */
1818
export const internalsMap = new WeakMap<ICustomElement, IElementInternals>();

Diff for: src/mutation-observers.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { internalsMap, shadowHostsMap, upgradeMap, hiddenInputMap } from './maps.js';
22
import { aom } from './aom.js';
3-
import { initForm, initLabels } from './utils.js';
3+
import { removeHiddenInputs, initForm, initLabels } from './utils.js';
44
import { ICustomElement } from './types.js';
55

66
export function observerCallback(mutationList) {
@@ -36,7 +36,7 @@ export function observerCallback(mutationList) {
3636
const internals = internalsMap.get(node);
3737
/** Clean up any hidden input elements left after an element is disconnected */
3838
if (internals && hiddenInputMap.get(internals)) {
39-
hiddenInputMap.get(internals).remove();
39+
removeHiddenInputs(internals);
4040
}
4141
/** Disconnect any unneeded MutationObservers */
4242
if (shadowHostsMap.has(node)) {

Diff for: src/types.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ export interface IElementInternals extends IAom {
4242
form: HTMLFormElement;
4343
labels: NodeListOf<HTMLLabelElement>|[];
4444
reportValidity: () => boolean;
45-
setFormValue: (value: string) => void;
46-
setValidity: (validityChanges: Partial<globalThis.ValidityState>, validationMessage?: string) => void;
45+
setFormValue: (value: string | FormData) => void;
46+
setValidity: (validityChanges: Partial<globalThis.ValidityState>, validationMessage?: string, anchor?: HTMLElement) => void;
4747
validationMessage: string;
4848
validity: globalThis.ValidityState;
4949
willValidate: boolean;

Diff for: src/utils.ts

+29-7
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,34 @@ export const getHostRoot = (node: Node): Node&ParentNode => {
2828
return parent;
2929
};
3030

31+
/**
32+
* Removes all hidden inputs for the given element internals instance
33+
* @param {IElementInternals} internals - The element internals instance
34+
* @return {void}
35+
*/
36+
export const removeHiddenInputs = (internals: IElementInternals): void => {
37+
const hiddenInputs = hiddenInputMap.get(internals);
38+
hiddenInputs.forEach(hiddenInput => {
39+
hiddenInput.remove();
40+
});
41+
hiddenInputMap.set(internals, []);
42+
}
43+
44+
/**
45+
* Creates a hidden input for the given ref
46+
* @param {ICustomElement} ref - The element to watch
47+
* @param {IElementInternals} internals - The element internals instance for the ref
48+
* @return {HTMLInputElement} The hidden input
49+
*/
50+
export const createHiddenInput = (ref: ICustomElement, internals: IElementInternals): HTMLInputElement | null => {
51+
const input = document.createElement('input');
52+
input.type = 'hidden';
53+
input.name = ref.getAttribute('name');
54+
ref.after(input);
55+
hiddenInputMap.get(internals).push(input);
56+
return input;
57+
}
58+
3159
/**
3260
* Initialize a ref by setting up an attribute observe on it
3361
* looking for changes to disabled
@@ -36,13 +64,7 @@ export const getHostRoot = (node: Node): Node&ParentNode => {
3664
* @return {void}
3765
*/
3866
export const initRef = (ref: ICustomElement, internals: IElementInternals): void => {
39-
if (ref.constructor['formAssociated']) {
40-
const input = document.createElement('input');
41-
input.type = 'hidden';
42-
input.name = ref.getAttribute('name');
43-
ref.after(input);
44-
hiddenInputMap.set(internals, input);
45-
}
67+
hiddenInputMap.set(internals, []);
4668
observer.observe(ref, observerConfig);
4769
};
4870

Diff for: test/ElementInternals.test.js

+54-3
Original file line numberDiff line numberDiff line change
@@ -130,19 +130,21 @@ describe('The ElementInternals polyfill', () => {
130130
});
131131

132132
describe('inside a custom element with a form', () => {
133-
let form, el, label, button, internals;
133+
let form, el, noname, label, button, internals;
134134

135135
beforeEach(async () => {
136136
form = await fixture(html`
137137
<form id="form">
138138
<label for="foo">Label text</label>
139139
<test-el name="foo" id="foo"></test-el>
140+
<test-el id="noname"></test-el>
140141
<button type="submit">Submit</button>
141142
</form>
142143
`);
143144
callCount = 0;
144145
label = form.querySelector('label');
145-
el = form.querySelector('test-el');
146+
el = form.querySelector('test-el[id=foo]');
147+
noname = form.querySelector('test-el[id=noname]');
146148
button = form.querySelector('button');
147149
internals = el.internals;
148150
});
@@ -254,7 +256,7 @@ describe('The ElementInternals polyfill', () => {
254256
// Lifecycle methods are stripped off at definition time
255257
// and added elsewhere so we can't use a spy. Instead
256258
// we're going to look for a side-effect
257-
expect(callCount).to.equal(1);
259+
expect(callCount).to.equal(2);
258260
});
259261

260262
it('will cancel form submission if invalid', (done) => {
@@ -286,5 +288,54 @@ describe('The ElementInternals polyfill', () => {
286288
it('will call formAssociatedCallback after internals have been set', () => {
287289
expect(internalsAvailableInFormAssociatedCallback).to.be.true;
288290
})
291+
292+
it('will not include null values set via setFormValue', () => {
293+
internals.setFormValue('test');
294+
internals.setFormValue(null);
295+
const output = new FormData(form);
296+
expect(Array.from(output.keys()).length).to.equal(0);
297+
})
298+
299+
it('will not include undefined values set via setFormValue', () => {
300+
internals.setFormValue('test');
301+
internals.setFormValue(undefined);
302+
const output = new FormData(form);
303+
expect(Array.from(output.keys()).length).to.equal(0);
304+
})
305+
306+
it('will include multiple form values passed via FormData to setFormValue', () => {
307+
let input;
308+
let output;
309+
input = new FormData();
310+
input.set('first', '1');
311+
input.set('second', '2');
312+
input.append('second', '22'); // Multi-value keys should also work
313+
internals.setFormValue(input);
314+
output = new FormData(form);
315+
expect(Array.from(output.values()).length).to.equal(3);
316+
expect(output.get('first')).to.equal('1');
317+
expect(output.getAll('second').length).to.equal(2);
318+
input = new FormData();
319+
input.set('override', '3');
320+
internals.setFormValue(input);
321+
output = new FormData(form);
322+
expect(Array.from(output.keys()).length).to.equal(1);
323+
expect(output.get('override')).to.equal('3');
324+
})
325+
326+
it('will not include form values from elements without a name', () => {
327+
noname.internals.setFormValue('noop');
328+
const output = new FormData(form);
329+
expect(Array.from(output.keys()).length).to.equal(0);
330+
})
331+
332+
it('will include form values from elements without a name if set with FormData', () => {
333+
const formData = new FormData();
334+
formData.set('formdata', 'works');
335+
noname.internals.setFormValue(formData);
336+
const output = new FormData(form);
337+
expect(Array.from(output.keys()).length).to.equal(1);
338+
expect(output.get('formdata')).to.equal('works');
339+
})
289340
});
290341
});

0 commit comments

Comments
 (0)