Christmas is coming!
And for the annual Christmas-themed web tutorial, we're going to embark on something deliciously frivolous - a ReactJS App that interactively shows the lyrics of the Christmas carol
The Twelve Days of Christmas.
For this, I will be assuming you already know the steps to creating an app through NodeJS Package Manager. (And if you don't here's a
link!) And as such, I'll mainly be walking through the setup, the implementation and testing. This isn't really meant to be a mobile app, so I'm not going to bother with the bootstrapping and shit.
Basic setup
We start off with a freshly created ReactJS App. There are some changes I like to make to a newly-minted app, such as changing the title. You don't have to, and choosing not to do so will not affect your app in any way.
public\index.html<title>12 Days of Christmas</title>
Now let's do something a bit more useful. In the
src folder, there's where we will be working most of the time. I am going to be working with a lot of SVG files, twelve of them, to be exact. So let's create a sub-folder,
svg, within
src.
In that folder are the SVG fies I will be working with. All of them were generated using
Method Draw SVG Editor, a free tool which served its purpose superbly.
If you want the files, they are at this
link.
Got all that? This is the easy part, so don't get lost here.
Clean up App
In the
src folder, we will need
App.js and
App.css; however, we will not need whatever NPM generated for us, so feel free to remove all that code inside those two files. We can keep
index.js and
index.css; no need to touch those.
This isn't going to be mobile-friendly, so for the App CSS class, just align all text center.
src/App.css.App {
text-align: center;
}
Here, we'll need to import the CSS file. Also, import the
React from the
react library.
src/App.jsimport React from 'react';
import './App.css';
Add the
App() function.
src/App.jsimport React from 'react';
import './App.css';
function App() {
}
It should return JSX - a div with a class name of
App (which we've styled) and some text in a h1 tag. Note the extra space in the middle of that text; we're going to fill it right up later!
src/App.jsimport React from 'react';
import './App.css';
function App() {
return (
<div className="App">
<h1>On the day of Christmas my true love gave to me</h1>
</div>
);
}
Finally, we export
App so that the parent (
index.js) can use it.
src/App.jsimport React from 'react';
import './App.css';
function App() {
return (
<div className="App">
<h1>On the day of Christmas my true love gave to me</h1>
</div>
);
}
export default App;
Take a look!
The aim here is to be able to adjust the number of the day. To do that, we'll need controls. Add this placeholder to the text.
src/App.js<h1>On the { dayControls } day of Christmas my true love gave to me</h1>
Now, within the
App() function, we will create the
dayControls component. It will consist of a div with a class of
dayControl.
src/App.jsimport React from 'react';
import './App.css';
function App() {
const dayControls = (
<div className="dayControl">
</div>
);
return (
<div className="App">
<h1>On the { dayControls } day of Christmas my true love gave to me</h1>
</div>
);
}
export default App;
Within that div, we have two more divs, styled using
dayText and
dayButtons, respectively.
src/App.jsimport React from 'react';
import './App.css';
function App() {
const dayControls = (
<div className="dayControl">
<div className="dayText">
</div>
<div className="dayButtons">
</div>
</div>
);
return (
<div className="App">
<h1>On the { dayControls } day of Christmas my true love gave to me</h1>
</div>
);
}
export default App;
And in that second div, we'll put two more divs. Those will be our up and down buttons. Style them using the class
dayButton.
src/App.jsimport React from 'react';
import './App.css';
function App() {
const dayControls = (
<div className="dayControl">
<div className="dayText">
</div>
<div className="dayButtons">
<div className="dayButton">▲</div>
<div className="dayButton">▼</div>
</div>
</div>
);
return (
<div className="App">
<h1>On the { dayControls } day of Christmas my true love gave to me</h1>
</div>
);
}
export default App;
OK, just some styling here.
dayControl needs to be inline, but we also want to specify width, so the
display property will be set to
inline-block.
src/App.css.App {
text-align: center;
}
.dayControl {
display: inline-block;
width: 4em;
}
The
dayButtons div will float left of its parent. We'll give it a nominal width and height.
src/App.css.App {
text-align: center;
}
.dayControl {
display: inline-block;
width: 4em;
}
.dayButtons {
float: left;
width: 1em;
height: 100%;
}
From here on, the
dayButton CSS class is mostly a matter of aesthetic choice.
src/App.css.App {
text-align: center;
}
.dayControl {
display: inline-block;
width: 4em;
}
.dayButtons {
float: left;
width: 1em;
height: 100%;
}
.dayButtons .dayButton{
width: 100%;
height: 50%;
text-align: right;
background: transparent;
color: rgb(0, 0, 0);
font-weight: normal;
font-size: 0.5em;
cursor: pointer;
}
.dayButtons .dayButton:hover{
color: rgb(100, 100, 100);
font-weight: bold;
}
And this is what it all looks like.
Now let's introduce another variable in there -
currentDay. This is the variable that is eventually going to control how it all looks.
src/App.jsconst dayControls = (
<div className="dayControl">
<div className="dayText">
{ currentDay }
</div>
<div className="dayButtons">
<div className="dayButton">▲</div>
<div className="dayButton">▼</div>
</div>
</div>
);
currentDay is state data, and to manage that, we'll use React hooks. To do that, we need to import
useState.
src/App.jsimport React, { useState } from 'react';
Then, using the
useState() function we just imported, we declare
currentDay as a hook, and
setCurrentDay as the function that alters the value of that piece of data. The default will be 1, which we'll pass into
useState as an argument.
src/App.jsimport React, { useState } from 'react';
import './App.css';
function App() {
const [currentDay, setCurrentDay] = useState(1);
const dayControls = (
<div className="dayControl">
<div className="dayText">
{ currentDay }
</div>
<div className="dayButtons">
<div className="dayButton">▲</div>
<div className="dayButton">▼</div>
</div>
</div>
);
In the CSS, let's set the text to
red.
src/App.css.dayControl {
display: inline-block;
width: 4em;
}
.dayText {
color: #FF0000;
float: right;
width: 3em;
}
.dayButtons {
float: left;
width: 1em;
height: 100%;
}
Yep!
Let's do some magic here. Add
onClick events to the buttons, and declare their respective functions.
src/App.jsconst [currentDay, setCurrentDay] = useState(1);
const BtnClickUp = () => {
}
const BtnClickDown = () => {
}
const dayControls = (
<div className="dayControl">
<div className="dayText">
{ currentDay }
</div>
<div className="dayButtons">
<div className="dayButton" onClick={ BtnClickUp }>▲</div>
<div className="dayButton" onClick={ BtnClickDown }>▼</div>
</div>
</div>
);
When the up button is clicked, we want to use
setCurrentDay() to increment the value of
currentDay. But only if
currentDay is not still less than 12. Similarly, we'll set a floor for
currentDay at 1, if the down button is clicked. Because we're only going to go from Day 1 to 12, yes?
src/App.jsconst BtnClickUp = () => {
if (currentDay < 12) {
setCurrentDay(currentDay + 1);
}
}
const BtnClickDown = () => {
if (currentDay > 1) {
setCurrentDay(currentDay - 1);
}
}
This guy should now respond to your button clicks!
But that's not nearly enough. Let's add a suffix to that number. In this component, right after
currentDay, add a sup tag. And in that tag, introduce the variable
daySuffix, which is an array. The index of the array will be the value of
currentDay, minus 1.
src/App.jsconst dayControls = (
<div className="dayControl">
<div className="dayText">
{ currentDay }
<sup>
{ daySuffix[currentDay - 1] }
</sup>
</div>
<div className="dayButtons">
<div className="dayButton" onClick={ BtnClickUp }>▲</div>
<div className="dayButton" onClick={ BtnClickDown }>▼</div>
</div>
</div>
);
And now, we will add the
daySuffix array. It's basically a list of suffixes that come after each number - 1st, 2nd, 3rd and so on until we reach 12. There's probably a less heavy-handed way of doing this, but it's just twelve elements and there are better uses for my time.
src/App.js const [currentDay, setCurrentDay] = useState(1);
const daySuffix = [
"st",
"nd",
"rd",
"th",
"th",
"th",
"th",
"th",
"th",
"th",
"th",
"th"
]
const BtnClickUp = () => {
if (currentDay < 12) {
setCurrentDay(currentDay + 1);
}
}
Now try it!
That's just the first line of the song, though. The rest will be handled by the use of a reusable component. I'm gonna call it, rather unimaginatively,
Row. First, we do an import. We will import
Row from the directory
components and subdirectory
Row, both of which we haven't created yet.
src/App.jsimport React, { useState } from 'react';
import './App.css';
import Row from './components/Row';
And now we're going to do something really lazy. In the return statement, we will add twelve instances of the
Row component. In each one, we will pass in the value of
currentDay as an attribute. We will also pass in the
day attribute, numbered from 12 to 1.
src/App.jsreturn (
<div className="App">
<h1>On the { dayControls } day of Christmas my true love gave to me</h1>
<Row currentDay = {currentDay} day="12" />
<Row currentDay = {currentDay} day="11" />
<Row currentDay = {currentDay} day="10" />
<Row currentDay = {currentDay} day="9" />
<Row currentDay = {currentDay} day="8" />
<Row currentDay = {currentDay} day="7" />
<Row currentDay = {currentDay} day="6" />
<Row currentDay = {currentDay} day="5" />
<Row currentDay = {currentDay} day="4" />
<Row currentDay = {currentDay} day="3" />
<Row currentDay = {currentDay} day="2" />
<Row currentDay = {currentDay} day="1" />
</div>
);
Next, create the directory
components. In that directory, create another directory
Row. Then create
Row.js within it.
In that file, we will import
React again.
src/components/Row/Row.jsimport React from 'react';
Also import
Row.css. We will create that file soon.
src/components/Row/Row.jsimport React from 'react';
import './Row.css';
And from here, we define the function
Row(), with a
return statement. And then export
Row.
src/components/Row/Row.jsimport React from 'react';
import './Row.css';
function Row(props) {
return (
);
}
export default Row;
Create
index.js, and add this line. Now whenever someone imports from the
Row directory, the first file to reference will be
index.js. And
index.js will export
default (which is
Row) from the
Row directory. This has the advantage of the caller not needing to know the implementation details of
Row (what component name to use, etc), but just needing to know that the component is from the
Row directory.
src/components/Row/index.jsexport { default } from './Row';
Now back to the
Row component. Remember we passed in two attributes? They can be accessed from
props. So declare
day and
currentDay accordingly.
src/components/Row/Row.jsfunction Row(props) {
let day = props.day;
let currentDay = props.currentDay;
return (
);
}
Then declare
content as an array. Its elements are objects, each with a single property for now. The property is
text, and it is the rest of the lyrics, according to
day. The element at index position 0 (the first one) is just a dummy.
src/components/Row/Row.jsfunction Row(props) {
let day = props.day;
let currentDay = props.currentDay;
let content = [
{ text: '', file: '' },
{ text: 'a partridge in a pear tree!' },
{ text: 'two turtle doves, and' },
{ text: 'three french hens' },
{ text: 'four calling birds' },
{ text: 'five gold rings' },
{ text: 'six swans a-swimming'},
{ text: 'seven geese a-laying' },
{ text: 'eight maids a-milking' },
{ text: 'nine ladies dancing' },
{ text: 'ten lords a-leaping' },
{ text: 'eleven pipers piping' },
{ text: 'twelve drummers drumming' }
];
return (
);
}
In the
return statement, we return a div.
src/components/Row/Row.jsreturn (
<div>
</div>
);
Here, we pass in the
text property of the relevant element in the
content array, pointed to by
day.
src/components/Row/Row.jsreturn (
<div>
<h1>{ content[day].text }</h1>
</div>
);
So, here are our rows, but it's not really what we want, is it?
Create the CSS file.
Row should take up 100% width and have, say, 200 pixels in height. In addition, set the
overflow property to
hidden. I added the
transition property just for fun. The
Hidden class is at zero transparency and height.
src/components/Row/Row.css.Row {
width: 100%;
height: 200px;
overflow: hidden;
transition: all 1s;
}
.Hidden {
opacity: 0;
height: 0px;
}
Now in here, set the class to be
Row, and if
currentDay is less than
day, add
Hidden. That means if whatever day is currently shown, only the
Row components with the
day attribute lesser than or equal to
currentDay, will be shown. In plain English, if you are at Day Four, you only want to see rows One to Four.
src/components/Row/Row.jsreturn (
<div
className={ 'Row' + (currentDay >= day ? '' : ' Hidden') }
>
<h1>{ content[day].text }</h1>
</div>
);
Does it work? Try clicking the up and down buttons. The appropriate rows should appear and disappear. There's a whole lot of white space because we set the height to 200 pixels, but that's deliberate. We're making space for the images.
Next
We'll put in the images, and go through some rudimentary automated tests!