Hello guys,
I have been developing web applications with Vue since 7 years now and I am loving it!
Vue 3 just came out and I found interesting to write a quick post on how to create your first Vue 3 application with Tailwindcss.
1. Install Vue.js
Prerequisites: install Node.js version 15.0 or higher.
Use the CLI to build your app:
npm init vue@latest
Then:
✔ Project name: … <your-project-name>
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit testing? … No / Yes
✔ Add Cypress for both Unit and End-to-End testing? … No / Yes
✔ Add ESLint for code quality? … No / Yes
✔ Add Prettier for code formatting? … No / Yes
Scaffolding project in ./<your-project-name>...
Done.
Install supabase:
npm i @supabase/supabase-js
Finally:
npm i && npm run dev
Your project should be running now.
2. Install Tailwindcss
In a previous article, I explained every steps to follow. Please check this article to install Tailwindcss.
3. Clean your views & components
By default, you have components and views. Remove them and instead create theses files:
src/views/Home.vue
src/views/Post.vue
src/components/Header.vue
src/store.js
src/supabase.js
You can immediately change your Header.vue component to:
<template>
<header class=" text-center my-8">
<h1 class="text-blue-300 text-6xl font-bold mb-4" @click="$router.push('/')">Guillaume's blog</h1>
<p class="text-xl text-slate-500">A blog with posts on what I like.</p>
</header>
</template>
4. Vue-Router from App.vue
Here, we want a root-file (App.vue) that displays our routes.
<script setup>
import { RouterView } from 'vue-router'
import Header from './components/Header.vue'
</script>
<template>
<Header />
<RouterView />
</template>
Also, we need our router to display these pages:
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue')
}, {
path: '/post/:id',
name: 'Post',
component: () => import('../views/Post.vue')
}
]
})
export default router
Now, you should have 2 routes available.
5. Configure Supabase
Please check my video in order to create your table "Posts" and configure your Supabase.
Go to supabase.js and configure your client:
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
const supabase = createClient(supabaseUrl, supabaseAnonKey);
export default supabase
You should now create an .env file:
touch .env
VITE_SUPABASE_URL=
VITE_SUPABASE_ANON_KEY=
Please restart your server.
6. Let's create a store.
In store.js, let's create a reactive constant. Reactive is important here to make your variable dynamic and display elements. Otherwise, in your app, data will stay blank.
import { reactive } from 'vue'
const store = reactive({
posts: []
})
export {store};
7. Home is home
Our Home.vue view display a list of posts fetched from Supabase.
In order to do so, we create a fetchPosts function triggered immediately on mount.
If there are no posts, a message appear and says: 'there is no posts'.
<script setup>
import { store } from '../store'
import supabase from '../supabase'
const getWordsNumber = (str) => (str.split(' ').length)
const fetchPosts = async () => {
let { data: posts, error } = await supabase
.from('posts')
.select()
if (error) throw new Error(error)
store.posts = posts
}
fetchPosts()
</script>
<template>
<div class="Home">
<main class="container mx-auto">
<div v-if="store.posts.length < 1">
There is no posts.
</div>
<div v-else>
<div class="PostItem border border-slate-200 mb-4 p-4 rounded-lg cursor-pointer" v-for="item, itemIndex in store.posts" :key="itemIndex" @click="$router.push(`/post/${item.id}`)">
<h1 class="text-slate-900 text-3xl font-bold">
{{ item.title }}
</h1>
<p>{{ getWordsNumber(item.description) }} words.</p>
</div>
</div>
</main>
</div>
</template>
You might see that on click, we go to post route previously created.
8. Post view
Our post view is a bit different.
We also have to get our post from Supabase otherwise our data will be empty.
Please take a look at fetchPost function. There is a guard: if our post is already in our store, we won't fetch but apply our found item to post variable.
<script setup>
import { reactive } from 'vue'
import { useRoute } from 'vue-router'
import {store} from '../store'
import supabase from '../supabase'
const route = useRoute()
let post = reactive({})
const fetchPost = async (id) => {
const found = store.posts.find(x => x.id === parseInt(route.params.id))
if (found) {
Object.assign(post, found)
return
}
let { data, error } = await supabase
.from('posts')
.select()
.eq('id', id)
.single()
if (error) throw new Error(error)
Object.assign(post, data)
}
fetchPost(route.params.id)
</script>
<template>
<div class="Post text-center container mx-auto">
<div v-if="!post">
No post found.
</div>
<div v-else>
<h1 class="text-slate-900 text-3xl font-bold mb-4">{{ post.title }}</h1>
<p class="text-md text-slate-300">{{ post.created_at }}</p>
<p class="text-xl text-slate-500">{{ post.description}}</p>
</div>
</div>
</template>
I hope you enjoyed this quick article, if you have any question or best practices, please be my guest.
Best !
Guillaume