Start

Tutorial: a notes app

Build a small notes app step by step: a Java data class, a page of separate files and the bridge between them.

In this tutorial you build a small notes app: type a note, add it to a list, delete it. It is tiny on purpose. The point is to see every piece of a Jolt app in one place: a Java class that owns the data, a page made of separate files, and a bridge between them. The code on this page was run on Windows against a real window.

You should have read Getting started and Messaging first. The tutorial uses the Rpc class from the messaging page unchanged.

What we are building

  • The page shows an input, an Add button and the list. It knows nothing about storage.
  • The Java side keeps the notes and offers three functions: notes.list, notes.add, notes.remove.
  • The bridge is the Rpc class in Java and the call() function in the page.

This is the same shape as a large app: swap the in-memory list for a database and the plain page for Vue or React, and nothing else changes.

Step 1: the project

Use the pom.xml from Getting started, and add Gson, which Rpc uses for JSON:

<dependency>
  <groupId>com.google.code.gson</groupId>
  <artifactId>gson</artifactId>
  <version>2.11.0</version>
</dependency>

The folders:

notes/
  pom.xml
  src/main/java/com/example/notes/
    NotesApp.java
    NoteStore.java
    Rpc.java            <- copy it from the Messaging page
  src/main/resources/ui/
    index.html
    style.css
    app.js

Step 2: the data, in plain Java

Start with the part that has nothing to do with windows. A note is a record, and the store keeps a list of them:

package com.example.notes;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;

public final class NoteStore {
    public record Note(int id, String text) {}

    private final List<Note> notes = new CopyOnWriteArrayList<>();
    private final AtomicInteger nextId = new AtomicInteger(1);

    public List<Note> list() { return List.copyOf(notes); }

    public Note add(String text) {
        if (text == null || text.isBlank()) throw new IllegalArgumentException("A note cannot be empty");
        Note note = new Note(nextId.getAndIncrement(), text.strip());
        notes.add(note);
        return note;
    }

    public void remove(int id) { notes.removeIf(n -> n.id() == id); }
}

The collections are thread-safe because Rpc runs your functions on worker threads, possibly at the same time. Throwing IllegalArgumentException for an empty note is how we will report a mistake to the page.

Step 3: the window and the bridge

package com.example.notes;

import io.github.b077as.jolt.*;

public final class NotesApp {
    public static void main(String[] args) {
        NoteStore store = new NoteStore();
        Application app = Application.create(AppOptions.of("com.example.notes"));
        try {
            app.run(() -> {
                WebWindow window = app.createWindow(WindowOptions.defaults()
                        .withTitle("Notes").withSize(520, 640).withDevTools(true));

                new Rpc(window)
                        .on("notes.list", params -> store.list())
                        .on("notes.add", params -> store.add(params.get("text").getAsString()))
                        .on("notes.remove", params -> { store.remove(params.get("id").getAsInt()); return true; });

                window.loadResources(AppResources.classpath("ui"));
            });
        } finally {
            app.close();
        }
    }
}

Read it top to bottom: build the store, create the app, in run create a window, register three Java functions on it, then show the files in ui. withDevTools(true) is there so you can inspect the page while learning; remove it for release.

Step 4: the page

Three small files in src/main/resources/ui.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Notes</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <h1>Notes</h1>
  <form id="form">
    <input id="text" placeholder="Write a note" autocomplete="off">
    <button id="add">Add</button>
  </form>
  <p id="error" role="alert"></p>
  <ul id="list"></ul>
  <script type="module" src="app.js"></script>
</body>
</html>
body { font-family: system-ui, sans-serif; margin: 2rem auto; max-width: 28rem; }
form { display: flex; gap: .5rem; }
input { flex: 1; padding: .5rem; }
li { display: flex; justify-content: space-between; padding: .4rem 0; border-bottom: 1px solid #ddd; }
#error { color: #b00020; min-height: 1.2em; }
// --- the bridge: call a Java function and wait for its answer ---
const pending = new Map();
let nextId = 1;

jolt.onMessage(raw => {
  const reply = JSON.parse(raw);
  const waiting = pending.get(reply.id);
  if (!waiting) return;
  pending.delete(reply.id);
  if (reply.error) waiting.reject(new Error(reply.error));
  else waiting.resolve(reply.result);
});

function call(method, params = {}) {
  return new Promise((resolve, reject) => {
    const id = nextId++;
    pending.set(id, { resolve, reject });
    jolt.postMessage(JSON.stringify({ id, method, params }));
  });
}

// --- the page ---
const list = document.getElementById('list');
const input = document.getElementById('text');
const error = document.getElementById('error');

function show(note) {
  const li = document.createElement('li');
  const span = document.createElement('span');
  span.textContent = note.text;                 // textContent, never innerHTML
  const del = document.createElement('button');
  del.textContent = 'Delete';
  del.onclick = async () => { await call('notes.remove', { id: note.id }); li.remove(); };
  li.append(span, del);
  list.append(li);
}

document.getElementById('form').addEventListener('submit', async event => {
  event.preventDefault();
  error.textContent = '';
  try {
    show(await call('notes.add', { text: input.value }));
    input.value = '';
  } catch (e) {
    error.textContent = e.message;
  }
});

(await call('notes.list')).forEach(show);

The first half of app.js is the bridge you already know. The second half is the app:

  • Adding a note calls Java and shows what Java returns (including the id Java chose). If Java throws, the promise rejects and the message appears in the red paragraph.
  • The last line asks Java for the saved notes when the page starts. It also shows the pattern from the messaging page: the page speaks first, so it is never too early for the answer.
  • Note text goes into the page with textContent. Setting innerHTML with text a user typed is how scripts get injected.

Step 5: run it

Run NotesApp with --enable-native-access=ALL-UNNAMED, as on the first page. Type a note and press Enter: it appears in the list. Press Add with an empty box: the error from your Java code shows in red. Delete removes a note.

Notes vanish when you close the window because they live in a list. Replacing NoteStore with one that writes a file in the app's data folder (AppOptions.dataDirectory()) or a database changes nothing in the page.

What you used

PieceWhere it is explained
Application, createWindow, runGetting started
Messages and the Rpc layerMessaging
loadResources and the ui folderBuilding the frontend
Threads and the UI threadHow Jolt works

Where to go from here