Courses

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

Post Edit Form: Route with Parameter

Summary of this lesson:
- Creating edit form route with parameters
- Implementing post data retrieval
- Reusing form components for editing
- Handling route parameters in Vue Router

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.

Previous: File Upload Example
avatar

"There's one image that is not available, only the text 'empty edit form' is written. Is it important or not?"

avatar

It wasn't important screenshot. Fixed the link.

avatar
You can use Markdown
avatar

In the edit form, the modified content field from the PostResource is shown. Is there a way to display the full content in the edit form but still keep the truncated content in the post index view? Could you create a PostEditResource and return the full values of the Post? It seems clicking save updates the content field to be exactly that truncated value.

avatar

You can just add another value to the

avatar

@Nerijus your reply seems truncated as well... I'd like to know how to update the full value of content shown in the Edit.vue

avatar

Tell more what you want

avatar

As Remy Jay mentioned, how to keep full value of content without limiting the words.

avatar

Add whatever values you want to a api resource and name it whatever you

avatar
You can use Markdown
avatar

Why I can't update post and error (Edit.vue): updatePost is not an function?

avatar

Why I can't update post and error (Edit.vue): updatePost is not an function?

Because you have to add the function in Edit.vue: const { post, getPost, updatePost, validationErrors, isLoading } = usePosts()

avatar
You can use Markdown
avatar

how to update a file using put? axios.put('/api/posts', post) - PostController $request = It works, but file is empty

let serializedPost = new FormData() for (let item in post) { if (post.hasOwnProperty(item)) {serializedPost.append(item, post[item])}} ... axios.put('/api/posts', serializedPost) - PostController $request->all() = empty array

avatar
You can use Markdown
avatar

I was getting, "Uncaught TypeError: ctx.updatePost is not a function at Edit.vue:2:28" in Dev Console. In resources/js/components/Posts/Edit.vue, shouldn't

const { post, getPost, validationErrors, isLoading } = usePosts() be instead const { post, getPost, updatePost, validationErrors, isLoading } = usePosts() ? I added updatePost and it now works fine.

avatar
You can use Markdown
avatar
You can use Markdown