Submitting Images in Forms
A common use case is a form field that allows an image to be uploaded. For instance, an application may let users customize their avatars. This page shows how this can be implemented for forms, using the vaadin-upload
component for local file selection (by file browser or dragging), but postponing the actual server upload until the form is submitted.
The example application allows a contact card to be edited. It assumes a server-side bean where the image is stored as a Base64-encoded string in the field avatarBase64
:
package com.vaadin.demo.fusion.forms;
/**
* Contact card with base64-encoded image
*/
public class Contact {
// other contact fields: name, address, phone, ...
private String avatarBase64;
public String getAvatarBase64() {
return avatarBase64;
}
public void setAvatarBase64(String avatarBase64) {
this.avatarBase64 = avatarBase64;
}
}
It also assumes that the server exposes an endpoint for saving updated Contact
instances:
package com.vaadin.demo.fusion.forms;
import com.vaadin.hilla.Endpoint;
@Endpoint
public class ContactEndpoint {
// other endpoint methods: read, delete, ...
public void saveContact(Contact contact) {
// persistently store the contact
}
}
The following form binds the avatarBase64
field of the instance to a vaadin-upload
component:
import ContactModel from 'Frontend/generated/com/vaadin/demo/fusion/forms/ContactModel';
import { ContactEndpoint } from 'Frontend/generated/endpoints';
import { useForm } from '@vaadin/hilla-react-form';
import { useEffect } from 'react';
import { Button } from '@vaadin/react-components/Button.js';
import { Upload, type UploadBeforeEvent } from '@vaadin/react-components/Upload.js';
import { readAsDataURL } from 'promise-file-reader';
export default function ContactForm() {
const form = useForm(ContactModel, {
onSubmit: async (contact) => {
await ContactEndpoint.saveContact(contact);
},
});
useEffect(() => {
ContactEndpoint.loadContact().then(form.read);
}, []);
return (
<div>
<img src={form.value?.avatarBase64} alt="contact's avatar" />
<Upload
capture="camera"
accept="image/*"
max-files="1"
onUploadBefore={async (e: UploadBeforeEvent) => {
const file = e.detail.file;
e.preventDefault();
if (form.value) {
form.value.avatarBase64 = await readAsDataURL(file);
}
}}
/>
<Button onClick={form.submit}>Save</Button>
</div>
);
}
In the above code, the custom onUploadBefore
listener prevents Upload
from uploading the received file to the server. Instead, it reads the file into a Base64-encoded string and updates the form field avatarBase64
via the Binder
. The small promise-file-reader
library wrapping FileReader
inside a promise is used here to handle the result synchronously.
The Contact
instance is first submitted to the saveContact()
endpoint. This happens through the
binder.submit
that is called upon clicking the Save button which executes the provided logic for onSubmit
while obtaining the binder instance.
Only then does the server-side endpoint implementation receive the image string. The server can then choose to recode the image for more efficient storage, if necessary.
The advantage of using the string type is simplicity; you can use the built-in serialization mechanism of Hilla’s form binder and endpoints. This approach isn’t suitable for larger files. A streamed upload may be more appropriate.