Hilla Documentation

Submitting Images in Forms

Allowing images to be uploaded 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 tutorial 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 tutorial 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 dev.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 { html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';

import '@vaadin/button';
import '@vaadin/upload';
import type { UploadBeforeEvent } from '@vaadin/upload';

import 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';

import { Binder } from '@hilla/form';

import { readAsDataURL } from 'promise-file-reader';

@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}" 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. Please note that this approach is not suitable for larger files. A streamed upload may be more appropriate.