Appearance
Convert a ReactJS Component to VueJS
In this tutorial, we're going to rewrite a Timeline component built in ReactJS by Florin Pop to VueJS. You can see the component in action here.
Scaffold your VueJS app using Vite
I try to use Vite whenever given the opportunity.
It increases development speed by an incredibly amount due to features such as Instant Server Start and Lightning Fast HMR (Hot Module Replacement).
- Let's init our project:
npm init vite@latest
- Follow the prompt and select
vue
as our framework and and as our variant. cd
into the generated directory and install the dependencies withnpm install
.- Run your new vite + vuejs project using
npm run dev
.
This will generate a project that has the following directory structure:
Building the VueJS Component
Now, the fun stuff. Let's work on converting the ReactJS code.
App.vue
looks very similar to the ReactJS version:
// App.vue
<script setup>
import Timeline from './components/Timeline.vue'
</script>
<template>
<Timeline />
</template>
// React Version
const App = () => (
<>
<h1>React Timeline</h1>
<Timeline />
</>
)
The key difference here is the template tag, an essential part of VueJS syntax.
Lets now dig into the Timeline component - Timeline.vue
The Timeline component is where the data gathering and container occur.
In this example, we are gathering the data from our local data.json
file. It shouldn't require too much additional work to get this component to work with live data using a package such as axios.
While the ReactJS version takes up less vertical space, the VueJS version is much easier to read. Rather than applying an inline map function, we use a v-for
to apply the same functionality.
VueJS attaches dynamic data a property called v-bind. The shorthand for v-bind is :
. As you can see :data=data
is the VueJS equivalent to data={data}
.
Also note, VueJS doesn't use className
to apply its CSS. Instead, you can use the classic class
keyword.
// React Version
const Timeline = () =>
timelineData.length > 0 && (
<div className="timeline-container">
{timelineData.map((data, idx) => (
<TimelineItem data={data} key={idx} />
))}
</div>
)
// components/Timeline.vue
<template>
<div class="timeline-container">
<TimelineItem v-for="(data, idx) in timelineData" :data="data" :key="idx" />
</div>
</template>
<script>
import json from '../assets/data.json'
import TimelineItem from './TimelineItem.vue'
export default {
components: {
TimelineItem,
},
data: () => ({
timelineData: json,
}),
}
</script>
Here's an example of what the data looks like:
[
{
"text": "Started working on the app-ideas repository",
"date": "February 25 2021",
"category": {
"tag": "app-ideas",
"color": "#FFDB14"
},
"link": {
"url": "https://github.com/florinpop17/app-ideas",
"text": "Check it out on GitHub"
}
},
...
]
Converting the TimelineItem Component to VueJS
The TimelineItem Component is where the majority of the UI logic is taking place. We are now playing with the data we gathered within the Timeline component.
There isn't much happening outside of destructing the data object and styling as needed.
We destructure data in VueJS differently than in ReactJS.
- When using the data between two HTML elements, you must destructure it using two curly brackets
- When accessing the data within a
v-bind:
, you destructure it using single curly brackets{data}
- When accessing the data within a directive such as
v-if
, you type in the datav-if="data"
const TimelineItem = ({ data }) => (
<div className="timeline-item">
<div className="timeline-item-content">
<span className="tag" style={{ background: data.category.color }}>
{data.category.tag}
</span>
<time>{data.date}</time>
<p>{data.text}</p>
{data.link && (
<a href={data.link.url} target="_blank" rel="noopener noreferrer">
{data.link.text}
</a>
)}
<span className="circle" />
</div>
</div>
)
// components/TimelineItem.vue
<template>
<div class="timeline-item">
<div class="timeline-item-content">
<span class="tag" :style="{ background: `${data.category.color}` }">
{{ data.category.tag }}
</span>
<time>{{ data.date }}</time>
<p>{{ data.text }}</p>
<a
v-if="data.link"
:href="data.link.url"
target="_blank"
rel="noopener noreferrer"
>
{{ data.link.text }}
</a>
<span class="circle" />
</div>
</div>
</template>
<script>
export default {
props: {
data: {
type: Object,
required: true,
},
},
}
</script>
<style></style>
Additional VueJS config
To get the CSS provided below to style our component, we need to expose the CSS to work within our VueJS application. For this example, I placed an import statement within my main.js
file.
import { createApp } from 'vue'
import App from './App.vue'
import './assets/main.css'
createApp(App).mount('#app')
Styling the component using CSS
For this component, I am using the main.css
file placed within our assets
directory. Here is the css used within this component:
/* assets/main.css */
@import url('https://fonts.googleapis.com/css?family=Lato');
* {
box-sizing: border-box;
}
body {
background-image: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
min-height: 100vh;
font-family: 'Lato', sans-serif;
margin: 0;
}
h1 {
text-align: center;
}
#app {
padding: 0 20px;
width: 100%;
}
.timeline-container {
display: flex;
flex-direction: column;
position: relative;
margin: 40px 0;
}
.timeline-container::after {
background-color: #e17b77;
content: '';
position: absolute;
left: calc(50% - 2px);
width: 4px;
height: 100%;
}
.timeline-item {
display: flex;
justify-content: flex-end;
padding-right: 30px;
position: relative;
margin: 10px 0;
width: 50%;
}
.timeline-item:nth-child(odd) {
align-self: flex-end;
justify-content: flex-start;
padding-left: 30px;
padding-right: 0;
}
.timeline-item-content {
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
border-radius: 5px;
background-color: #fff;
display: flex;
flex-direction: column;
align-items: flex-end;
padding: 15px;
position: relative;
width: 400px;
max-width: 70%;
text-align: right;
}
.timeline-item-content::after {
content: ' ';
background-color: #fff;
box-shadow: 1px -1px 1px rgba(0, 0, 0, 0.2);
position: absolute;
right: -7.5px;
top: calc(50% - 7.5px);
transform: rotate(45deg);
width: 15px;
height: 15px;
}
.timeline-item:nth-child(odd) .timeline-item-content {
text-align: left;
align-items: flex-start;
}
.timeline-item:nth-child(odd) .timeline-item-content::after {
right: auto;
left: -7.5px;
box-shadow: -1px 1px 1px rgba(0, 0, 0, 0.2);
}
.timeline-item-content .tag {
color: #fff;
font-size: 12px;
font-weight: bold;
top: 5px;
left: 5px;
letter-spacing: 1px;
padding: 5px;
position: absolute;
text-transform: uppercase;
}
.timeline-item:nth-child(odd) .timeline-item-content .tag {
left: auto;
right: 5px;
}
.timeline-item-content time {
color: #777;
font-size: 12px;
font-weight: bold;
}
.timeline-item-content p {
font-size: 16px;
line-height: 24px;
margin: 15px 0;
max-width: 250px;
}
.timeline-item-content a {
color: #333;
text-decoration: none;
font-size: 14px;
font-weight: bold;
}
.timeline-item-content a::after {
content: ' ►';
font-size: 12px;
}
.timeline-item-content .circle {
background-color: #fff;
border: 3px solid #e17b77;
border-radius: 50%;
position: absolute;
top: calc(50% - 10px);
right: -40px;
width: 20px;
height: 20px;
z-index: 100;
}
.timeline-item:nth-child(odd) .timeline-item-content .circle {
right: auto;
left: -40px;
}
@media only screen and (max-width: 1023px) {
.timeline-item-content {
max-width: 100%;
}
}
@media only screen and (max-width: 767px) {
.timeline-item-content,
.timeline-item:nth-child(odd) .timeline-item-content {
padding: 15px 10px;
text-align: center;
align-items: center;
}
.timeline-item-content .tag {
width: calc(100% - 10px);
text-align: center;
}
.timeline-item-content time {
margin-top: 20px;
}
.timeline-item-content a {
text-decoration: underline;
}
.timeline-item-content a::after {
display: none;
}
}
footer {
background-color: #222;
color: #fff;
font-size: 14px;
bottom: 0;
position: fixed;
left: 0;
right: 0;
text-align: center;
z-index: 999;
}
footer p {
margin: 10px 0;
}
footer i {
color: red;
}
footer a {
color: #3c97bf;
text-decoration: none;
}
Additional Resources
- Github Repository with source code - https://github.com/CodyBontecou/timeline-component-vuejs
- Florin Pop made a Youtube video walking through building this component using ReactJS.
- ReactJS Codepen