DEV Community

Connie Leung
Connie Leung

Posted on

Day 27 - Create a simple blog page

On day 27, I completed Vue 3 Composition API course to build a simple blog to show blog posts. The site calls https://jsonplaceholder.typicode.com/posts to retrieve all the posts and display the users.

The blog posts are built in 3 parts:

  • Part 1: Create a usePost composable to retrieve all posts and a post
  • Part 2: Create a useUser composable to retrieve a user. Use watch to monitor post and retrieve a user
  • Part 3: Build a reusable composable to retrieve Post and User resources
  • Part 4: Add a loading indicator

Install Tailwindcss

Refer to https://tailwindcss.com/docs/installation/framework-guides to install TailwindCSS for Vue 3, Svelte 5 and Angular.

Copy Vue boilerplates

Copy the boilerplates from https://github.com/vueschool/vue-3-composition-api/tree/boilerplate/ to your Vue repo. The author wrote the codes in JavaScript and without the <script setup lang="ts"> syntactic sugar, so I rewrote it with TypeScript.

Create Common Types

Create a Post type to retrieve id, title, body and userId.

export type Post = { userId: number; id: number; title: string; body: string; } 
Enter fullscreen mode Exit fullscreen mode

Create a User type to retrieve id, name.

export type User = { id: number; name: string; } 
Enter fullscreen mode Exit fullscreen mode

Define the Routes

Vue 3 application

import { createRouter, createWebHistory } from 'vue-router' import Home from '../views/Home.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', name: 'Home', component: Home, }, { path: '/post/:id', name: 'Post', component: () => import('../views/Post.vue'), }, ], }) export default router 
Enter fullscreen mode Exit fullscreen mode

I have not learned Vue Router yet, so I use the generated routes of the Vue 3 app.

SvelteKit application

SvelteKit uses file based routing.

routes/ |-----posts/[id] |-----+layout.svelte |-----+page.svelte |-----+page.server.ts |-----+layout.svelte |-----+page.server.ts |-----+page.svelte 
Enter fullscreen mode Exit fullscreen mode

The home page is designed in +page.svelte and +page.server.ts defines a load function to retrieve all posts on the server side.

Under posts/[id] folder, +page.svelt is the details page and +page.ts retrieves a specific post by id.

Angular 20 application

import { Routes } from '@angular/router'; import { postResolver } from './post/resolvers/post.resolver'; export const routes: Routes = [ { path: 'home', loadComponent: () => import('./home/home.component').then(m => m.HomeComponent), }, { path: 'post/:id', loadComponent: () => import('./post/post.component'), }, ... other routes ... ]; 
Enter fullscreen mode Exit fullscreen mode

Create home and post/:id routes that load the HomeComponent and PostComponent lazily.

Load Posts and Post data

Vue 3 application

Create ausePost composable under src/composables folder.

Implement a fetchAll function and declare a posts ref to retrieve all posts.

import type { Post } from '@/types/post' import { ref } from 'vue' export function usePost() { const posts = ref<Post[]>([]) const baseUrl = 'https://jsonplaceholder.typicode.com/posts' function fetchAll() { return fetch(baseUrl) .then((response) => response.json() as Promise<Post[]>) .then((data) => (posts.value = data)) .catch((err) => alert(err)) } return { posts, fetchAll, } } 
Enter fullscreen mode Exit fullscreen mode

The fetchAll makes a request to the baseUrl to retrieve all posts and assign the posts toposts.value.

import type { Post } from '@/types/post' import { ref } from 'vue' export function usePost() { const post = ref<Post | null>(null) function fetchOne(id: number) { return fetch(`${baseUrl}/${id}`) .then((response) => response.json() as Promise<Post>) .then((data) => (post.value = data)) } return { posts, post, fetchAll, fethcOne, } } 
Enter fullscreen mode Exit fullscreen mode

Next, declare a post ref and implement a fetchOne function. The fetchOne function accepts a post ID to retrieve a blog post and assigns the result to post.value.

