Sunday, 20 December 2020

Web Tutorial: The Twelve Days of Christmas (Part 1/2)

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.js
import React from 'react';
import './App.css';


Add the App() function.

src/App.js
import 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.js
import 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.js
import 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.js
import 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.js
import 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.js
import React from 'react';
import './App.css';

function App() {
    const dayControls = (
        <div className="dayControl">
            <div className="dayText">

            </div>

            <div className="dayButtons">
                <div className="dayButton">&#9650;</div>
                <div className="dayButton">&#9660;</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.js
const dayControls = (
    <div className="dayControl">
        <div className="dayText">
            { currentDay }
        </div>

        <div className="dayButtons">
            <div className="dayButton">&#9650;</div>
            <div className="dayButton">&#9660;</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.js
import 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.js
import 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">&#9650;</div>
                <div className="dayButton">&#9660;</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.js
const [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 }>&#9650;</div>
            <div className="dayButton" onClick={ BtnClickDown }>&#9660;</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.js
const 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.js
const dayControls = (
    <div className="dayControl">
        <div className="dayText">
            { currentDay }
            <sup>
                { daySuffix[currentDay - 1] }
            </sup>

        </div>

        <div className="dayButtons">
            <div className="dayButton" onClick={ BtnClickUp }>&#9650;</div>
            <div className="dayButton" onClick={ BtnClickDown }>&#9660;</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.js
import 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.js
return (
    <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.js
import React from 'react';


Also import Row.css. We will create that file soon.

src/components/Row/Row.js
import 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.js
import 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.js
export { 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.js
function 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.js
function 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.js
return (
    <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.js
return (
    <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.js
return (
    <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!

No comments:

Post a Comment