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.
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', }">↑</span> <span :class="{ 'text-blue-600': orderDirection === 'desc' && orderColumn === 'created_at', 'hidden': orderDirection !== '' && orderDirection !== 'desc' && orderColumn === 'created_at', }">↓</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>// ...
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.
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.
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.
Now, we need to save the updated post to the DB.
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.