DEV Community

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

Posted on

7 2

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.

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs