6. File

Creating a widget to upload a file is relatively straightforward. It can be as simple as the following two lines of code, along with including a @File data model property field:

<? register input @File ?>
<input type="file" name="<?attr OutputId(@File) ?>">

However, it becomes a bit trickier when working with dynamically created binary data instead of a static file on the desktop. Nowadays, in JavaScript, you can only create a binary file programmatically because browsers have become more restricted due to security measures. Additionally, you can only assign a file to a file input through user interaction, such as a button click.

Let’s first look at how to do it the traditional way, and then follow that up with a more modern approach.

If you examine the code below, you may wonder where the base64 option in the register input comes into play. The reason for this is legacy. Previously, JavaScript did not have an out-of-the-box solution for creating native files; it only offered emulation using a string or an array of numbers. This is how WEM could provide a way to upload files in base64 format. Let’s examine our script below using base64 as an option:

<? register input @File base64 = true ?>
<input type="text" id="<?attr OutputId(@File) ?>" name="<?attr OutputId(@File) ?>">
<button id="<?attr OutputId(@File) ?>-btn">Create binary data</button>
<? startupscript ?>
	
	function clickHandler() {
		const helloFile = createHelloFile("hello.txt");

		// Assign the text field with the formatted contents of `helloFile`.
		fileInputEl.value = helloFile;
	}

	function createHelloFile(fileName) {
		if (fileName.includes(";"))
			throw new Error("Filename cannot contain a ';'.");

		// Create Base64 representation of the text "Hello".
		const encoded = btoa("Hello");
		
		// Return the emulated file in the format "filename;base64 data".
        return `${fileName};${encoded}`;
	}

	const fileInputEl = document.getElementById(<?js OutputId(@File) ?>);
	
	const btnEl = document.getElementById(<?js OutputId(@File) ?> + "-btn");
	btnEl.addEventListener("click", () => clickHandler());

<? end ?>

Looks pretty cool, right? You simply encode the "Hello" text, and you're good to go. However, be aware that several issues can arise if you're not careful; we are using a simple "Hello" text as an example. Let’s walk through the process.

First, we add base64 = true to our register input, which is straightforward. However, note that we changed our input to a text field using <input type="text">. We are no longer working with files but rather with text that simulates a file. This can become cumbersome when dealing with large files, especially when using the btoa() function to encode the string into base64 format. Additionally, encoding data into base64 increases the size of the contents by approximately 35%.

Another important point is that we are using a string to hold our "Hello" data. In JavaScript, this string is in Unicode. However, Base64, by design, expects binary data as its input. In terms of JavaScript strings, this means that each character's code point should occupy only one byte. If you pass a string like "a Ā 𐀀 文 🦄" into btoa(), which contains characters that occupy more than one byte, you will encounter an error.

Finally, what happens when we call clickHandler() directly without user interaction? Since we are using a regular text field to set its contents with a string value, there is no error in that case.

What does a modern version look like without using the base64 option?

<? register input @File ?>
<input type="file" id="<?attr OutputId(@File) ?>" name="<?attr OutputId(@File) ?>">
<button id="<?attr OutputId(@File) ?>-btn">Create binary data</button>
<? startupscript ?>
	
	function clickHandler() {
		const helloFile = createHelloFile("hello.txt");

		// Use the DataTransfer to temporarily create a file list for the file input.
		const dataTransfer = new DataTransfer();
		dataTransfer.items.add(helloFile);

		// Assign the file input with the file list from the data transfer.
		fileInputEl.files = dataTransfer.files;
	}

	function createHelloFile(fileName) {
		// Binary data that represents "Hello" in ASCII.
        const binaryData = new Uint8Array([72, 101, 108, 108, 111]);

		// We need a Blob to hold our binary data.
		const blob = new Blob([binaryData], { type: "application/octet-stream" });

		// Finally, return a plain text file with a custom file name.
        return new File([blob], fileName, { type: "text/plain" });
	}

	const fileInputEl = document.getElementById(<?js OutputId(@File) ?>);
	
	const btnEl = document.getElementById(<?js OutputId(@File) ?> + "-btn");
	btnEl.addEventListener("click", () => clickHandler());
	
<? end ?>

In this version, we have removed the base64 option from our register input, and we now have a <input type="file"> again. However, our JavaScript code now utilizes several new classes that we didn't have before, such as Uint8Array, Blob, File, and DataTransfer, each serving its own purpose. By reading the comments in the code above, it becomes clear how to programmatically create a file containing binary data and how to set the file input with that file.

Last updated

Was this helpful?