Back to Course |
Vue.js 3 + Laravel 11 + Vite: SPA CRUD

Post Edit Form: Route with Parameter

Now let's work on editing the post. This is going to be kind of a repeating thing, similar to what we did with the create form but with minor changes.

edit form


Edit Page Route

So first, let's add a link to the Edit page in the posts list.

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

<template>
<div class="overflow-hidden overflow-x-auto p-6 bg-white border-gray-200">
<div class="min-w-full align-middle">
// ...
<table class="min-w-full divide-y divide-gray-200 border">
<thead>
<tr>
// ...
<th class="px-6 py-3 bg-gray-50 text-left">
<div class="flex flex-row items-center justify-between cursor-pointer" @click="updateOrdering('created_at')">
<div class="leading-4 font-medium text-gray-500 uppercase tracking-wider" :class="{ 'font-bold text-blue-600': orderColumn === 'created_at' }">
Created at
</div>
<div class="select-none">
<span :class="{
'text-blue-600': orderDirection === 'asc' && orderColumn === 'created_at',
'hidden': orderDirection !== '' && orderDirection !== 'asc' && orderColumn === 'created_at',
}">&uarr;</span>
<span :class="{
'text-blue-600': orderDirection === 'desc' && orderColumn === 'created_at',
'hidden': orderDirection !== '' && orderDirection !== 'desc' && orderColumn === 'created_at',
}">&darr;</span>
</div>
</div>
</th>
<th class="px-6 py-3 bg-gray-50 text-left">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Actions</span>
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200 divide-solid">
<tr v-for="post in posts.data">
// ...
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{{ post.created_at }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
<router-link :to="{ name: 'posts.edit', params: { id: post.id } }">Edit</router-link>
</td>
</tr>
</tbody>
</table>
 
<TailwindPagination :data="posts" @pagination-change-page="page => getPosts(page, selectedCategory)" class="mt-4" />
</div>
</div>
</template>
 
<script setup>
// ...

edit action link

Now, for the edit link, we added a parameter id. Next, we need to add a route with this parameter. The syntax for adding a parameter is to add a colon before the parameter.

resources/js/routes/index.js:

import { createRouter, createWebHistory } from 'vue-router';
 
import PostsIndex from '@/components/Posts/Index.vue'
import PostsCreate from '@/components/Posts/Create.vue'
import PostsEdit from '@/components/Posts/Edit.vue'
 
const routes = [
{
path: '/',
name: 'posts.index',
component: PostsIndex,
meta: { title: 'Posts' }
},
{
path: '/posts/create',
name: 'posts.create',
component: PostsCreate,
meta: { title: 'Add new post' }
},
{
path: '/posts/edit/:id',
name: 'posts.edit',
component: PostsEdit,
meta: { title: 'Edit post' }
},
]
 
export default createRouter({
history: createWebHistory(),
routes
})

Let's create a PostsEdit Vue component, which for now will be empty.

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

<template>
Edit form
</template>

After visiting the edit page for any post you should see that the URL is correct and should see a dummy text.

empty edit form


Edit Form: Vue Component and Composable

Next, the form in the edit page will be identical to the create page, except instead of the storePost() method for the form action we will use updatePost(). Just copy the template from the create page and change the action.

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

<template>
<form @submit.prevent="storePost(post)">
<form @submit.prevent="updatePost(post)">
<!-- Title -->
<div>
<label for="post-title" class="block font-medium text-sm text-gray-700">
Title
</label>
<input v-model="post.title" id="post-title" type="text" class="block mt-1 w-full rounded-md shadow-sm border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
<div class="text-red-600 mt-1">
<div v-for="message in validationErrors?.title">
{{ message }}
</div>
</div>
</div>
 
<!-- Content -->
// ...
</form>
</template>

Now, we need to add a new method to get a single post in the posts composable which will accept ID as a parameter.

resources/js/composables/posts.js:

import { ref } from 'vue'
import { useRouter } from 'vue-router'
 
export default function usePosts() {
const posts = ref({})
const post = ref({})
const router = useRouter()
const validationErrors = ref({})
const isLoading = ref(false)
// ...
const getPost = async (id) => {
axios.get('/api/posts/' + id)
.then(response => {
post.value = response.data.data;
})
}
// ...
return { posts, getPosts, storePost, validationErrors, isLoading }
return { posts, post, getPosts, getPost, storePost, validationErrors, isLoading }
}

On the back-end in Laravel, we need to create a new show method in the PostController which needs to return a new instance of Post resource.

app/Http/Controllers/Api/PostController.php:

class PostController extends Controller
{
public function show(Post $post)
{
return new PostResource($post);
}
}

Now, in the PostsEdit Vue component, we need to get the post from the Composable. And also we need to use vue-router Composable useRoute to get the ID from the URL.

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

<template>
// ...
</template>
 
<script setup>
import { onMounted } from "vue";
import { useRoute } from "vue-router";
import useCategories from "@/composables/categories";
import usePosts from "@/composables/posts";
 
const { categories, getCategories } = useCategories()
const { post, getPost, validationErrors, isLoading } = usePosts()
const route = useRoute()
 
onMounted(() => {
getPost(route.params.id)
getCategories()
})
</script>

In the edit page, now you should see the post but without the category selected.

edit form without category

In the category select we have v-model="post.category_id". The problem is that we don't return that from the API. So we just need to add category_id in the PostResource.

app/Http/Resources/PostResource.php:

class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'content' => substr($this->content, 0, 50) . '...',
'category_id' => $this->category_id,
'category' => $this->category->name,
'created_at' => $this->created_at->toDateString()
];
}
}

Now we also have a category selected in the edit form.

edit form


Submitting The Form

Now, we need to save the updated post to the DB.

updated post

Let's start by adding a new update method to the PostController.

app/Http/Controllers/Api/PostController.php:

class PostController extends Controller
{
// ...
public function update(Post $post, StorePostRequest $request)
{
$post->update($request->validated());
 
return new PostResource($post);
}
}

In the Controller, we just get a post using Route Model Binding and update the post with the validated data. Then, just return the new post resource.

Now in the posts Composable, we need to add the updatePost() method which will accept the post as a parameter. Inside this method, we need to make an Axios HTTP PUT request.

resources/js/composables/posts.js:

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

If you try to edit any post, after a successful save you should be redirected to the posts index page.