Alexandro Martinez
Posted on March 5, 2022
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
- Run the client app
- Sidebar and Translations → Tasks sidebar icon
- Routing → /app/tasks
- The Task Model → DTO
- Task Services → API calls
- 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 .
Open the VS Code terminal, install dependencies and run the app:
yarn
yarn dev
Navigate to localhost:3000:
Let's remove the top banner. Open the App.vue file and remove the following line:
...
<template>
<div id="app">
- <TopBanner />
<metainfo>
...
We'll work on a Sandbox environment, design first, implement later.
- 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
...
{
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:
Two issues here:
- We need a Tasks icon
- 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,
}
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>
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';
...
Now our sidebar item has a custom 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"
+ }
+ }
src/modules/todo/locale/es-MX.json
+ {
+ "todo": {
+ "tasks": "Tareas"
+ }
+ }
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,
+ },
},
...
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.
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>
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],
+ },
+ },
];
You'll get an empty app view with a meta title.
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
}
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;
}
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;
}
UpdateTaskRequest.ts:
src/modules/todo/application/contracts/UpdateTaskRequest.ts
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:
- ITaskService.ts - Interface
- FakeTaskService.ts - Fake API implementation (for sanbdox environment)
- 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>;
}
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);
}
}
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);
});
}
}
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();
...
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
+ })
+ })
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
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"
}
}
}
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"
}
}
}
In part 2 we're going to implement the .NET backend.
Posted on March 5, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.