Vuetify 3 TypeScript Tutorial Series -Part 8

Habibi Coding | حبيبي كودنق
Nerd For Tech
Published in
6 min readFeb 15, 2024
tutorial banner

In Part 7 of this tutorial series, we covered the following steps:

  • Implemented task card details

If you missed Part 7, you can find it here: Part 7

Updating a task

In the previous parts we enabled from the CRUD operations the create and read possibilities. Update and delete are missing, so let us tackle now the update/editing part. In src -> componentscreate the file TaskUpdateForm.vue and add these lines:

<script setup lang="ts">
import {reactive, ref} from "vue";
import {useTaskStore} from "@/store/taskStore";
import {useField} from "vee-validate";
import {Priority, TaskUpdateRequest} from "@/dtos/taskDto";
import {MIN_TASK_DESCRIPTION} from "@/constants/appConstants";


const priority = ref([
Priority[Priority.LOW],
Priority[Priority.MEDIUM],
Priority[Priority.HIGH],
]);

const taskStore = useTaskStore();

const request = reactive<TaskUpdateRequest>({
description: taskStore.taskToEdit.description,
isReminderSet: taskStore.taskToEdit.isReminderSet,
isTaskOpen: taskStore.taskToEdit.isTaskOpen,
priority: taskStore.taskToEdit.priority,
});

const description = useField('description');

const emit = defineEmits(['updated-task', 'abort-clicked']);

const handleSubmit = () => {
if (request.description.length >= MIN_TASK_DESCRIPTION) {
emit('updated-task', taskStore.taskToEdit.id, request);
} else {
description.errorMessage.value = 'Description needs to be at least 3 characters.';
}
};

const handleReset = () => {
emit('abort-clicked');
};

</script>

Then add the UI structure:

<template>
<form @submit.prevent="handleSubmit">
<v-text-field
v-model="request.description"
:counter="10"
:error-messages="description.errorMessage.value"
label="Description"
/>

<v-checkbox v-model="request.isReminderSet" label="Reminder" type="checkbox"/>

<v-checkbox v-model="request.isTaskOpen" label="Open" type="checkbox"/>

<v-select v-model="request.priority" :items="priority" label="Priority"/>

<v-btn class="mb-4 clear-btn" @click="handleReset">abort</v-btn>

<v-btn class="mb-4 submit-btn" type="submit">update</v-btn>

</form>
</template>

Now, we need a new composable in order to abstract logic for editing a task, so create in package composables the file editTask.ts and add these lines:

import {Ref} from "vue";
import {AxiosError} from "axios";
import {taskService} from "@/services/taskApi";
import logRequestError from "@/composables/logRequestError";
import {TaskUpdateRequest} from "@/dtos/taskDto";

export async function editTask(
id: number,
request: TaskUpdateRequest,
isLoading: Ref<boolean>,
isNetworkError: Ref<boolean>,
axiosError: Ref<AxiosError | unknown>,
navigateToTasksView: () => void): Promise<void> {
isLoading.value = true;
isNetworkError.value = false;
await taskService.updateTask(id, request)
.then(() => {
isLoading.value = false;
navigateToTasksView();
})
.catch((err: AxiosError | unknown) => {
logRequestError('updateTask', err);
axiosError.value = err instanceof AxiosError ? err : undefined;
isNetworkError.value = true;
})
.finally(() => {
isLoading.value = false;
});
}

Function Declaration:

  • editTask is an async function, meaning it can perform asynchronous operations.
  • It takes several parameters:
  • id: The identifier of the task to be updated.
  • request: An object of type TaskUpdateRequest, likely containing the updated task data.
  • isLoading: A Ref object from Vue's Composition API, used to track whether the task is being processed.
  • isNetworkError: A Ref object to indicate if there's a network error during the task update process.
  • axiosError: A Ref to store any error that occurs during the HTTP request.
  • navigateToTasksView: A function that, when called, navigates the user to a different view, presumably the tasks view.

Function Logic:

  • The isLoading flag is set to true at the beginning, indicating the start of an asynchronous operation.
  • isNetworkError is initially set to false.
  • The taskService.updateTask method is called with id and request as arguments. This method likely makes an HTTP request to update the task on a server.
  • .then(): If the request is successful (i.e., the task is updated), isLoading is set to false, and navigateToTasksView is called to redirect the user.
  • .catch(): If an error occurs, the error handling logic is executed. If the error is an instance of AxiosError, it is stored in axiosError, and isNetworkError is set to true.
  • .finally(): Regardless of the outcome (success or failure), isLoading is set to false, signifying the end of the operation.

Purpose:

  • This function encapsulates the logic for updating a task. It handles the loading state, error state, successful update scenario, and navigation upon success.