Then, the composable returns all posts, post, fetchAll and fetchOne so the components can accesss.

SvelteKit application

export const BASE_URL = 'https://jsonplaceholder.typicode.com'; 
Enter fullscreen mode Exit fullscreen mode
import type { PageServerLoad } from './$types'; import { BASE_URL } from '$lib/constants/posts.const'; import type { Post } from '$lib/types/post'; import type { RequestHandler } from '@sveltejs/kit'; // retreive all posts export const load: PageServerLoad = async ({ fetch }: RequestHandler) => { const posts = await fetch(`${BASE_URL}/posts`) .then((response) => response.json() as Promise<Post[]>) .catch((error) => { console.error('Error fetching posts:', error); return [] as Post[]; }); return { posts }; }; 
Enter fullscreen mode Exit fullscreen mode

The load function uses the native fetch function to retrieve all blog posts.

import { BASE_URL } from '$lib/constants/posts.const'; import type { Post } from '$lib/types/post'; import type { PageServerLoad } from './$types'; // retreive a post by an ID export const load: PageServerLoad = async ({ params, fetch }): Promise<{ post: Post | undefined }> => { console.log('params', params); const post = await fetch(`${BASE_URL}/posts/${params.id}`) .then((response) => response.json() as Promise<Post>) .catch((error) => { console.error('Error fetching posts:', error); return undefined; }); return { post }; }; 
Enter fullscreen mode Exit fullscreen mode

This load function runs on the server and makes a HTTP request to retrieve a post by an ID.

Angular 20 application

Provide the provideHttpClient provider in the ApplicationConfig.

import { provideRouter, withComponentInputBinding } from '@angular/router'; import { routes } from './app.routes'; import { provideHttpClient } from '@angular/common/http'; export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes, withComponentInputBinding()), provideHttpClient(), ] }; 
Enter fullscreen mode Exit fullscreen mode

It is because httpResource uses HttpClient under the hood and I also need it to make a GET request to retrieve a post.

The provideRouter is a provider that configures the routes. withComponentInputBinding is a feature that converts routed data, path parameters, and query parameters to input signala. It simplifies routing data to routed components immensely.

Create a PostsService.

import { httpResource } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Post } from '../types/post.type'; const BASE_URL = 'https://jsonplaceholder.typicode.com/posts'; @Injectable({ providedIn: 'root' }) export class PostsService { posts = httpResource<Post[]>(() => BASE_URL, { defaultValue: [] as Post[] }); } 
Enter fullscreen mode Exit fullscreen mode

I use the experimental httpResource to create a posts resource to retrieve all posts.

import { httpResource } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Post } from '../types/post.type'; const BASE_URL = 'https://jsonplaceholder.typicode.com/posts'; @Injectable({ providedIn: 'root' }) export class PostsService { readonly httpService = inject(HttpClient); getPost(id: number): Observable<Post> { return this.httpService.get<Post>(`${BASE_URL}/${id}`); } } 
Enter fullscreen mode Exit fullscreen mode

I implemented a getPost method to retrieve a post. Thie method will be used in a route resolver during route navigation.

import { inject } from '@angular/core'; import { ActivatedRouteSnapshot } from '@angular/router'; import { of } from 'rxjs'; import { PostsService } from '../services/posts.service'; export const postResolver = (route: ActivatedRouteSnapshot) => { const postId = route.paramMap.get('id'); if (!postId) { return of(undefined); } return inject(PostsService).getPost(+postId); } 
Enter fullscreen mode Exit fullscreen mode

Create a postResolver that retrieves a post by the ID path parameter.

Update the post/:id route to calls the the postResolver.

{ path: 'post/:id', loadComponent: () => import('./post/post.component'), resolve: { post: postResolver } } 
Enter fullscreen mode Exit fullscreen mode

The PostComponent has post input signal that is a Post.

Replace the Post mock data with the fetch calls

Vue 3 application

In the Home component, import the usePost composable and destructure fetchAll and posts.

