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

Post Create Form & Category Dropdown

Now that we can go through pages, we can move on to building a full CRUD for posts. In this lesson, we will build a create form with the select input for choosing a category.

select categories


HTML Form in Vue

So, first, let's add the HTML code for the form.

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

<template>
<form>
<!-- Title -->
<div>
<label for="post-title" class="block text-sm font-medium text-gray-700">
Title
</label>
<input id="post-title" type="text" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
</div>
 
<!-- Content -->
<div class="mt-4">
<label for="post-content" class="block text-sm font-medium text-gray-700">
Content
</label>
<textarea id="post-content" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"></textarea>
</div>
 
<!-- Category -->
<div class="mt-4">
<label for="post-category" class="block text-sm font-medium text-gray-700">
Category
</label>
<select id="post-category" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
<option value="" selected>-- Choose category --</option>
</select>
</div>
 
<!-- Buttons -->
<div class="mt-4">
<button class="rounded-md bg-indigo-600 px-3 py-2 text-sm uppercase text-white">Save</button>
</div>
</form>
</template>

If you visit the "Create post" page now, you would see the form.

create post form


Dropdown List of Categories

Now, let's add a list of categories to the category select. This is where the Composables shine. We have already written all the needed code and can easily reuse it!

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

<script setup>
import { onMounted } from 'vue';
import useCategories from '@/composables/categories';
 
const { categories, getCategories } = useCategories()
 
onMounted(() => {
getCategories()
})
</script>

This part of the code is identical to how we got the categories in the PostsIndex Vue component. Now, below the Choose category option we need to show all the categories using the v-for directive.

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

<template>
// ...
<label for="post-category" class="block text-sm font-medium text-gray-700">
Category
</label>
<select id="post-category" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
<option value="" selected>-- Choose category --</option>
<option v-for="category in categories" :value="category.id">
{{ category.name }}
</option>
</select>
// ...
</template>

Now we have all the categories as a select option.

select categories


A "Fake" Submit Method

For the post submission, for now, let's add a method submit which will only console log after submitting.

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

<template>
<form>
<form @submit.prevent="submit">
// ...
</form>
</template>
 
<script setup>
import { onMounted } from 'vue';
import useCategories from '@/composables/categories';
 
const { categories, getCategories } = useCategories()
 
const submit = () => { <!-- [tl! ++] -->
console.log('submitted') <!-- [tl! ++] -->
} <!-- [tl! ++] -->
 
onMounted(() => {
getCategories()
})
</script>

After clicking the Save button you will see in the console message submitted.

console log message


Submit on the Back-end API

Now, for submit to actually work, we will take care of the back-end. For the validation, we will use Form Request.

php artisan make:request StorePostRequest

app/Http/Requests/StorePostRequest.php:

class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
 
public function rules(): array
{
return [
'title' => ['required'],
'content' => ['required'],
'category_id' => ['required', 'exists:categories,id'],
];
}
}

And then, inside PostController we need to create a new method store with the validation from the Form Request.

And inside store we create a post and return the new object of the post.

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

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

Next, in the API routes instead of the get posts route we need to change it to apiResource. Later, when we edit and delete the post we won't need to add those routes manually.

routes/api.php:

Route::get('posts', [PostController::class, 'index']);
Route::apiResource('posts', PostController::class);
Route::get('categories', [CategoryController::class, 'index']);

Vue Submit: Composable Method

Now in the posts composable, we need to create a new method for storing post, let's call it storePost(). It will accept the post object as a parameter. After creating the post, we will redirect the user to the posts index page. For this, we need to use the push() method from the vue-router Composable.

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 storePost = async (post) => {
axios.post('/api/posts', post)
.then(response => {
router.push({ name: 'posts.index' })
})
}
 
return { posts, getPosts }
return { posts, getPosts, storePost }
}

Similar to the getPosts() in the storePost(), we make an Axios HTTP request, but this time it's a POST request and we pass the post as a parameter.


Vue Submit: Component Method

Next, in the PostsCreate Vue component we need to import posts Composable and add the storePost() method. For the submit action, we now need to change it to use the storePost() from the composable.

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

<template>
<form @submit.prevent="submit">
<form @submit.prevent="storePost(post)">
// ...
</form>
</template>
 
<script setup>
import { onMounted } from 'vue';
import useCategories from '@/composables/categories';
import usePosts from '@/composables/posts';
 
const { categories, getCategories } = useCategories()
const { storePost } = usePosts()
 
const submit = () => {
console.log('submitted')
}
 
onMounted(() => {
getCategories()
})
</script>

Now, the storePost(post) parameters: that object needs to be a reactive object.

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

<script setup>
import { onMounted } from 'vue';
import { onMounted, reactive } from 'vue';
import useCategories from '@/composables/categories';
import usePosts from '@/composables/posts';
 
const post = reactive({
title: '',
content: '',
category_id: ''
})
// ...
</script>

Now, we need to add a v-model directive to all fields.

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

<template>
<form @submit.prevent="storePost(post)">
<!-- Title -->
<div>
<label for="post-title" class="block text-sm font-medium text-gray-700">
Title
</label>
<input id="post-title" type="text" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
<input v-model="post.title" id="post-title" type="text" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
</div>
 
<!-- Content -->
<div class="mt-4">
<label for="post-content" class="block text-sm font-medium text-gray-700">
Content
</label>
<textarea id="post-content" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"></textarea>
<textarea v-model="post.content" id="post-content" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"></textarea>
</div>
 
<!-- Category -->
<div class="mt-4">
<label for="post-category" class="block text-sm font-medium text-gray-700">
Category
</label>
<select id="post-category" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
<select v-model="post.category_id" id="post-category" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
<option value="" selected>-- Choose category --</option>
<option v-for="category in categories" :value="category.id">
{{ category.name }}
</option>
</select>
</div>
 
<!-- Buttons -->
<div class="mt-4">
<button class="rounded-md bg-indigo-600 px-3 py-2 text-sm uppercase text-white">Save</button>
</div>
</form>
</template>

And now our form saves the post to the DB and redirects to the posts list page.

create form filled

new post in the posts list