Time to add page for updating a task, so create in pages the file TaskUpdatePage.vue and add these lines:

<script setup lang="ts">
import {useTaskNavigation} from "@/composables/useTaskNavigation";
import router from "@/router";
import {ref} from "vue";
import {AxiosError} from "axios";
import {editTask} from "@/composables/editTask";
import {TaskUpdateRequest} from "@/dtos/taskDto";
import Navbar from "@/components/Navbar.vue";
import MainBackground from "@/components/MainBackground.vue";
import TaskUpdateForm from "@/components/TaskUpdateForm.vue";
import LoadingSpinner from "@/components/LoadingSpinner.vue";
import ErrorDialog from "@/components/ErrorDialog.vue";


defineProps({
id: {
type: String,
required: true
}
});

const {handleTaskTypeSelected, logoClicked, navigateToTasksView} = useTaskNavigation();
const isLoading = ref(false);
const isNetworkError = ref(false);
const axiosError = ref<AxiosError>();

const clickedAbort = () => {
router.back();
};

const updateTask = (id: number, request: TaskUpdateRequest) => {
editTask(id, request, isLoading, isNetworkError, axiosError, navigateToTasksView);
};

</script>

Then add the UI structure:

<template>
<Navbar @taskTypeSelected="handleTaskTypeSelected" @logoClicked="logoClicked"/>
<MainBackground>
<TaskUpdateForm @updated-task="updateTask" @abort-clicked="clickedAbort"/>
<LoadingSpinner :is-loading="isLoading"/>
<ErrorDialog :model-value="isNetworkError" :axios-error="axiosError"/>
</MainBackground>
</template>

Lastly, to navigate to the update page we need to adapt in package router the file index.ts and add this object:

{
path: 'edit/:id',
name: TASK_UPDATE_VIEW,
component: TaskUpdatePage,
props: true,
}

Start again the project:

pnpm dev

Open http://localhost:3000/ and click on “Edit Task” on an item card:

Edit Task

Then you should see a form to edit your task:

edit form

I will now take away the checkmark from “Open” so it goes to the closed tasks and I will adapt the description and of course don’t forget to click on the “update” button.

Click on “Closed Tasks” now:

closed tasks

Notice when you click on “All Tasks” how the open and closed tasks have different border colors:

All Tasks

Implement logic to delete a task

We are almost done with our CRUD application, we need to know enable the delete part. As you might have guessed, we need to create a new file in components and create the file TaskDeleteDialog.vue and add these lines:

<script setup lang="ts">
import {ref, watch, defineProps} from 'vue';

const props = defineProps({
modelValue: Boolean,
taskDescription: String
});

const isDialogActive = ref(false);
const emits = defineEmits(['confirm-delete', 'update:modelValue']);

watch(() => props.modelValue, newVal => {
isDialogActive.value = newVal;
});

const confirmDelete = () => {
emits('confirm-delete');
isDialogActive.value = false;
emits('update:modelValue', false);
};

</script>

Define Props:

  • Using defineProps to define the component's props.
  • modelValue: A Boolean indicating whether the dialog is visible or not.
  • taskDescription: A String to hold the description of the task being considered for deletion.

Reactive State Declaration:

  • isDialogActive: A reactive reference (ref) that tracks whether the dialog is open or closed.

Emit Declaration:

  • emits: Defines the events that the component can emit to its parent. In this case, it can emit confirm-delete and update:modelValue events.

Watch Effect:

  • A watch effect that monitors changes to props.modelValue. When props.modelValue changes, isDialogActive is updated to reflect this new value. This synchronizes the component's internal state with the parent's state.

Confirm Delete Method:

  • confirmDelete: A method that emits the confirm-delete event when the user confirms the deletion action. It also sets isDialogActive to false (closing the dialog) and emits update:modelValue with false as the value to update the parent's state, indicating that the dialog should be closed.

Now, add the structure for the UI:

<template>
<v-row justify="center">
<v-dialog v-model="isDialogActive" persistent width="auto">
<v-card>
<v-card-title class="text-h5">
Are you sure you want to delete this task?
</v-card-title>
<v-card-text>
Task Description: {{ props.taskDescription }}
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="red-darken-1"
variant="text"
@click="isDialogActive = false;
emits('update:modelValue', false);">
Cancel
</v-btn>
<v-btn color="green-darken-1" variant="text" @click="confirmDelete">
Confirm
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-row>
</template>

With that, we conclude the first part of this tutorial series. If you found it useful and informative, give it a clap. Here is Part 9

Don’t forget to check out the video playlist on YouTube.

Here is the source code on GitHub, check out the branch: part-eight

--

--