We celebrate Ada Lovelace Day next month!
Ada Lovelace Day is a day dedicated by computer geeks and mathematicians worldwide, to a woman who is commonly recognized as the mother of modern computing, Ada Lovelace. It falls on the second Tuesday of October, and while it's nice and easy to remember, sometimes we just want the exact date for any given year.
That's where today's web tutorial comes in! Using ReactJS, we'll generate the exact date for Ada Lovelace Day from any user input year.
We begin with a HTML/ReactJS layout. Stuff will generally be in a div with an id of
appContainer.
<!DOCTYPE html>
<html>
<head>
<title>Ada Lovelace Day</title>
<style>
</style>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6.26.0/babel.js"></script>
</head>
<body>
<div id="appContainer">
</div>
<script type="text/babel">
</script>
</body>
</html>
Now, in appContainer, we'll insert two more divs, with ids of
adaContainer and
genContainer, respectively.
<div id="appContainer">
<div id="adaContainer">
</div>
<div id="genContainer">
</div>
</div>
genContainer will hold the components, but
adaContainer will hold this lovely little image I found.
 |
| ada.png |
So let's start styling. Set all divs to have a
red outline. Then give
appContainer this height and width, and align it to the middle of the screen.
<style>
div {outline: 1px solid #FF0000;}
#appContainer
{
height: 500px;
width: 450px;
margin: 5% auto 0 auto;
}
</style>
OK so far? Great.
Now, make sure both
adaContainer and
genContainer have 100% width. They'll have different heights.
adaContainer will have a
deep grey bottom border while
genContainer will have a thicker
light grey top border.
<style>
div {outline: 1px solid #FF0000;}
#appContainer
{
height: 500px;
width: 450px;
margin: 5% auto 0 auto;
}
#adaContainer
{
height: 300px;
width: 100%;
border-bottom: 1px solid #444444;
}
#genContainer
{
border-top: 2px solid #CCCCCC;
height: 200px;
width: 100%;
}
</style>
The special effect isn't all that apparent due to the
red outline, but never mind for now.
Now use the image as
adaContainer's background. Set the
background-size property to
contain. The image should be aligned bottom.
#adaContainer
{
height: 300px;
width: 100%;
border-bottom: 1px solid #444444;
background: url(ada.png) center bottom no-repeat;
background-size: contain;
}
Oh, we're off to a great start!
Now for some serious business!
In the script tag, declare the class
AdaLovelaceDay with a constructor method,
constructor(). In it, call the
super() function using
props as an argument (so that we can use this to set state), then set state with two properties -
year (default 0) and
adaLovelaceDay (default empty string). The class should also have a
componentDidMount() method, because we want stuff to happen on page load. And of course, we'll have a
render() method which returns the component markup.
<script type="text/babel">
class AdaLovelaceDay extends React.Component
{
constructor(props)
{
super(props);
this.state =
{
"year": 0,
"adaLovelaceDay": ""
};
}
componentDidMount()
{
}
render()
{
return (
);
}
}
ReactDOM.render(<AdaLovelaceDay />, document.getElementById("genContainer"));
</script>
Bind the
showAdaLovelaceDayForYear() method to the constructor, then declare the method.
constructor(props)
{
super(props);
this.state =
{
"year": 0,
"adaLovelaceDay": ""
};
this.showAdaLovelaceDayForYear = this.showAdaLovelaceDayForYear.bind(this);
}
showAdaLovelaceDayForYear(renderYear)
{
}
componentDidMount()
{
}
What the
showAdaLovelaceDayForYear() method does is accept the parameter
renderYear, which is a four-digit number representing the year for which we want to find the second Tuesday of October. So we start by declaring a variable,
day, which we set to 1. And the variable
tuesdays, which we set to 0.
showAdaLovelaceDayForYear(renderYear)
{
var day = 1;
var tuesdays = 0;
}
Use a
While loop, set to continue only if
tuesdays is less than 2.
showAdaLovelaceDayForYear(renderYear)
{
var day = 1;
var tuesdays = 0;
while (tuesdays < 2)
{
}
}
Declare a variable,
october. Set it to a date using the
Date() function, and passing in the arguments
renderYear, 9 and
day. Since day is 1, you've basically set
october to the first day of October (we use 9 because the count starts at 0, so the 10th month, October, is represented by 9) in the year
renderYear.
showAdaLovelaceDayForYear(renderYear)
{
var day = 1;
var tuesdays = 0;
while (tuesdays < 2)
{
var october = new Date(renderYear, 9, day);
}
}
Use the
getDay() method on
october. If the result is 2, which means it's a Tuesday, increment
tuesdays.
showAdaLovelaceDayForYear(renderYear)
{
var day = 1;
var tuesdays = 0;
while (tuesdays < 2)
{
var october = new Date(renderYear, 9, day);
if (october.getDay() == 2) tuesdays++;
}
}
If
tuesdays is still less than 2 at this point, increment
day. So this continues until you reach the
second Tuesday of October, and by that time, day would be the... well, you get the idea.
showAdaLovelaceDayForYear(renderYear)
{
var day = 1;
var tuesdays = 0;
while (tuesdays < 2)
{
var october = new Date(renderYear, 9, day);
if (october.getDay() == 2) tuesdays++;
if (tuesdays < 2) day++;
}
}
The rest is fluff. Declare the variable
adaLovelaceDay and set it to the value of
day, then depending on what day of the month it is, append either "st", "nd" or "th" to it. It's not
absolutely necessary, but it's pretty, so let's do it!
showAdaLovelaceDayForYear(renderYear)
{
var day = 1;
var tuesdays = 0;
while (tuesdays < 2)
{
var october = new Date(renderYear, 9, day);
if (october.getDay() == 2) tuesdays++;
if (tuesdays < 2) day++;
}
var adaLovelaceDay = day;
if (day == 1 || day == 21 || day == 31)
{
adaLovelaceDay += "st";
}
else if (day == 2 || day == 12)
{
adaLovelaceDay += "nd";
}
else
{
adaLovelaceDay += "th";
}
}
Finally set
state. Use the
setState() method to modify the properties
year to
renderYear and
adaLovelaceDay to
adaLovelaceDay.
showAdaLovelaceDayForYear(renderYear)
{
var day = 1;
var tuesdays = 0;
while (tuesdays < 2)
{
var october = new Date(renderYear, 9, day);
if (october.getDay() == 2) tuesdays++;
if (tuesdays < 2) day++;
}
var adaLovelaceDay = day;
if (day == 1 || day == 21 || day == 31)
{
adaLovelaceDay += "st";
}
else if (day == 2 || day == 12)
{
adaLovelaceDay += "nd";
}
else
{
adaLovelaceDay += "th";
}
this.setState({"year": renderYear, "adaLovelaceDay" : adaLovelaceDay});
}
In the
componentDidMount() method, run the
showAdaLovelaceDayForYear() method, passing in the current year as an argument. So upon page load, the first thing this script does is get Ada Lovelace Day for the current year.
componentDidMount()
{
this.showAdaLovelaceDayForYear((new Date()).getFullYear());
}
That's all well and good, but...
Yeah, we're getting there. Since we've set the state and all, we'll need to display it. That's where
render() comes in! It will return this div, id
adaText, with the following template string.
render()
{
return (
<div className="adaText">
Ada Lovelace Day is on <b>{this.state.adaLovelaceDay} October</b>.
</div>
);
}
Style
adaText. Just do what looks nice for you.
#genContainer
{
border-top: 2px solid #CCCCCC;
height: 200px;
width: 100%;
}
.adaText
{
height: 1.5em;
width: 80%;
margin: 0 auto 0 auto;
padding-top: 1em;
font-size: 16px;
font-family: arial;
text-align: center;
}
And here we go! Ada Lovelace Day for 2019 is... 8th October!
Hold on, we're not done!
What's the point if we
only get to forecast the current year? This would be a lot more useful if we included controls in the interface to input other years. We'll make the numbers of each four-digit year mutable via the interface.
In the
render() method, declare an empty array,
digitGens. Use a
For loop to run 4 times.
render()
{
var digitGens = [];
for (var i = 0; i <= 3; i++)
{
}
return (
<div className="adaText">
Ada Lovelace Day is on <b>{this.state.adaLovelaceDay} October</b>.
</div>
);
}
On every iteration, push a new element into the
digitGens array. It will be an object containing the properties
number and
id.
number is set to each digit of the four digit year stored in the state. We'll first need to convert it to a string, then extract the character at the appropriate position.
id is set to the current value of
i in the
For loop.
render()
{
var digitGens = [];
for (var i = 0; i <= 3; i++)
{
digitGens.push
(
{
"number": this.state.year.toString().charAt(i),
"id": i
}
);
}
return (
<div className="adaText">
Ada Lovelace Day is on <b>{this.state.adaLovelaceDay} October</b>.
</div>
);
}
While we're here, give each element in digitGens a
inc() and
dec() method. That method will call this component's
changeYear() method, passing in the argument
true for incrementing, and
false for decrementing. (I know, we haven't created the
changeYear() method yet. Soon!) Also, since this would be out of scope inside that
For loop, we first declare the variable
functionParent and set it to the value of
this, and then use
functionParent in place of
this in the
For loop.
render()
{
var digitGens = [];
var functionParent = this;
for (var i = 0; i <= 3; i++)
{
digitGens.push
(
{
"number": this.state.year.toString().charAt(i),
"id": i,
"inc": function() {functionParent.changeYear(true);},
"dec": function() {functionParent.changeYear(false);}
}
);
}
return (
<div className="adaText">
Ada Lovelace Day is on <b>{this.state.adaLovelaceDay} October</b>.
</div>
);
}
Now, declare the variable
digitGen (note the singular) and set it to the value of a
map() method run on the
digitGens array.
render()
{
var digitGens = [];
var functionParent = this;
for (var i = 0; i <= 3; i++)
{
digitGens.push
(
{
"number": this.state.year.toString().charAt(i),
"id": i,
"inc": function() {functionParent.changeYear(true);},
"dec": function() {functionParent.changeYear(false);}
}
);
}
var digitGen = digitGens.map(
(item, key) =>
{
return (
);
}
);
return (
<div className="adaText">
Ada Lovelace Day is on <b>{this.state.adaLovelaceDay} October</b>.
</div>
);
}
We're going to use the
digitGens array to generate a series of four number-scrollers mini-components. So, what happens is that a div with class of
yearGen (note that we use
className in order to avoid confusing the Babel compiler) contains two divs. The first div is styled using
genDigit and contains the number.
var digitGen = digitGens.map(
(item, key) =>
{
return (
<div className="yearGen">
<div className="genDigit">
{item.number}
</div>
<div>
</div>
</div>
);
}
);
The second div is styled using
buttons and has an id of
digit plus the current element's
id property. It contains two buttons. The buttons have
data attributes, namely,
i. Set that value to the
id property of the current element. The
onClick event handlers in each button will call the current item's
inc() and
dec() methods respectively. The button's text are HTML symbols ▲ and ▼.
var digitGen = digitGens.map(
(item, key) =>
{
return (
<div className="yearGen">
<div className="genDigit">
{item.number}
</div>
<div id="digit{item.id}" className="buttons">
<button data-i={item.id} onClick={item.inc}>▲</button>
<button data-i={item.id} onClick={item.dec}>▼</button>
</div>
</div>
);
}
);
Next, we enclose the returned div in an outer div, and style the outer div using
genContent. Put the entire
digitGen component in it, just before the div.
return (
<div className="genContent">
{digitGen}
<div className="adaText">
Ada Lovelace Day is on <b>{this.state.adaLovelaceDay} October</b>.
</div>
</div>
);
Don't forget to create the
changeYear() method! Leave it blank, for now.
showAdaLovelaceDayForYear(renderYear)
{
var day = 1;
var tuesdays = 0;
while (tuesdays < 2)
{
var october = new Date(renderYear, 9, day);
if (october.getDay() == 2) tuesdays++;
if (tuesdays < 2) day++;
}
var adaLovelaceDay = day;
if (day == 1 || day == 21 || day == 31)
{
adaLovelaceDay += "st";
}
else if (day == 2 || day == 12)
{
adaLovelaceDay += "nd";
}
else
{
adaLovelaceDay += "th";
}
this.setState({"year": renderYear, "adaLovelaceDay" : adaLovelaceDay});
}
changeYear(inc)
{
}
componentDidMount()
{
this.showAdaLovelaceDayForYear((new Date()).getFullYear());
}
Yikes. This looks like a horrible, horrible mess. Let's clean this up.
First, ensure that
genContent occupies the full height and width of its parent.
#genContainer
{
border-top: 2px solid #CCCCCC;
height: 200px;
width: 100%;
}
.genContent
{
height: 100%;
width: 100%;
}
.adaText
{
height: 1.5em;
width: 80%;
margin: 0 auto 0 auto;
padding-top: 1em;
font-size: 16px;
font-family: arial;
text-align: center;
}
Each div styled using
yearGen should be 150 pixels tall. They should take up 25% of their parent's width, because there are four of these suckers, and four of 25% makes up... 100%! Float them all left.
genDigit basically determines how each number looks. Use personal taste here. But do center the text using the
text-align property.
.adaText
{
height: 1.5em;
width: 80%;
margin: 0 auto 0 auto;
padding-top: 1em;
font-size: 16px;
font-family: arial;
text-align: center;
}
.yearGen
{
height: 150px;
width: 25%;
float: left;
}
.genDigit
{
width: 100%;
height: 100px;
font-size: 80px;
font-family: verdana;
font-weight: bold;
text-align: center;
}
Much better.
Now for the buttons. The container,
buttons, takes up full width and text content is centered. The button tags are styled to be small squares with rounded corners, and a little margin.
.genDigit
{
width: 100%;
height: 100px;
font-size: 80px;
font-family: verdana;
font-weight: bold;
text-align: center;
}
.buttons
{
width: 100%;
text-align: center;
}
button
{
margin: 0.25em;
width: 2.5em;
height: 2.5em;
text-align: center;
border-radius: 20%;
}
Excellent!
To prevent the text from overlapping, set the
clear property to
both.
.adaText
{
clear: both;
height: 1.5em;
width: 80%;
margin: 0 auto 0 auto;
padding-top: 1em;
font-size: 16px;
font-family: arial;
text-align: center;
}
Yep...
And hey, while you're at it, remove the
red lines.
div {outline: 0px solid #FF0000;}
Great!
Now let's deal with the
changeYear() method. Declare
yearString and set it to the
state object's
year property, converted to a string. Then declare
digits, which will be an array of
yearString's numbers.
changeYear(inc)
{
var yearString = this.state.year.toString();
var digits = yearString.split("");
}
Finally, declare
index, which will be the element's dataset's custom attribute
i. Remember setting that while creating the sub-elements? This will tell you which number's buttons are being clicked.
changeYear(inc)
{
var yearString = this.state.year.toString();
var digits = yearString.split("");
var index = parseInt(event.target.dataset["i"]);
}
Now, use the
map() method and the
parseInt() function to convert all elements of the
digits array back to numbers.
changeYear(inc)
{
var yearString = this.state.year.toString();
var digits = yearString.split("");
var index = parseInt(event.target.dataset["i"]);
digits = digits.map(function(x) {return parseInt(x)});
}
inc is true if incrementing, and
false if decrementing. So if
inc is true, check if the digit to be incremented is less than 9, and increment the number if so. This is to set a limit for the number... because in a year string you obviously can't go above 9.
changeYear(inc)
{
var yearString = this.state.year.toString();
var digits = yearString.split("");
var index = parseInt(event.target.dataset["i"]);
digits = digits.map(function(x) {return parseInt(x)});
if (inc)
{
if (digits[index] < 9) digits[index]++;
}
else
{
}
}
If it's decrementing, more checks are needed. We don't want the first digit to be a 0 ever, so first check if it's the first digit (i.e.
index is 0), then decrement only if the number is greater than 1. That means you get a 1, minimum.
changeYear(inc)
{
var yearString = this.state.year.toString();
var digits = yearString.split("");
var index = parseInt(event.target.dataset["i"]);
digits = digits.map(function(x) {return parseInt(x)});
if (inc)
{
if (digits[index] < 9) digits[index]++;
}
else
{
if (index == 0)
{
if (digits[index] > 1)
{
digits[index]--;
}
}
else
{
}
}
}
If it's any number other than the first, check if the number is more than 0 before decrementing. That means all other numbers can be decremented all the way to 0.
changeYear(inc)
{
var yearString = this.state.year.toString();
var digits = yearString.split("");
var index = parseInt(event.target.dataset["i"]);
digits = digits.map(function(x) {return parseInt(x)});
if (inc)
{
if (digits[index] < 9) digits[index]++;
}
else
{
if (index == 0)
{
if (digits[index] > 1)
{
digits[index]--;
}
}
else
{
if (digits[index] > 0)
{
digits[index]--;
}
}
}
}
Next, set
yearString to be the joined string of all the numbers in the
digits array. Then call the
showAdaLovelaceDayForYear() method, passing in
yearString converted back to an integer, as an argument. Remember that
showAdaLovelaceDayForYear() changes the state of the component, updating the
year and
adaLovelaceDay properties?
changeYear(inc)
{
var yearString = this.state.year.toString();
var digits = yearString.split("");
var index = parseInt(event.target.dataset["i"]);
digits = digits.map(function(x) {return parseInt(x)});
if (inc)
{
if (digits[index] < 9) digits[index]++;
}
else
{
if (index == 0)
{
if (digits[index] > 1)
{
digits[index]--;
}
}
else
{
if (digits[index] > 0)
{
digits[index]--;
}
}
}
yearString = digits.join("");
this.showAdaLovelaceDayForYear(parseInt(yearString));
}
Well, now click on the buttons, increment and decrement as you wish, and the generated Ada Lovelace Day should change! That's the power of ReactJS binding, right there. Try changing the year to 2018. Does Ada Lovelace Day change to 9th October?
Wishing you all a happy and productive Ada Lovelace Day!
Decades after her death, look how far we've come. It's nice to remember the amazing woman who got us started down the road towards computing as we know it today.
All my Love(lace),
T___T