DEV Community

Cover image for Build a To-Do app - Part 1 - Vue3 app
Alexandro Martinez
Alexandro Martinez

Posted on

Build a To-Do app - Part 1 - Vue3 app

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

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

Demo: vue3-todo-app.saasfrontends.com.

Requirements


Steps

  1. Run the client app
  2. Sidebar and Translations → Tasks sidebar icon
  3. Routing → /app/tasks
  4. The Task Model → DTO
  5. Task Services → API calls
  6. Tasks CRUD components → Tasks view, table and form

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 . 
Enter fullscreen mode Exit fullscreen mode

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

yarn yarn dev 
Enter fullscreen mode Exit fullscreen mode

Navigate to localhost:3000:

localhost

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

... <template> <div id="app"> - <TopBanner />  <metainfo> ... 
Enter fullscreen mode Exit fullscreen mode

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

- VITE_VUE_APP_SERVICE=api + VITE_VUE_APP_SERVICE=sandbox 
Enter fullscreen mode Exit fullscreen mode

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

... { 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", - ..., - }, 
Enter fullscreen mode Exit fullscreen mode

You should get the following sidebar:

Initial Sidebar

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

export enum SvgIcon {  ... EMPLOYEES, + TASKS, } 
Enter fullscreen mode Exit fullscreen mode

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

src/components/layouts/icons/IconTasks.vue

<template> <!-- You'll paste the svg icon here --> </template> 
Enter fullscreen mode Exit fullscreen mode

Go to icons8.com and find a decent tasks icon. I'm using this one.

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

<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'; ... 
Enter fullscreen mode Exit fullscreen mode

Now our sidebar item has a custom icon:

Sidebar Icon

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

+ { + "todo": { + "tasks": "Tasks" + } + } 
Enter fullscreen mode Exit fullscreen mode

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

+ { + "todo": { + "tasks": "Tareas" + } + } 
Enter fullscreen mode Exit fullscreen mode

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

src/locale/i18n.ts

... 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, + },  }, ... 
Enter fullscreen mode Exit fullscreen mode

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

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

<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> 
Enter fullscreen mode Exit fullscreen mode

Now we need to hook the view with the URL.

3.2. URL route → /app/tasks

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

src/router/appRoutes.ts

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], + }, + }, ]; 
Enter fullscreen mode Exit fullscreen mode

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

Tasks View

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

export enum TaskPriority { LOW, MEDIUM, HIGH } 
Enter fullscreen mode Exit fullscreen mode

4.2. TaskDto.ts

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

src/modules/todo/application/dtos/TaskDto.ts

import { AppWorkspaceEntityDto } from "@/application/dtos/core/AppWorkspaceEntityDto"; import { TaskPriority } from "../enums/TaskPriority"; export interface TaskDto extends AppWorkspaceEntityDto { name: string; priority: TaskPriority; } 
Enter fullscreen mode Exit fullscreen mode

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

import { TaskPriority } from "../enums/TaskPriority"; export interface CreateTaskRequest { name: string; priority: TaskPriority; } 
Enter fullscreen mode Exit fullscreen mode

UpdateTaskRequest.ts:

src/modules/todo/application/contracts/UpdateTaskRequest.ts

import { TaskPriority } from "../enums/TaskPriority"; export interface UpdateTaskRequest { name: string; priority: TaskPriority; } 
Enter fullscreen mode Exit fullscreen mode

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

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>; } 
Enter fullscreen mode Exit fullscreen mode

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

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); } } 
Enter fullscreen mode Exit fullscreen mode

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

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); }); } } 
Enter fullscreen mode Exit fullscreen mode

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

 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();  ... 
Enter fullscreen mode Exit fullscreen mode

5.5. GetAll

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

src/modules/todo/views/Tasks.vue

<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 + }) + }) 
Enter fullscreen mode Exit fullscreen mode

Task View Services

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

Restart the app and test CRUD operations.

7. All translations

Update your translations:

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

{ "todo": { "tasks": "Tasks", "noTasks": "There are no tasks", "models": { "task": { "object": "Task", "name": "Name", "priority": "Priority" } }, "priorities": { "LOW": "Low", "MEDIUM": "Medium", "HIGH": "High" } } } 
Enter fullscreen mode Exit fullscreen mode

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

{ "todo": { "tasks": "Tareas", "noTasks": "No hay tareas", "models": { "task": { "object": "Tarea", "name": "Nombre", "priority": "Prioridad" } }, "priorities": { "LOW": "Baja", "MEDIUM": "Media", "HIGH": "Alta" } } } 
Enter fullscreen mode Exit fullscreen mode

In part 2 we're going to implement the .NET backend.

Top comments (0)