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 '@vaadin/button';
import '@vaadin/upload';
import { html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { readAsDataURL } from 'promise-file-reader';
import { Binder } from '@vaadin/hilla-lit-form';
import type { UploadBeforeEvent } from '@vaadin/upload';
import type Contact from 'Frontend/generated/com/vaadin/demo/fusion/forms/Contact';
import ContactModel from 'Frontend/generated/com/vaadin/demo/fusion/forms/ContactModel';
import { ContactEndpoint } from 'Frontend/generated/endpoints';
@customElement('contact-form')
export class ContactForm extends LitElement {
private binder = new Binder(this, ContactModel);
@property({ type: Object })
set contact(value: Contact) {
this.binder.read(value);
}
render() {
return html`
<img src="${this.binder.model.avatarBase64.valueOf()}" alt="contact's avatar" />
<vaadin-upload
capture="camera"
accept="image/*"
max-files="1"
@upload-before="${async (e: UploadBeforeEvent) => {
const file = e.detail.file;
e.preventDefault();
const base64Image = await readAsDataURL(file);
this.binder.for(this.binder.model.avatarBase64).value = base64Image;
}}"
></vaadin-upload>
<!-- other form fields -->
<vaadin-button @click="${this.save}">Save</vaadin-button>
`;
}
async save() {
await this.binder.submitTo(ContactEndpoint.saveContact);
}
}
In the above code, the custom upload-before
listener prevents vaadin-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
statement this.binder.submitTo(saveContact)
in the save()
method.
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.