SKJ Blog

Menu

.
back to js articles

Build a News App with VueJs 3, ViteJS and TailwindCSS with Composition Api

Shallom Kyle Jacinto

April 21st 2021 5 minute read

Build a news app with progressive ui framework (vuejs) and next generation tool for frontend compiling (vitejs) and a utility first css framework (tailwind css).

Prerequisites

A node.js installed in your computer.

A knowledge of Vue 2 or Vue 3

A basic knowledge of Tailwind CSS

Setup

To create a vue vite app, run ...

npm init @vitejs/app news-app

Then choose vue for framework and select javascript for variant

cd news-app
npm install

To install latest tailwind css version, run ...

npm install -D tailwindcss@latest postcss@latest autoprefixer@latest

To create a tailwind.config.js and postcss.config.js run ...

npx tailwindcss init -p

Inside in our tailwind.config.js, add this configuration ...

- purge: []
+ purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],

This will remove all unused classes in tailwind.

And add index.css into ./src/assets/index.css and then inside in our css, add ...

@tailwind base;
@tailwind components;
@tailwind utilities;

And in your main.js, import the index.css

import './assets/index.css'

App Structure

- node_modules
- public 
- src
  - assets
    - index.css
    - logo.png
  - components
    - ErrorCard.vue
    - Loading.vue
    - Navbar.vue
    - NewsList.vue
  - composition
    - useFetch.js
  - router
    - index.js
  - views
    - Home.vue
    - TopHeadlines.vue
  - App.vue
  - main.js
- .gitignore
- index.html
- package.json
- postcss.config.js
- tailwind.config.js
- vite.config.js

Routes

To create our routes, open a new tab of terminal and run ...

npm install vue-router@4

and create a new folder name routes and create a file `index.js

index.js

import { createRouter, createWebHistory } from 'vue-router'
const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('../views/Home.vue')
  },
  {
    path: '/top-headlines',
    name: 'Top Headlines',
    component: () => import('../views/TopHeadlines.vue')
  },
]
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})
export default router

Create a folder name views which will define all our pages and then create a two files, Home.vue and TopHeadlines.vue

Home.vue

<template>
  <div>
    <p>Home</p>
  </div>
</template>

TopHeadlines.vue

<template>
  <div>
    <p>Top Headlines</p>
  </div>
</template>

App.vue

<template>
   <div>
      <router-view />
   </div>
</template>

And import the router on main.js

import router from './router/
...
.use(router)

To create our Navbar with router-links, we must create a new component Navbar.vue.

<template>
  <div class="px-4 py-3 flex flex-wrap justify-between shadow-sm">
    <div><span class="text-indigo-400">Vue 3</span> News</div>
    <div>
      <router-link exact to='/' class="mr-4">Home</router-link>
      <router-link to="/top-headlines">Top Headlines</router-link>
    </div>
  </div>
</template>

<style>
.router-link-exact-active {
  @apply text-indigo-400 border-b-2 border-indigo-500;
}
</style>

To add an active class, we put a exact attribute in home router to tell our vue-router that home is different with other paths and we add a .router-link-exact-active for styling.

App.vue

<template>
  <Navbar />
  <div class="px-4 py-4">
    <router-view /> 
  </div>
</template>
<script>
import Navbar from './components/Navbar.vue'
</script>

Navbar

Reusable Composition

In our app it's not a good practice that in every pages like fetching data in our home are the same on fetching data on other pages code, so to make our code maintainable, we create a folder composition which we will place all our reusable compositions and we create a file useFetch.js means this will be only use if there is fetching.

useFetch.js

import { toRefs, reactive } from 'vue'
export default function(url) {
  const state = reactive({
    data: [],
    error: null,
    fetching: false
  })
  const fetchData = async () => {
    state.fetching = true
    try {
      await fetch(url)
        .then(response => response.json())
        .then(result => {
          if(result.status == "error") {
            state.error = result.message
          } else {
            state.data = result
          }
        })
    } 
    catch(e) {
      state.error = e
    }
    finally {
      state.fetching = false
    }
  }
  return {...toRefs(state), fetchData}
}

As you can see we pass the parameter in function url, we create our own state with reactive and an object of data which will contain all our data, error if there's an error, and fetching to tell our users that our data are still fetching or loading.

In our fetchData function, we set the fetching into true, we use a try catch block and inside in try block, we fetch the url and to get the data, we response it with json, we make an if else statement that if result is error then we set the error with message and else we set the data.

We return the state with toRefs means that we return our reactive object into plain object.

News

To get a news, first we must create an account in newsapi.org and after that go to account settings and copy your apiKey.

Home.vue

<template>
  <div>
    <div v-if="error" class="m-auto md:w-1/2">
      <ErrorCard :error="error" />
    </div>
    <div v-else-if="fetching" class="m-auto md:w-1/2">
      <Loading />
    </div>
    <div v-else class="m-auto md:w-1/2">
      <NewsList :data="data.articles" />
    </div>
  </div>
</template>

<script>
import { onMounted, ref } from 'vue'
import useFetch from '../composition/useFetch'
import NewsList from '../components/NewsList.vue'
import Loading from '../components/Loading.vue'
import ErrorCard from '../components/ErrorCard.vue'

export default {
  components: {
    NewsList,
    Loading,
    ErrorCard
  },
  setup() {
    const { fetchData, data, error, fetching } = useFetch('https://newsapi.org/v2/everything?q=news&apiKey=__your__api__key')
    
    onMounted(async () => {
      await fetchData()
    })
  return { fetchData, data, error, fetching }
  }
}
</script>

As you can see we create a lifecycle hook onMounted and then we fetchData().

We use destructuring to get the keys, we import the useFetch, we pass the url api with apiKeys and then return all our state which is data,error, fetching.

We use v-if if there's an error, and then Display the loading if our app is still fetching and we pass our data as a props if fetching is done.

NewsList.vue

<template>
  <div>
    <div v-for="article in data" :key="article.id" class="p-4 bg-gray-50 mb-4 rounded-lg">
      <img :src="article.urlToImage" class="rounded-lg w-full ">
      <p class="mt-2 text-indigo-500 font-bold">{{ article.author }}</p>
      <p class="tracking-wide text-gray-700 font-medium mt-1">{{ article.title }}</p>
      <p class="mt-2 text-gray-700 text-sm">{{ article.description }}</p>
      <a :href="article.url" target="_blank" class="inline-block mt-3 bg-indigo-500 text-white px-3 py-1.5 rounded">Read more</a>
    </div>
  </div>
</template>

<script>
  export default {
    props: {
      data: Array
    }
  }
</script>

We loop our data props with v-for.

If fetching ...

If done fetching...

And for our Top Headlines route, we just copy all codes and replace our url into https://newsapi.org/v2/top-headlines?country=us&apiKey=your__api__key. Thats it!

Source code

For source code, open this github repo


js vuejs vitejs tailwind