Creating the Frontend

On this page you will create the website that will display the actual todo list and access the backend using FeGen to store the todo list items.

Instantiate the API client

If you have not yet done it after the last step, run ./gradlew fegenWeb in the backend directory. You should see generated files in the frontend/src/api-client directory.

The code generated by FeGen does not know under which URL the backend can be found, so you will need to pass that information when instantiating the api client.

Replace the content of frontend/src/main.ts with the following:

import {ApiClient} from "./api-client/ApiClient";

const apiClient = new ApiClient(new FetchAdapter("http://localhost:8080"));

The second line creates a new instance of ApiClient, which is generated by FeGen and takes an instance of FetchAdapter which controls how HTTP communication is done by the generated code. It takes the base URL (the URL where the client expects find the backend) as first parameter. Since you will run the backend on the same machine for now and Spring applications by default run on port 8080 and do not offer HTTPS, the base URL is http://localhost:8080 in this case.

In a production environment, you will need to change that base url. Depending on your use case, you may want to use the same web server to deliver your frontend and handle your backend api calls. In that case, it is a good idea to set a base path in Spring that the paths of all your API URLs start with. If you use some kind of reverse proxy, you will also need to configure Spring to correctly recognize under what domain it is running. To learn how to do this, refer to the Backend Configuration page of the reference documentation.

Displaying the (empty) List of Todos

To show the todo items when opening the web app, append the following code to your frontend/src/main.ts:

/**
 * Create an HTML element representing the given todoItem
 */
const createTodoElement = (todoItem: TodoItem): HTMLElement => {
    const todoEl = document.createElement("div");

    const doneEl = document.createElement("input");
    doneEl.type = "checkbox";
    doneEl.checked = todoItem.done;
    todoEl.appendChild(doneEl);

    const textEl = document.createElement("span");
    textEl.textContent = todoItem.text;
    todoEl.appendChild(textEl);
    
    return todoEl;
}

const loadTodoItems = () => {
    // Get the <div> element that will contain the items
    const listEl = document.querySelector("#todoItems")!;
    // Remove old items if there have been any
    listEl.innerHTML = "";
    apiClient.todoItemClient.readAll().then(todoItems => {
        for (const todoItem of todoItems.items) {
            const itemEl = createTodoElement(todoItem);
            listEl.appendChild(itemEl);
        }
    }).catch(err => {
        console.error("Failed to fetch todo items", err);
        alert("Failed to fetch todo items")
    });
};

The line starting with apiClient refers to the variable you created in the previous step. It contains an instance of a class generated by FeGen. That class has a member called todoItemClient that contains method for basic CRUD functionality for the TodoItem entity like reading all instances of that entity. FeGen will create such a member for each Entity / Repository combination you define.

The readAll method returns all todo item entity that currently exist in the backend. It does not return them as a plain array though, but as a Promise, so the execution of frontend code is not interrupted while the browser is waiting for the backend to respond. Using the then method of that Promise, we can specify what to do with the todo items once they are successfully retrieved from the backend. The todoItems parameter is correctly typed using a type generated by FeGen, so you can enjoy autocompletion while iterating through the contained items array and accessing the text property that you have defined for that entity in the backend. In case something goes wrong while retrieving the todo items (for example, if the backend server cannot be reached) the callback passed to the catch block is called, and you will see a dialog box.

The method readAll actually does not return all elements by default, but only up to 20. To learn how to configure this behaviour, refer to the Paging and Sorting page of the reference documentation

There are various ways besides a simple call to readAll to retrieve entities from the backend. For example, you can pass sorting and paging parameters to readAll to only retrieve a sublist of a certain length in a specific order. You can also create projections in the backend which specify a set of fields to return, which enables you to query related entities in one step. With repository and custom searches you are able to filter the entities that are returned by the backend. To learn how you can use these options with FeGen, refer to the Retrieving Data page of the reference documentation

To actually run the code, you need to call the function that contains it, so append the following line at the bottom of main.ts:

const onReady = () => {
    loadTodoItems();
};

if (document.readyState !== 'loading') {
    // HTML is already completely initialized
    onReady();
} else {
    // Wait until initialization has completed
    document.addEventListener('DOMContentLoaded', onReady);
}

You can start the web app by running ./gradlew bootRun in the backend directory and at the same time running npm run dev in the frontend directory. However, if you open http://localhost:5000/ in your browser, you will only see the headline, because there aren't any todo items yet.

Adding Todo Items

To add todo items, we first need an input field to enter the new item's text and a button to trigger the creation. Add the following HTML just below the <h1>Todo App</h1> tag:

<div>
  <label>
    New Todo:
    <input id="newTodoText" type="text" />
  </label>
  <button id="newTodoButton">Create</button>
</div>

Now we still need to give that button some functionality. Add the following code between the loadTodoItems and the onReady function:

const createTodo = async () => {
    const text = document.querySelector<HTMLInputElement>("#newTodoText").value;
    await apiClient.todoItemClient.create({
        text,
        done: false
    });
    // Use async function and await create result to avoid loading items before creation has finished
    loadTodoItems();
};

This will read the text out of the input element and use the code generated by FeGen to create a new instance of the todo item entity with that text in the backend. After it waits for that operation to complete, it loads the todo items from the server again and updates the list.

If you are developing a real application, you should probably wrap the call to create together with the await in a try block and let the user know if an error occurred in the corresponding catch block. That is the async function equivalent to the error handling you did with catch when fetching all todo items.

To run this code whenever the Create button is clicked, add the following line at the end of the onReady function body:

document.querySelector<HTMLButtonElement>("#newTodoButton").onclick = createTodo;

If you left ./gradlew bootRun and npm run dev running, you will now be able to add todo items by entering a text and pressing Create after reloading the page once. The todo items' texts should then appear below. Try to reload the page or close and open the browser, and you will see that the todo items appear again because they are persisted in the backend.

If you stop and restart the backend, the todo items will be lost, since you are currently only using the h2 in-memory database. For applications used in a production environment, you should use a persistent database such as PostgreSQL.

Modifying Todo Items

You can already click the checkbox in front of each todo item to mark it as done, but that change will not be saved if you reload the application. To make the changes persistent, add the following code between the lines doneEl.checked = todoItem.done; and todoEl.appendChild(doneEl); within the createTodoElement function's body:

doneEl.onchange = async () => {
  await apiClient.todoItemClient.update({
    ...todoItem,
    done: doneEl.checked
  });
  loadTodoItems();
};

Now each time the checkbox is clicked, the frontend will propagate the change to the backend, so you can reload the web app and still see which todos are done.

Keep in mind that the todoItem variable does not only contain the text and done fields, but also the id field which is necessary for Spring to find the correct entity to modify. This is why just replacing ...todoItem by text: todoItem.text won't work.

Deleting Todo Items

Now that we can create todo items and mark them as done, there should also be a way to get rid of them. Add the following code just above the return todoEl within the createTodoElement function's body:

const deleteButtonEl = document.createElement("button");
deleteButtonEl.textContent = "X";
deleteButtonEl.onclick = async () => {
    await apiClient.todoItemClient.delete(todoItem);
    loadTodoItems();
};
todoEl.appendChild(deleteButtonEl);

The items in the list will now not only consist of a text, but also a button to delete the todo item.