Submitting Images in Client-Side Forms

A common use case is a form field that allows uploading an image. For instance, an application may let users customize their avatars. This tutorial shows how this can be implemented for client-side 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 app provides editing of a contact card. It assumes a server-side bean where the image is stored as a Base64-encoded string in the avatarBase64 field:

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.fusion.Endpoint;

@Endpoint
public class ContactEndpoint {
  // other endpoint methods: read, delete, ...

  public void saveContact(Contact contact) {
    // persistently store the contact
  }
}

The following client-side 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/vaadin-upload';
import '@vaadin/vaadin-button';

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 '@vaadin/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: CustomEvent) => {
          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 reading it into a Base64-encoded string and updating 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.

Only after submitting the Contact instance to the saveContact endpoint (the statement this.binder.submitTo(saveContact) in the save method), 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 Fusion’s form binder and endpoints. Please note that this approach is not suitable for larger files (a streamed upload may be more appropriate).