<script setup lang="ts"> import PostCard from '../components/PostCard.vue' import { usePost } from '@/composables/usePost' const { posts, fetchAll } = usePost() fetchAll() </script> 
Enter fullscreen mode Exit fullscreen mode

Invoke the fetchAll function to retrieve all posts asynchronously. Then, delete the mock data of the posts array to display the real data.

<template> <div class="flex flex-wrap flex-grow"> <PostCard v-for="post in items" :key="post.id" :post="post" /> </div> </template> 
Enter fullscreen mode Exit fullscreen mode

In the Post component, import usePost and invoke fetchOne to retrieve a post by an ID.

import { useRoute } from 'vue-router' import { usePost } from '@/composables/usePost' const { post, fetchOne } = usePost() const { params } = useRoute(); fetchOne(+params.id) 
Enter fullscreen mode Exit fullscreen mode

SvelteKit application

In routes/+page.svelte, the load function is executed and the page data can be obtained by $props() macro.

Destructure posts from data to get all the posts.

<script lang="ts"> import type { PageProps } from './$types'; import PostCard from '$lib/components/post-card.svelte'; const { data }: PageProps = $props(); const { posts } = data; </script> 
Enter fullscreen mode Exit fullscreen mode
<div class="flex flex-grow flex-wrap"> {#each posts as post (post.id)} <PostCard {post} /> {/each} </div> 
Enter fullscreen mode Exit fullscreen mode

The template iterates the posts and passes each post to the PostCard component.

<script lang="ts"> import type { Post } from '$lib/types/post'; import { resolve } from '$app/paths'; type Props = { post: Post; }; const { post }: Props = $props(); const postUrl = resolve('/posts/[id]', { id: `${post.id}` }); </script> 
Enter fullscreen mode Exit fullscreen mode

The PostCard component uses the resolve function to determine the URL for navigation. /posts/[id] is the route path and id is the path parameter.

<div> <img src="https://placehold.co/150" alt="placeholder" style="background: #cccccc" width="150" height="150" /> <a href={postUrl}> {post.title} </a> </div> 
Enter fullscreen mode Exit fullscreen mode
<script lang="ts"> import type { PageProps } from './$types'; const { data }: PageProps = $props(); const { post } = data; </script> 
Enter fullscreen mode Exit fullscreen mode

In posts/[id]/+page.svelte, the load function retrieves a post by an ID.

<div class="mb-10"> <h1 class="text-3xl">{ post.title }</h1> <div class="text-gray-500 mb-10">by Connie</div> <div class="mb-10">{ post.body }</div> </div> 
Enter fullscreen mode Exit fullscreen mode

The template displays post title and body dynamically, and the user name is hardcoded currently.

Angular 20 application

import { ChangeDetectionStrategy, Component, input, signal } from '@angular/core'; import { Post } from './types/post.type'; import { User } from './types/user.type'; @Component({ selector: 'app-post', styles: ` @reference "../../styles.css"; :host { @apply flex m-2 gap-2 items-center w-1/4 shadow-md flex-grow rounded overflow-hidden } `, template: `...inline template...`, changeDetection: ChangeDetectionStrategy.OnPush, }) export default class PostComponent { post = input<Post>(); user = signal<User>({ id: 1, name: 'Connie', }); } 
Enter fullscreen mode Exit fullscreen mode

The post is resolved during route navigation, so it is available to the PostComponent as an input signal. The user signal is currently hardcoded value but a request will be made to retrieve the post user.

@let myUser = user(); @let myPost = post(); @if (myPost && myUser) { <div class="mb-10"> <h1 class="text-3xl">{{ myPost.title }}</h1> <div class="text-gray-500 mb-10">by {{ myUser.name }}</div> <div class="mb-10">{{ myPost.body }}</div> </div> } 
Enter fullscreen mode Exit fullscreen mode

The @let syntax allows temporary assignment of the signal getter functions. When myUser and myPost are defined, the post title, post body, and user name are displayed.

We have successfully displayed the home page, details page, and set up the routing of this simple blog.

Github Repositories

Resources

Top comments (0)