Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ coverage
node_modules
dist
.npmrc
.idea
23 changes: 17 additions & 6 deletions src/element-internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
validationMessageMap,
} from './maps';
import {
createFileList,
createHiddenFileInput,
createHiddenInput,
findParentForm,
initRef,
Expand Down Expand Up @@ -165,23 +167,32 @@ export class ElementInternals implements IElementInternals {
}

/** Sets the element's value within the form */
setFormValue(value: string | FormData | null): void {
setFormValue(value: string | FormData | File): void {
const ref = refMap.get(this);
throwIfNotFormAssociated(ref, `Failed to execute 'setFormValue' on 'ElementInternals': The target element is not a form-associated custom element.`);
removeHiddenInputs(this);
if (value != null && !(value instanceof FormData)) {
if (value instanceof File) {
if (ref.getAttribute('name')) {
const hiddenInput = createHiddenInput(ref, this);
hiddenInput.value = value;
const hiddenInput = createHiddenFileInput(ref, this);
hiddenInput.files = createFileList([value]);
}
} else if (value != null && value instanceof FormData) {
} else if (value instanceof FormData) {
Array.from(value).reverse().forEach(([formDataKey, formDataValue]) => {
if (typeof formDataValue === 'string') {
const hiddenInput = createHiddenInput(ref, this);
hiddenInput.name = formDataKey;
hiddenInput.value = formDataValue;
} else if (formDataValue instanceof File) {
const hiddenInput = createHiddenFileInput(ref, this);
hiddenInput.name = formDataKey;
hiddenInput.files = createFileList([formDataValue]);
}
});
} else if (value != null) {
if (ref.getAttribute('name')) {
const hiddenInput = createHiddenInput(ref, this);
hiddenInput.value = value;
}
}
refValueMap.set(ref, value);
}
Expand Down Expand Up @@ -262,7 +273,7 @@ export class ElementInternals implements IElementInternals {
declare global {
interface CustomElementConstructor {
formAssociated?: boolean;
}
}

interface Window {
ElementInternals: typeof ElementInternals
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export interface IElementInternals extends IAom {
form: HTMLFormElement;
labels: LabelsList;
reportValidity: () => boolean;
setFormValue: (value: string | FormData | null) => void;
setFormValue: (value: string | FormData | File) => void;
setValidity: (
validityChanges: Partial<globalThis.ValidityState>,
validationMessage?: string,
Expand Down
30 changes: 30 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,36 @@ export const createHiddenInput = (ref: ICustomElement, internals: IElementIntern
return input;
}

/**
* Creates a hidden file input for the given ref
* @param {ICustomElement} ref - The element to watch
* @param {IElementInternals} internals - The element internals instance for the ref
* @return {HTMLInputElement} The hidden input
*/
export const createHiddenFileInput = (ref: ICustomElement, internals: IElementInternals): HTMLInputElement | null => {
const input = document.createElement('input');
input.type = 'file';
input.name = ref.getAttribute('name');
input.style.display = 'none !important';
input.setAttribute('hidden', '');
ref.after(input);
hiddenInputMap.get(internals).push(input);
return input;
}

/**
* Creates a FileList instance from the given files.
* @param {File[]} files
* @return {FileList}
*/
export const createFileList = (files: File[]): FileList => {
const dt = new DataTransfer()
files.forEach(file => {
dt.items.add(file);
});
return dt.files;
}

/**
* Initialize a ref by setting up an attribute observe on it
* looking for changes to disabled
Expand Down
42 changes: 42 additions & 0 deletions static/file.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!doctype html>
<html>
<head>
<title>Appended via tree test</title>
<script type="module">
import '../dist/index.js';

customElements.define('my-file-upload', class extends HTMLElement {
static formAssociated = true;
internals = this.attachInternals();
});
</script>
</head>
<body>
<form id="form">
<input type="file" name="orig" required id="original" onchange="dupeFile()">
<my-file-upload name="file" required id="custom"></my-file-upload>
<button type="submit">submit</button>
</form>

<script>
let original, custom;

window.addEventListener('load', () => {
original = document.querySelector('#original');
custom = document.querySelector('#custom');
});

function dupeFile() {
custom.internals.setFormValue(original.files[0] || '');
}

document.querySelector('#form').addEventListener('submit', e => {
event.preventDefault();
const formData = new FormData(event.target);
// The input with the name "file" should contain the File from the original input
console.log('file = ', formData.get('file'));
return false;
})
</script>
</body>
</html>
49 changes: 49 additions & 0 deletions test/FormValueFile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { expect, fixture, fixtureCleanup, html } from '@open-wc/testing';
import '../dist/index.js';

class TestFileUpload extends HTMLElement {
static formAssociated = true;
internals = this.attachInternals();
}

customElements.define('test-file-upload', TestFileUpload);

async function createForm(): Promise<HTMLFormElement> {
return await fixture<HTMLFormElement>(html`
<form id="form">
<test-file-upload name="foo" id="foo"></test-file-upload>
<button type="submit">Submit</button>
</form>`);
}

describe('setFormValue polyfill with File type', () => {
let form, el, file;

beforeEach(async () => {
form = await createForm();
el = form.querySelector('#foo') as TestFileUpload;
file = new File(['foo'], 'foo.txt', { type: 'text/plain' });
});

afterEach(async () => {
await fixtureCleanup();
});

it('must submit a file', async () => {
el.internals.setFormValue(file);

const result = new FormData(form);
expect(result.get('foo')).to.equal(file);
});

it('must submit multiple files through FormData', async () => {
const formData = new FormData();
formData.append('foo', file);
formData.append('foo', file); // append the same file twice

el.internals.setFormValue(formData);

const result = new FormData(form);
expect(result.getAll('foo')).to.deep.equal([file, file]);
});
});
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"declaration": true,
"declarationDir": "dist",
"outDir": "dist",
"rootDir": "src"
"rootDir": "src",
"moduleResolution": "Node"
},
"lib": ["dom", "ESNext"],
"include": ["./src/**/*"],
Expand Down