Quarkus Web Application with SvelteJS - Part 3

Part 3 - Adding a SvelteJS Front End

Introduction

Hopefully you have been following along, and you have built a working ToDo List API, fully database backed, and with a robust unit testing framework in place too. If not, you may want to go back to Part 1.

Let’s finish the whole thing off with a front end to make it a complete application.

SvelteJS

Just like I didn’t try to explain the background of Quarkus, I am not going to explain SvelteJS either. If you want to read about it, just head over to svelte.dev to read all about it. However, what I like about Svelte is that it compiles down to native JavaScript which gives super-fast performance and hugely streamlined download size. I also find it intuitive, and favour it over the alternatives (currently at least, and I do change my mind a lot on front-end frameworks).

I used a tutorial from Freshman.tech to give a head-start building the app. If you are interested in learning Svelte in a little more detail, head over to https://freshman.tech/svelte-todo/.

Integrating a Front End with Quarkus

One thing to note is that Quarkus by default enforces CORS, so expects frontend requests to come from the same domain (which includes the port) of the Quarkus app. To achieve this, we can do one of a few things

  1. Copy the compiled SvelteJS application to the resources directory of the Quarkus app, so that it is served from Quarkus
  2. Run Quarkus / SvelteJS separately, and put a reverse proxy (e.g Nginx) in front, allowing both to look like they are being served from the same.
  3. Turn off CORS

My recommendation would usually option 2, as it is reasonably straightforward, and it also allows both Quarkus and Svelte to be running in Dev mode with hot-reloading. For our app though, we will go with option 1 as it is the easiest approach and doesn’t bring another technology into play. I don’t recommend option 3, because CORS is a best practice for web applications.

Let’s Get Started!

Svelte uses NPM to setup and run the necessary toolset. So, at the root of our project, let’s run the following to set it up.

npx degit sveltejs/template todo-frontend
cd todo-frontend/
npm install

If we were using option 2 above, we could go into Dev mode now, and start hacking out our code. To do this we would run npm run dev , but we will go into our code editor, and when done create a compiled version of our app.

We need to head over to todo-frontend\src\App.svelte and open it in our editor. We won’t need to edit any other files as part of our tutorial (unless the default title bothers you, which you can modify in the public\index.html file.

A Svelte component (our app will be a single encapsulated component), has three sections within the file…the script, the style, and the main (which is where our HTML goes). Delete the contents of the App.svelte file, and add the following code.

<style>
  .todo-list { list-style: none; margin-bottom: 20px;}
  .done span { text-decoration: line-through; 
  .done .tick::before { display: inline; }
  .tick::before { content: '✓'; font-size: 20px; display: none; }
  .tick {
    width: 30px; height: 30px; border: 3px solid #333; 
    border-radius: 50%; display: inline-flex; 
    justify-content: center; align-items: center; cursor: pointer;
  }
  .delete-todo {
    border: none; background-color: transparent; outline: none; 
    cursor: pointer;
  }
<main>
  <div class="container">
    <h1 class="app-title">My Todos</h1>
      <ul class="todo-list">
      {#each todoItems as todo}
        <li class="todo-item {todo.completed ? 'done' : ''}">
          <label class="tick" on:click={() => toggleDone(todo.id)}></label>
          <span>{todo.txt}</span>
          <button class="delete-todo" on:click={() => deleteTodo(todo.id)}> X </button>
        </li>
      {/each}
    </ul>
   <form on:submit|preventDefault={addTodo}>
      <input class="js-todo-input" type="text" aria-label="Enter a new todo" placeholder="Enter your todo" bind:value={newTodo}>
    </form>
  </div>
</main>

If you are familiar with front end frameworks, this should look reasonably familiar. The styling is configured to strikethrough text and display a tick inside of the circular checkbox (a label with a radius of 50%). The CSS is set using a Handlebars style code { somelogic ? "output if true":"else output if false" }, and events are managed in two different ways

  1. Clicking on the label or button executes a method using the on:click= approach, which then executes a function which we will create in the next bit.
  2. When the Enter key is pressed, it executes {addTodo} as a result of the on:submit. PreventDefault prevents the Form from submitting through standard HTML. However, when the addTodo function is called, it will read the value from newTodo which has been updated automatically due to the bind:value={newTodo} . The bind value will ensure that any changes to the input is automatically updated in the value stored in the javascript. This is done by the compiled code, and not through a shadowDOM like the other popular frameworks (Angular, React, VUE), and is why Svelte is very low in memory utilisation.

So now that we have our HTML and CSS code, it’s time to add our front end logic. At the top of the same file, add the following code.

<script>
// Svelte code based on https://freshman.tech/svelte-todo/
import { afterUpdate } from ‘svelte’;
import { onMount } from “svelte”;
let todoItems = [];
let newTodo = ‘’;
onMount(async function() {
  const response = await fetch(“/todos”);
  const json = await response.json();
  todoItems = json;
});
afterUpdate(() => {
  document.querySelector(‘.js-todo-input’).focus();
});
</script>

This code creates the two variables that we need for our app. The first is the list of the Todos, and the second is the bind variable for the input field.

The next function reads the current Todos (getAll) from our API that we created in Parts 1 and 2, and stores it in the todoItems list.

And finally, the afterUpdate function ensures that focus is returned to the input field after state is changed, so that we don’t have to keep clicking back on the input field.

Next we have to add the addTodo, toggleDone, deleteTodo functions. So, before the closing script tag, add the remaining code.

async function addTodo() {
  newTodo = newTodo.trim();
  if (!newTodo) return;
  const response = await fetch("/todos", { method: 'POST', headers: {'Content-Type': 'application/json'}, body: newTodo });
  const todo = await response.json();
  todoItems = […todoItems, todo];
  newTodo = '';
}
function toggleDone(id) {
  const index = todoItems.findIndex(item => item.id === Number(id));
  todoItems[index].completed = !todoItems[index].completed;
  update(todoItems[index])
}
async function update(todo) {
   const response = await fetch("/todos/"+todo.id, { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(todo) });
   const updatedTodo = await response.json();
   todo = updatedTodo;
}
async function deleteTodo(id) {
  const response = await fetch("/todos/"+id, { method: 'DELETE' });
  const success = await response.ok;
  if (success) todoItems = todoItems.filter(item => item.id !== Number(id));
}

Again, this code should look like plain old Javascript, so there shouldn’t be too much to discuss. The addTodo function checks that there is is data, then calls a fetch statement to our Quarkus API using an HTTP POST. When the full Todo (including the ID) is returned, it is added to the todoItems list and finally the bindValue newTodo is cleared, so that we can add our next Todo.

DeleteTodo simply calls the HTTP DELETE method, again using the Fetch statement. If the response is a success, we update the list by filtering out the deleted Todo.

ToggleDone works by switching the todo.completed value and then sending this on to the update function. The update function simply takes the todo, executes an HTTP PUT, and then updating the local Todo with the response from the server. The reason for splitting the Toggle into two functions is so that if we add a future function to update the text of a todo, the update function can be shared.

Pulling it all together

Now that our code is written, we need to package the code, and copy it across to our Quarkus app, so that we can run it together. To do this, we need to run the following command from within the todo-frontend directory.

npm run build

This will compile and package the whole Svelte app. Now we just need to copy the index.html and build directory to src/main/resources/META-INF/resources and re-run the Quarkus app (mvn quarkus:dev) and our Todo App should be complete!!