DEV Community

loading...
Cover image for How to Build a To-do app with Svelte, Strapi & Tailwind CSS
Strapi

How to Build a To-do app with Svelte, Strapi & Tailwind CSS

Shada
Originally published at strapi.io ・11 min read

Introduction

In this post, we will learn how to use three modern and powerful technologies Strapi, Svelte, and Tailwind CSS, to build an elegant and functional todo app. We will use Strapi for our backend, Svelte as our JavaScript framework for building our interface, and Tailwind CSS for styling our App.

Below is a demo of what we will be building with these three technologies.

To-do app with Svelte, Strapi and Tailwind CSS

Before we get started, let's see what the technologies we'll be working with is about:

What is Strapi?

Strapi is an open-source headless Node.js CMS built purely with JavaScript that helps to create fully customizable and scalable APIs. With Strapi, we can build APIs using REST or GraphQL architecture.

What is Svelte?

Svelte is another core technology we will be using in the tutorial. Svelte is the talk of the town in the JavaScript ecosystem because it follows an entirely different approach in building user interfaces as opposed to React and Vue, as it doesn't use the Virtual DOM.

What is Tailwind CSS?

Tailwind CSS is a utility-first CSS framework use for writing CSS in a composable fashion right in your HTML. It is a powerful utility that allows you to build modern, pixel-perfect, and fully responsive interfaces without writing your custom CSS. Unlike Bootstrap, Tailwind CSS is not opinionated, and it doesn't have prebuilt components, making building good looking-interfaces fun!

Goals

In the tutorial, we will build REST API using Strapi and consume our data from the client-side using Axios and make our components with Svelte.

We will write our JavaScript functions perform CRUD operation on our todos data coming from our backend powered by Strapi, and style our App, so it looks elegant with Tailwind CSS.

Requirement

Download and install the following technologies:

  1. Node.js installed on your computer; only LTS versions are supported (version 12.x minimum).
  2. npm: Npm ships with Node.js
  3. Yarn: A fast, secure, and reliable Node package manager. To install yarn globally, run the following command on your terminal.
    npm i yarn -g
Enter fullscreen mode Exit fullscreen mode

No Knowledge of Svelte, Strapi, or Tailwind CSS is required. However, it will help if you have some familiarity with CSS and JavaScript.

Creating a Strapi Project

Now, let's bootstrap our Strapi project. To do this, navigate to the folder where you want your project to be and run one of the following commands in your terminal:

    npx create-strapi-app strapi-todo-api --quickstart
    #or
    yarn create strapi-app strapi-todo-api --quickstart
Enter fullscreen mode Exit fullscreen mode

This command will create a Strapi project called "strapi-todo-api" with all the necessary dependencies installed.

Generating a Strapi Project

Once that is done, cd into the project directory and run one of the following commands:

    npm run develop
    #or 
    yarn develop
Enter fullscreen mode Exit fullscreen mode

This command will start our dev server and open our application on our default browser, and you should see this on the page:

Fill in your details accordingly and click on the let's start button and you'll be redirected to the onboarding page:

Next, let’s create our content to populate our todo items

Creating Content Types

Now, let's build the collection for our data and fill in some todo items to display when we fetch our data in our Svelte App. To achieve this, click on Collection Type Builder on the left side of the admin panel, and after that, click on create new collection type and fill in the following data in the "Display name" field as shown below. When that is done, click on the click on continue.

This image will create a new Todo collection so let's go ahead and add documents to our newly created collections.

We will add two fields in each of the documents in our Todo collection, one for our todo items and the other a boolean to check the state of our todo; whether it is marked as done.
Click on the "Add another field" button and add a field with type text and check the long text type as shown below:

Click on add another field and select the boolean type. We will call our field isCompleted. We will be using it to toggle our todo state when a user checks on the completed checkbox in our App.

Setting up Roles and Permissions

The next thing we have to do is to set up our roles and permission so that we can get access to our data from our Svelte App. To do this, navigate to SettingUsers & Permissions Plugin → and then tick on the select all checkbox under the Permissions section and click on the Save button:

Frame 83.png

Testing our API

