Courses

Vue.js 3 + Laravel 11 API + Vite: SPA CRUD

Processing Form: Loading Indicator

Summary of this lesson:
- Adding loading indicators during form submission
- Implementing button disable states
- Managing loading states in composables
- Handling form submission timeouts

If the form takes longer to submit, then we need to show some kind of loading indicator and disable the submit button so the user wouldn't hit it twice.

loading indicator


It's pretty easy to achieve. In the posts Composable, we need a variable isLoading which will be true or false. When the button is clicked we just set it as true.

Also, we will reset the validation errors from the previous lesson. And if there are any errors, we need to set isLoading to false so that we could submit the form again.

resources/js/composables/posts.js:

import { ref } from 'vue'
import { useRouter } from 'vue-router'
 
export default function usePosts() {
const posts = ref({})
const router = useRouter()
const validationErrors = ref({})
const isLoading = ref(false)
// ...
const storePost = async (post) => {
if (isLoading.value) return;
 
isLoading.value = true
validationErrors.value = {}
 
axios.post('/api/posts', post)
.then(response => {
router.push({ name: 'posts.index' })
})
.catch(error => {
if (error.response?.data) {
validationErrors.value = error.response.data.errors
isLoading.value = false
}
})
}
 
return { posts, getPosts, storePost, validationErrors }
return { posts, getPosts, storePost, validationErrors, isLoading }
}

And in the PostsCreate Vue component for the button we need to do a couple of things:

  • Bind disabled to the isLoading variable.
  • Using the v-show directive show the loading spinner.
  • And last use v-if: if isLoading is true then show the text Processing..., otherwise using v-else show the Save text.

resources/js/components/Posts/Create.vue:

<template>
<form @submit.prevent="storePost(post)">
// ...
<!-- Buttons -->
<div class="mt-4">
<button class="rounded-md bg-indigo-600 px-3 py-2 text-sm uppercase text-white">Save</button>
<button :disabled="isLoading" class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm uppercase text-white disabled:opacity-75 disabled:cursor-not-allowed">
<span v-show="isLoading" class="inline-block animate-spin w-4 h-4 mr-2 border-t-2 border-t-white border-r-2 border-r-white border-b-2 border-b-white border-l-2 border-l-blue-600 rounded-full"></span>
<span v-if="isLoading">Processing...</span>
<span v-else>Save</span>
</button>
</div>
</form>
</template>
 
<script setup>
import { onMounted, reactive } from 'vue';
import useCategories from '@/composables/categories';
import usePosts from '@/composables/posts';
 
const post = reactive({
title: '',
content: '',
category_id: ''
})
 
const { categories, getCategories } = useCategories()
const { storePost, validationErrors } = usePosts()
const { storePost, validationErrors, isLoading } = usePosts()
 
onMounted(() => {
getCategories()
})
</script>

That's it. This is how easy it is to add a loading indicator.

loading indicator

avatar

Really useful. Now I am wondering in the composable way the best approach for dealing with same logic repeated all over the place. That is, clearly the 'isLoading' and 'validaitonErrors' will be needed on each CRUD. So, should a generic frontend Javascript interface should be coded or considering consumption of third party package?

avatar

I have a tip for this. Use FormKit it can show errormrssages from the backend and also validate before submitting.

avatar

Can anyone answer why there isnt the await keyword after async in storePost? Was it overlooked or is it good practice to add it?

avatar
You can use Markdown
avatar
You can use Markdown