Build a To-Do app - Part 1 - Vue3 app
Tags: vue3, dotnet, saas, tutorial
Published: 2022-03-05 21:05:37

---
title: Build a To-Do app - Part 1 - Vue3 app
published: true
description: In this tutorial, we'll use the SaasFrontends Vue3 codebase to build a basic To-Do app with Tasks, Routing, Model, and CRUD Components.
tags: vue3,dotnet,saas,tutorial
cover_image: https://yahooder.sirv.com/saasfrontends/tutorials/build-a-to-do-app/todo-1-vue3-frontend.png
---

In this tutorial, we'll use the SaasFrontends Vue3 codebase to build a basic To-Do app with Tasks, Routing, Model, and CRUD Components.

- [Part 1 - Frontend](https://dev.to/alexandromtzg/build-a-to-do-app-part-1-vue3-app-51m0) (Current)
- [Part 2 - Backend](https://dev.to/alexandromtzg/build-a-to-do-app-part-2-net-api-3j50)
- [Part 3 - Database per Tenant](https://dev.to/alexandromtzg/build-a-to-do-app-part-3-database-per-tenant-4l9p)

We'll create a simple **CRUD app** in a **modular** way.

Demo: [vue3-todo-app.saasfrontends.com](https://vue3-todo-app.saasfrontends.com).

## Requirements

- Any [Vue3 SaasFrontends edition](https://alexandromg.gumroad.com/l/SaasFrontends-Vue3).

---

## Steps

1. <a href="#1-run-the-client-app" className='font-normal border-dashed'>Run the client app</a>
2. <a href="#2-sidebar-item-and-translations" className='font-normal border-dashed'>Sidebar and Translations &rarr; Tasks sidebar icon</a>
3. <a href="#3-routing" className='font-normal border-dashed'>Routing &rarr; /app/tasks</a>
4. <a href="#4-the-task-model" className='font-normal border-dashed'>The Task Model  &rarr; DTO</a>
5. <a href="#5-task-services" className='font-normal border-dashed'>Task Services  &rarr; API calls</a>
6. <a href="#6-tasks-crud-components" className='font-normal border-dashed'>Tasks CRUD components  &rarr; Tasks view, table and form</a>

## 1. Run the client app

Open your terminal and navigate to the Client folder, and open it on VS Code:

```
cd src/NetcoreSaas.WebApi/ClientApp
code .
```

Open the VS Code terminal, install dependencies and run the app:

```
yarn
yarn dev
```

Navigate to [localhost:3000](http://localhost:3000):

![localhost](https://yahooder.sirv.com/saasfrontends/tutorials/build-a-to-do-app/todo-app-part-1/localhost.png)

Let's remove the top banner. Open the App.vue file and remove the following line:

```diff
...
<template>
  <div id="app">
-   <TopBanner />
    <metainfo>
    ...
```

We'll work on a Sandbox environment, design first, implement later.

```diff
- VITE_VUE_APP_SERVICE=api
+ VITE_VUE_APP_SERVICE=sandbox
```

Restart the app, and navigate to /app. It will redirect you to login, but since we are in a sandbox environment, you can type any email/password.

## 2. Sidebar Item and Translations

Our application is about tasks, so we'll remove everything related to **Links**, **Contracts** and **Employees**.

### 2.1. AppSidebar.ts

Open ***AppSidebar.ts*** file and remove the following sidebar items:

- /app/links/all
- /app/contracts/pending
- /app/employees

and add the following **/app/tasks** sidebar item:

src/application/AppSidebar.ts
```diff
...
      {
        title: i18n.global.t("app.sidebar.dashboard"),
        path: "/app/dashboard",
        icon: SvgIcon.DASHBOARD,
        userRoles: [TenantUserRole.OWNER, TenantUserRole.ADMIN, TenantUserRole.MEMBER, TenantUserRole.GUEST],
      },
+      {
+        title: i18n.global.t("todo.tasks"),
+        path: "/app/tasks",
+        icon: SvgIcon.TASKS,
+        userRoles: [TenantUserRole.OWNER, TenantUserRole.ADMIN, TenantUserRole.MEMBER, TenantUserRole.GUEST],
+      },
-      {
-        path: "/app/links/all",
-        ...,
-      },
-      {
-        path: "/app/contracts/pending",
-        ...,
-      },
-      {
-        path: "/app/employees",
-        ...,
-      },
```

You should get the following sidebar:

![Initial Sidebar](https://yahooder.sirv.com/saasfrontends/tutorials/build-a-to-do-app/todo-app-part-1/sidebar-initial.png)

Two issues here:

1. We need a Tasks icon
2. We need the `todo.tasks` translations

### 2.2. Sidebar icon

Open the ***SvgIcon.ts*** file and add a TASKS value.

src/application/enums/shared/SvgIcon.ts
```diff
export enum SvgIcon {
  ...
  EMPLOYEES,
+  TASKS,
}
```

Create the ***IconTasks.vue*** file in the existing folder ***src/components/layouts/icons***.

src/components/layouts/icons/IconTasks.vue
```html
<template>
  <!-- You'll paste the svg icon here -->
</template>
```

Go to [icons8.com](https://icons8.com) and find a decent tasks icon. I'm using [this one](https://icons8.com/icon/61195/todo-list).

Recolor the icon to white (#FFFFFF), click on **Embed HTML** and copy-paste the svg icon in your ***IconTasks.vue*** file.

If needed, replace all the `style=" fill:#FFFFFF;"` or `style=" fill:#000000;"` to `fill="currentColor"`.

Now add your new icon component to ***SidebarIcon.vue***:

src/components/layouts/icons/SidebarIcon.vue
```diff
<template>
  ...
   <IconEmployees :class="$attrs.class" v-else-if="icon === 14" />

+  <IconTasks :class="$attrs.class" v-else-if="icon === 15" />
</template>
<script setup lang="ts">
+  import IconTasks from './IconTasks.vue';
...
```

Now our sidebar item has a custom icon:

![Sidebar Icon](https://yahooder.sirv.com/saasfrontends/tutorials/build-a-to-do-app/todo-app-part-1/sidebar-icon.png)

But we still need to translate `todo.tasks`.

### 2.3. Translations

Since we want our app to be built in a modular way, we will create a ***src/modules*** folder and add the first module we're creating: **todo**.

Inside, create a ***locale*** folder, and add the following files: 

- src/modules/todo/locale/en-US.json
- src/modules/todo/locale/es-MX.json

Of course you can customize the languages and regions you will support.

src/modules/todo/locale/en-US.json
```diff
+ {
+   "todo": {
+     "tasks": "Tasks"
+   }
+ }
```

 src/modules/todo/locale/es-MX.json
```diff
+ {
+   "todo": {
+     "tasks": "Tareas"
+   }
+ }
```

Open the ***i18n.ts*** file and add our new **todo translations**:

src/locale/i18n.ts
```diff
...
import en from "./en-US.json";
import es from "./es-MX.json";
+ import enTodo from "../modules/todo/locale/en-US.json";
+ import esTodo from "../modules/todo/locale/es-MX.json";
...
  messages: {
-    en,
-    es,
+    en: {
+      ...en,
+      ...enTodo,
+    },
+    es: {
+      ...es,
+      ...esTodo,
+    },
  },
  ...
```

You should see the `todo.tasks` translations both in english and spanish. You can test it by changing the app language in /app/settings/profile.

![Sidebar Translations](https://yahooder.sirv.com/saasfrontends/tutorials/build-a-to-do-app/todo-app-part-1/sidebar-translations.png)

## 3. Routing

If you click on Tasks, you will get a blank page, let's fix that.

### 3.1. Tasks view

Create a view called ***Tasks.vue*** where we will handle the ***/app/tasks*** route. Create the ***views*** folder inside ***src/modules/todo***.

src/modules/todo/views/Tasks.vue
```html
<template>
 <div>Tasks</div>
</template>

<script setup lang="ts">
import i18n from '@/locale/i18n';
import { useMeta } from 'vue-meta';
useMeta({
  title: i18n.global.t("todo.tasks").toString()
})
</script>
```

Now we need to hook the view with the URL.

### 3.2. URL route &rarr; /app/tasks

Open the ***appRoutes.ts*** file, delete the Contracts and Employees routes, and set our ***Tasks.vue*** URL:

src/router/appRoutes.ts
```diff
import { TenantUserRole } from "@/application/enums/core/tenants/TenantUserRole";
- ...
+ import Tasks from "@/modules/todo/views/Tasks.vue";

export default [
-  ...
+  {
+    path: "tasks", // -> /app/tasks
+    component: Tasks,
+    meta: {
+      roles: [TenantUserRole.OWNER, TenantUserRole.ADMIN, TenantUserRole.MEMBER],
+    },
+  },
];
```

You'll get an empty app view with a meta title.

![Tasks View](https://yahooder.sirv.com/saasfrontends/tutorials/build-a-to-do-app/todo-app-part-1/tasks-view.png)

If you log out, and go to ***/app/tasks***, it will ask you to log in first, and then redirect you to this view.

## 4. The Task Model

Our model will contain only 2 custom properties:

- Name - Task description
- Priority - Low, Medium or High

### 4.1. TaskPriority.ts enum

We can see that we need a ***TaskPriority*** enum. Place it inside the ***src/modules/todo/application/enums*** folder.

src/modules/todo/application/enums/TaskPriority.ts
```js
export enum TaskPriority {
  LOW,
  MEDIUM,
  HIGH
}
```

### 4.2. TaskDto.ts

Now create the following ***TaskDto.ts*** interface inside ***src/modules/todo/application/dtos/***.

src/modules/todo/application/dtos/TaskDto.ts
```js
import { AppWorkspaceEntityDto } from "@/application/dtos/core/AppWorkspaceEntityDto";
import { TaskPriority } from "../enums/TaskPriority";

export interface TaskDto extends AppWorkspaceEntityDto {
  name: string;
  priority: TaskPriority;
}
```

We're extending **AppWorkspaceEntityDto**, so each task will be on a certain Workspace.

### 4.3. Create and Update Contracts

When creating or updating a Task, we don't want to send the whole ***TaskDto*** object, instead we do it by sending specific requests.

***CreateTaskRequest.ts***:

src/modules/todo/application/contracts/CreateTaskRequest.ts
```js
import { TaskPriority } from "../enums/TaskPriority";

export interface CreateTaskRequest {
  name: string;
  priority: TaskPriority;
}
```

***UpdateTaskRequest.ts***:

src/modules/todo/application/contracts/UpdateTaskRequest.ts
```js
import { TaskPriority } from "../enums/TaskPriority";

export interface UpdateTaskRequest {
  name: string;
  priority: TaskPriority;
}
```

This gives us flexibility in the long run.

## 5. Task Services

We'll create the following files:

1. ***ITaskService.ts*** - Interface
2. ***FakeTaskService.ts*** - Fake API implementation (for sanbdox environment)
3. ***TaskService.ts*** - Real API implementation (to call our .NET API)

### 5.1. ITaskService.ts

We need `GET`, `PUT`, `POST` and `DELETE` methods:

src/modules/todo/services/ITaskService.ts
```js
import { CreateTaskRequest } from "../application/contracts/CreateTaskRequest";
import { UpdateTaskRequest } from "../application/contracts/UpdateTaskRequest";
import { TaskDto } from "../application/dtos/TaskDto";

export interface ITaskService {
  getAll(): Promise<TaskDto[]>;
  get(id: string): Promise<TaskDto>;
  create(data: CreateTaskRequest): Promise<TaskDto>;
  update(id: string, data: UpdateTaskRequest): Promise<TaskDto>;
  delete(id: string): Promise<any>;
}
```

### 5.2. TaskService.ts

This service will be called when we set our environment variable `VITE_VUE_APP_SERVICE` to `api`.

Create a ***TaskService.ts*** class that extends the ***ApiService*** class and implements the ***ITaskService*** interface. 

src/modules/todo/services/TaskService.ts
```js
import { ApiService } from "@/services/api/ApiService";
import { CreateTaskRequest } from "../application/contracts/CreateTaskRequest";
import { UpdateTaskRequest } from "../application/contracts/UpdateTaskRequest";
import { TaskDto } from "../application/dtos/TaskDto";
import { ITaskService } from "./ITaskService";

export class TaskService extends ApiService implements ITaskService {
  constructor() {
    super("Task");
  }
  getAll(): Promise<TaskDto[]> {
    return super.getAll("GetAll");
  }
  get(id: string): Promise<TaskDto> {
    return super.get("Get", id);
  }
  create(data: CreateTaskRequest): Promise<TaskDto> {
    return super.post(data, "Create");
  }
  update(id: string, data: UpdateTaskRequest): Promise<TaskDto> {
    return super.put(id, data, "Update");
  }
  delete(id: string): Promise<any> {
    return super.delete(id);
  }
}
```

### 5.3. FakeTaskService.ts

This service will be called when we set our environment variable `VITE_VUE_APP_SERVICE` to `sandbox`.

Create a ***FakeTaskService.ts*** class that implements the ***ITaskService*** interface.

Here we want to return fake data, but also we want to simulate that we are calling a real API.

src/modules/todo/services/FakeTaskService.ts
```js
import { CreateTaskRequest } from "../application/contracts/CreateTaskRequest";
import { UpdateTaskRequest } from "../application/contracts/UpdateTaskRequest";
import { TaskDto } from "../application/dtos/TaskDto";
import { ITaskService } from "./ITaskService";

const tasks: TaskDto[] = [];
for (let index = 0; index < 3; index++) {
  const task: TaskDto = {
    id: (index + 1).toString(),
    createdAt: new Date(),
    name: `Task ${index + 1}`,
    priority: index,
  };
  tasks.push(task);
}

export class FakeTaskService implements ITaskService {
  tasks = tasks;
  getAll(): Promise<TaskDto[]> {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(this.tasks);
      }, 500);
    });
  }
  get(id: string): Promise<TaskDto> {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        const task = this.tasks.find((f) => f.id === id);
        if (task) {
          resolve(task);
        }
        reject();
      }, 500);
    });
  }
  create(data: CreateTaskRequest): Promise<TaskDto> {
    return new Promise((resolve) => {
      setTimeout(() => {
        const id = this.tasks.length === 0 ? "1" : (this.tasks.length + 1).toString();
        const item: TaskDto = {
          id,
          name: data.name,
          priority: data.priority,
        };
        this.tasks.push(item);
        resolve(item);
      }, 500);
    });
  }
  update(id: string, data: UpdateTaskRequest): Promise<TaskDto> {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        let task = this.tasks.find((f) => f.id === id);
        if (task) {
          task = {
            ...task,
            name: data.name,
            priority: data.priority,
          };
          resolve(task);
        }
        reject();
      }, 500);
    });
  }
  delete(id: string): Promise<any> {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        const task = this.tasks.find((f) => f.id === id);
        if (!task) {
          reject();
        } else {
          this.tasks = this.tasks.filter((f) => f.id !== id);
          resolve(true);
        }
      }, 500);
    });
  }
}
```

### 5.4. Initializing the Task services

Add our interface as a property and initialize the implementations depending on the environment variable:

src/services/index.ts
```diff
 class Services {
   ...
   employees: IEmployeeService;

+  tasks: ITaskService;
   constructor() {
     if (import.meta.env.VITE_VUE_APP_SERVICE === "sandbox") {
+      this.tasks = new FakeTaskService();
       ...
     } else {
+      this.tasks = new TaskService();
       ...
```

### 5.5. GetAll

Open the ***Tasks.vue*** view and call the `getAll` method when the component mounts:

src/modules/todo/views/Tasks.vue
```diff
<template>
-   <div>Tasks</div>
+   <div>
+     <pre>{{ tasks.map(f => f.name) }}</pre>
+   </div>
</template>

<script setup lang="ts">
...
+ import services from '@/services';
+ import { onMounted, ref } from 'vue';
+ import { TaskDto } from '../application/dtos/TaskDto';
...
+ const tasks = ref<TaskDto[]>([]);
+ onMounted(() => {
+   services.tasks.getAll().then((response) => {
+     tasks.value = response
+   })
+ })
```

![Task View Services](https://yahooder.sirv.com/saasfrontends/tutorials/build-a-to-do-app/todo-app-part-1/tasks-view-services.png)

## 6. Tasks CRUD components

I redesigned the ***Tasks.vue*** view and created the following components:

- src/modules/todo/components/***TasksTable.vue*** - List all tasks
- src/modules/todo/components/***TaskForm.vue*** - Create, Edit, Delete
- src/modules/todo/components/***PrioritySelector.vue*** - Select task priority
- src/modules/todo/components/***PriorityBadge.vue*** - Color indicator

[You can download them here](https://saasfrontends.com/docs/build-a-todo-app-part-1-vue3-frontend#6-tasks-crud-components)

Restart the app and test CRUD operations.

## 7. All translations

Update your translations:

src/modules/todo/locale/en-US.json
```json
{
  "todo": {
    "tasks": "Tasks",
    "noTasks": "There are no tasks",
    "models": {
      "task": {
        "object": "Task",
        "name": "Name",
        "priority": "Priority"
      }
    },
    "priorities": {
      "LOW": "Low",
      "MEDIUM": "Medium",
      "HIGH": "High"
    }
  }
}
```

src/modules/todo/locale/es-MX.json
```json
{
  "todo": {
    "tasks": "Tareas",
    "noTasks": "No hay tareas",
    "models": {
      "task": {
        "object": "Tarea",
        "name": "Nombre",
        "priority": "Prioridad"
      }
    },
    "priorities": {
      "LOW": "Baja",
      "MEDIUM": "Media",
      "HIGH": "Alta"
    }
  }
}
```

---

[In part 2](https://dev.to/alexandromtzg/build-a-to-do-app-part-2-net-api-3j50) we're going to implement the .NET backend.