With that done, let's test our API with Postman. To access our resource, open [http://localhost:1337/todos](http://localhost:1337/todos) in your browser or using an API client like Postman to test our created data.

Bootstrap a Svelte Project

Now that we are creating our API using Strapi let's move over to the client-side of our application, where we build out our components using Svelte. To generate a new Svelte app, run the following command in your terminal to generate a project in a folder called "SvelteTodoApp":

    npx degit sveltejs/template SvelteTodoApp
    cd SvelteTodoApp
    npm install
    npm run dev
Enter fullscreen mode Exit fullscreen mode

When this is done, navigate to [http://localhost:54890](http://localhost:54890) from your browser, and you should see this:

You have to install the Svelte extension to get syntax highlighting and IntelliSense support for VS Code

Create a file in src/Todo.svelte and replace everything in the main tag in App.svelte with the following:

      <Todo />
Enter fullscreen mode Exit fullscreen mode

Adding Tailwind CSS to our Svelte App

Next, let's add Tailwind CSS to our Svelte App. Run one of the following commands in your terminal to install the necessary dependencies:

    npm install tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
    # or
    yarn add tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
    # and
    npx tailwindcss init  tailwind.config.js
Enter fullscreen mode Exit fullscreen mode

This will create a tailwind.config.js and rollup.config.js file in the root of our app. Replace the code in your tailwind.config.js file with the following:

    const production = !process.env.ROLLUP_WATCH;
    module.exports = {
      purge: {
        content: ['./src/**/*.svelte'],
        enabled: production, 
      },
      darkMode: false, 
      theme: {
        extend: {},
      },
      variants: {
        extend: {},
      },
      plugins: [],
      future: {
        purgeLayersByDefault: true,
        removeDeprecatedGapUtilities: true,
      },
    };
Enter fullscreen mode Exit fullscreen mode

Next, open your rollup.config.js and replace what you have with the following:

    import svelte from 'rollup-plugin-svelte';
    import commonjs from '@rollup/plugin-commonjs';
    import resolve from '@rollup/plugin-node-resolve';
    import livereload from 'rollup-plugin-livereload';
    import { terser } from 'rollup-plugin-terser';
    import css from 'rollup-plugin-css-only';
    import sveltePreprocess from 'svelte-preprocess';
    const production = !process.env.ROLLUP_WATCH;
    function serve() {
      let server;
      function toExit() {
        if (server) server.kill(0);
      }
      return {
        writeBundle() {
          if (server) return;
          server = require('child_process').spawn(
            'npm',
            ['run', 'start', '--', '--dev'],
            {
              stdio: ['ignore', 'inherit', 'inherit'],
              shell: true,
            }
          );
          process.on('SIGTERM', toExit);
          process.on('exit', toExit);
        },
      };
    }
    export default {
      input: 'src/main.js',
      output: {
        sourcemap: true,
        format: 'iife',
        name: 'app',
        file: 'public/build/bundle.js',
      },
      plugins: [
        svelte({
          preprocess: sveltePreprocess({
            sourceMap: !production,
            postcss: {
              plugins: [require('tailwindcss'), require('autoprefixer')],
            },
          }),
          compilerOptions: {
            // enable run-time checks when not in production
            dev: !production,
          },
        }),
        // we'll extract any component CSS out into
        // a separate file - better for performance
        css({ output: 'bundle.css' }),
        // If you have external dependencies installed from
        // npm, you'll most likely need these plugins. In
        // some cases you'll need additional configuration -
        // consult the documentation for details:
        // https://github.com/rollup/plugins/tree/master/packages/commonjs
        resolve({
          browser: true,
          dedupe: ['svelte'],
        }),
        commonjs(),
        // In dev mode, call `npm run start` once
        // the bundle has been generated
        !production && serve(),
        // Watch the `public` directory and refresh the
        // browser on changes when not in production
        !production && livereload('public'),
        // If we're building for production (npm run build
        // instead of npm run dev), minify
        production && terser(),
      ],
      watch: {
        clearScreen: false,
      },
    };
Enter fullscreen mode Exit fullscreen mode

Finally, add the following to your App.svelte:

    <style global lang="postcss">
      @tailwind base;
      @tailwind components;
      @tailwind utilities;
    </style>
Enter fullscreen mode Exit fullscreen mode

Fetch todo items

We will use Axios to make our HTTP request to get our data from the backend. Run the following command in your Svelte project directory to install Axios:

    npm install axios
    #or
    yarn add axios
Enter fullscreen mode Exit fullscreen mode

When that is done, add the following to the top of your script section in your Svelte App:

    import { onMount } from 'svelte';
    import axios from 'axios';

    let isError = null;
    let todos = [];
Enter fullscreen mode Exit fullscreen mode

The [onMount](https://svelte.dev/docs#onMount) is a lifecycle function in Svelte that schedules a callback to run immediately after the component is mounted to the DOM. The todos array is where all the data we get from our API will be.

Let’s create a function called getTodo and call it on the onMount lifecycle function:

    const getTodos = async () => {
        try {
          const res = await axios.get('http://localhost:1337/todos');
          todos = res.data;
        } catch (e) {
          isError = e;
        }
      };

    onMount(() => {
        getTodos();
    });
Enter fullscreen mode Exit fullscreen mode

The getTodos function will make an asynchronous call to the endpoint we created earlier and set the result of our todos array to be the data from our API.

To render the output of our todos in the DOM, add the following block of code in your template:

    // Todo.svelte
    {#if todos.length > 0}
        <p class="text-2xl mb-4">Today's Goal</p>
      {/if}
      {#if isError}
        <p class="text-xl mb-2 text-red-600">{isError}</p>
      {/if}
      <ul>
        {#each todos as todo}
          <li
            class="rounded-xl bg-black bg-opacity-10  p-5 mb-4 flex items-center justify-between cursor-pointer hover:shadow-lg transition transform hover:scale-110"
          >
            <div class="flex items-center w-full">
              <input
                type="checkbox"
                class="mr-3"
                bind:checked={todo.isCompleted}
                on:click={toggleComplete(todo)}
              />
              <input
                class:completed={todo.isCompleted}
                class="border-0 bg-transparent w-full"
                bind:value={todo.todoItem}
                on:change={updateTodo(todo)}
                on:input={updateTodo(todo)}
              />
            </div>
            <button on:click={deleteTodo(todo)} class="border-0"
              ><img src={deletIcon} alt="delete todo" class="w-6 " /></button
            >
          </li>
        {:else}
          <p>No goals for today!</p>
        {/each}
      </ul>
Enter fullscreen mode Exit fullscreen mode

Notice how we are making use of the {#if expression} to check if we have any items in our todos array and then conditionally a text, We are also doing the same thing with the isError checking if there’s an error from our API.

The {#each ...} expression is where the magic happens, we are looping through our array of todos and rendering the todoItem and then we use the {:[else](https://svelte.dev/docs#each)}...{/each}to conditionally render a text when there’s no result.

Notice we haven’t created the updateTodo and deleteTodo function. We will do that later

Create todo items

Next, we want to create a function that will allow users to add a todo to your API from our Svelte App. To do this, add the following block of code in the script section of your App:

      let todoItem = '';

      const addTodo = async () => {
        try {
          if (!todoItem) return alert('please add a goal for today!');
          const res = await axios.post('http://localhost:1337/todos', {
            todoItem,
            isCompleted: false,
          });
          // Using a more idiomatic solution
          todos = [...todos, res?.data];
          todoItem = '';
        } catch (e) {
          isError = e;
        }
Enter fullscreen mode Exit fullscreen mode

The todoItem variable is what we will use to get the user's input, and then in our addTodo function, we are making sure it's not empty before making a POST request to our todos endpoint.

Next, we send the user input stored in todoItem and setting isCompleted: false because we want the todo to be undone when created. Finally, we are updating our todos array with the data coming in from the API call.

Add the following markup to after the else statement:

    <input
        type="text"
        bind:value={todoItem}
        class="w-full rounded-xl bg-white border-0 outline-none bg-opacity-10 p-4 shadow-lg mt-4"
        placeholder="Add new goals"
      />

    <button
        on:click={addTodo}
        class="my-5  p-5 bg-black text-white rounded-xl w-full hover:bg-opacity-60 transition border-0 capitalize flex items-center justify-center"
        ><span><img src={addIcon} alt="add todo" class="w-6 mr-4" /></span>Add new
        todo</button>
Enter fullscreen mode Exit fullscreen mode

Notice the bind:value={todoItem} in our input field above. This binding is used to achieve two-way data binding in Svelte.

Update todo items

Updating our App will happen in two forms. Users can mark todo as done by clicking on the checkbox beside each todo item and editing the todo text.

Let’s create a function for toggling the checkbox:

     const toggleComplete = async (todo) => {
        const todoIndex = todos.indexOf(todo);
        try {
          const { data } = await axios.put(
            `http://localhost:1337/todos/${todo.id}`,
            {
              isCompleted: !todo.isCompleted,
            }
          );
          todos[todoIndex].isCompleted = data.isCompleted;
        } catch (e) {
          isError = e;
        }
      };
Enter fullscreen mode Exit fullscreen mode

We are getting the index of the todo item that is clicked using the indexOf method and then making a PUT request to the server to update the particular todo item.

We are toggling the isCompleted field in our API by sending isCompleted: !todo.isCompleted in our request. When our API is resolved, we update our todos array in our state with the payload from our API by setting todos[todoIndex].isCompleted = data.isCompleted;

Next, let’s create a function to edit the todo text:

     const updateTodo = async (todo) => {
        const todoIndex = todos.indexOf(todo);
        try {
          const { data } = await axios.put(
            `http://localhost:1337/todos/${todo.id}`,
            {
              todoItem: todo.todoItem,
            }
          );
          todos[todoIndex].todoItem = data.todoItem;
        } catch (e) {
          isError = e;
        }
      };
Enter fullscreen mode Exit fullscreen mode

Our updateTodo function does almost the same thing as the toggleComplete except that it updates the todo text.

After that is done, add the following to your template:

    <div class="flex items-center w-full">
              <input
                type="checkbox"
                class="mr-3"
                bind:checked={todo.isCompleted}
                on:click={toggleComplete(todo)}
              />
              <input
                class:completed={todo.isCompleted}
                class="border-0 bg-transparent w-full"
                bind:value={todo.todoItem}
                on:change={updateTodo(todo)}
                on:input={updateTodo(todo)}
              />
            </div>
Enter fullscreen mode Exit fullscreen mode

To sync the data from our state to our input field, we are using the [bind:value={}](https://svelte.dev/docs#bind_element_property) syntax provided to us by Svelte.

Observe how we are binding a class attribute in our input field using this syntax: class:completed={todo.isCompleted}. We are telling Svelte that it should add the completed class whenever todo.isCompleted is truthy.

This will apply the following class:

    <style>
      .completed {
        text-decoration: line-through;
      }
    </style>
Enter fullscreen mode Exit fullscreen mode

Delete todo items

Next, let’s create a function to delete items from our API and todos array:

     const deleteTodo = async (todo) => {
        try {
          await axios.delete(`http://localhost:1337/todos/${todo.id}`);
          todos = todos.filter((to) => to.id !== todo.id);
        } catch (e) {
          isError = e;
    }
Enter fullscreen mode Exit fullscreen mode

Notice we call the delete method on Axios and then appending the id value of the todo item the user clicks to our URL. This call will effectively remove the item clicked from our todos collection in our API, and then we are filtering our todos array and returning all the todos except for the one deleted.

This is how we use the function in our template:

    <button on:click={deleteTodo(todo)} class="border-0"><img src={deletIcon} alt="delete todo" class="w-6 " /></button>
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this article, we've seen how powerful and very easy to use Strapi is. Setting up a backend project is like a walk in the park, very simple and easy. By just creating our collections, Strapi will provide us with endpoints we need following best web practices.

We've also seen our to work with Svelte, and we built our component using one and styled our App with Tailwind CSS.

You can find the complete code used in this tutorial for the Svelte App here, and the backend code is available on GitHub. You can also find me on Twitter, LinkedIn, and GitHub. Thank you for reading! Feel free to drop a comment to let me know what you thought of this article.

Discussion (0)