Building a Calendar using React JS, LESS CSS and Font Awesome
Today, I'm going to talk about how to create a calendar control using React JS, LESS CSS, Font Awesome and Moment JS. I'm going to assume you're at least halfway familiar with these tools, but if you're not, I suggest you take a look using the links above. The calendar itself will be a React JS class that allows the user to select a date, which is set on a controller's property. I've styled the calendar, and I'll include that style in this tutorial, but you can obviously feel free to tweak how it looks to your heart's content. If you're the kind of reader that prefers to view a completed product and just check the source yourself, I've put together a demo page which shows off the calendar. You can see it here. This calendar control shouldn't be taken as a copy and paste solution to a problem you're having; it's meant as a guide to React itself as opposed to providing a production-ready calendar.
Note: The CSS shown here might not match up with exactly what you see on the demo page because I want the demo to look at least halfway decent with some pretty styling. For the most part, though, I'm going to try to keep the CSS on the tutorial as bare as possible.
I've split the calendar control up into a few different classes: Calendar, DayNames and Week. The Calendar class contains the other two. DayNames is a simple header for the calendar which displays (unsurprisingly) week names, while the Week class is responsible for rendering each individual week.
Calendar
var Calendar = React.createClass({
getInitialState: function() {
return {
month: this.props.selected.clone()
};
},
previous: function() {
var month = this.state.month;
month.add(-1, "M");
this.setState({ month: month });
},
next: function() {
var month = this.state.month;
month.add(1, "M");
this.setState({ month: month });
},
select: function(day) {
this.props.selected = day.date;
this.forceUpdate();
},
render: function() {
return <div>
<div className="header">
<i className="fa fa-angle-left" onClick={this.previous}></i>
{this.renderMonthLabel()}
<i className="fa fa-angle-right" onClick={this.next}></i>
</div>
<DayNames />
{this.renderWeeks()}
</div>;
},
renderWeeks: function() {
var weeks = [],
done = false,
date = this.state.month.clone().startOf("month").add("w" -1).day("Sunday"),
monthIndex = date.month(),
count = 0;
while (!done) {
weeks.push(<Week key={date.toString()} date={date.clone()} month={this.state.month} select= {this.select} selected={this.props.selected} />);
date.add(1, "w");
done = count++ > 2 && monthIndex !== date.month();
monthIndex = date.month();
}
return weeks;
},
renderMonthLabel: function() {
return <span>{this.state.month.format("MMMM, YYYY")}</span>;
}
});
When rendering this class, the only prop it requires is the initially set date; everything else is derived from that. In our getInitialState
method, we're setting the current month to be the same as the selected date. Month and selected date are two different variables to account for the user moving from February, for example, to March without selecting a new date. Outside of rendering, our Calendar class has three methods: previous
, next
and select
. The previous
and next
methods are responsible for moving the user's calendar view from the current month to the previous and next month, respectively, while the select method takes care of marking a new date as the selected date. We need to perform a forceUpdate
here to force a render of the view, as the Calendar class isn't watching the selected property.
The render
method writes out some pretty standard HTML. Here, we're using Font Awesome to render some previous and next arrows, which wrap the current month's label. Then, we render the DayNames
class, followed by the list of Week
classes. There's some logic in there to allow for showing trailing and leading days, too.
DayNames
The DayNames
class is the simplest of the three. It's sole responsibility is to render the names of the days near the top of the calendar.
var DayNames = React.createClass({
render: function() {
return <div className="week names">
<span className="day">Sun</span>
<span className="day">Mon</span>
<span className="day">Tue</span>
<span className="day">Wed</span>
<span className="day">Thu</span>
<span className="day">Fri</span>
<span className="day">Sat</span>
</div>;
}
});
There's not really much to say about this. Note the use of className
instead of class
; it's an easy mistake to make when first starting out with React.
Week
var Week = React.createClass({
render: function() {
var days = [],
date = this.props.date,
month = this.props.month;
for (var i = 0; i < 7; i++) {
var day = {
name: date.format("dd").substring(0, 1),
number: date.date(),
isCurrentMonth: date.month() === month.month(),
isToday: date.isSame(new Date(), "day"),
date: date
};
days.push(<span key={day.date.toString()} className={"day" + (day.isToday ? " today" : "") + (day.isCurrentMonth ? "" : " different-month") + (day.date.isSame(this.props.selected) ? " selected" : "")} onClick={this.props.select.bind(null, day)}>{day.number}</span>);
date = date.clone();
date.add(1, "d");
}
return <div className="week" key={days[0].toString()}>
{days}
</div>
}
});
The Week
class takes four props: date, month, select and selected. The first indicates the start of the week, while month gives us the current month for determining if we need trailing or leading days. The third and fourth props deal with selecting a date. The render method renders seven days, while setting various CSS classes on the day, one each for selected, today or trailing/leading days.
CSS
.vertical-centre (@height) {
height:@height;
line-height:@height !important;
display:inline-block;
vertical-align:middle;
}
.border-box {
box-sizing:border-box;
-moz-box-sizing:border-box;
}
@border-colour:#CCC;
calendar {
float:left;
display:block;
.border-box;
background:white;
width:300px;
border:solid 1px @border-colour;
margin-bottom:10px;
@secondary-colour:#2875C7;
@spacing:10px;
@icon-width:40px;
@header-height:40px;
>div.header {
float:left;
width:100%;
background:@secondary-colour;
height:@header-height;
color:white;
>* {
.vertical-centre(@header-height);
}
>i {
float:left;
width:@icon-width;
font-size:1.125em;
font-weight:bold;
position:relative;
.border-box;
padding:0 @spacing;
cursor:pointer;
}
>i.fa-angle-left {
text-align:left;
}
>i.fa-angle-right {
text-align:right;
margin-left:@icon-width*-1;
}
>span {
float:left;
width:100%;
font-weight:bold;
text-transform:uppercase;
.border-box;
padding-left:@icon-width+@spacing;
margin-left:@icon-width*-1;
text-align:center;
padding-right:@icon-width;
color:inherit;
}
}
>div.week {
float:left;
width:100%;
border-top:solid 1px @border-colour;
&:first-child {
border-top:none;
}
>span.day {
float:left;
width:100%/7;
.border-box;
border-left:solid 1px @border-colour;
font-size:0.75em;
text-align:center;
.vertical-centre(30px);
background:white;
cursor:pointer;
color:black;
&:first-child {
border-left:none;
}
&.today {
background:#E3F2FF;
}
&.different-month {
color:#C0C0C0;
}
&.selected {
background:@secondary-colour;
color:white;
}
}
&.names>span {
color:@secondary-colour;
font-weight:bold;
}
}
}
The first few lines define a few helpful mixins using LESS CSS. The first allows us to centre elements vertically, presuming we have the height of the wrapping element. The second sets the box-sizing property, which Firefox doesn't support without the -moz- prefix.
Next, we're setting some variables related to styling and spacing that are used more than once. If you want to change the colour, for example, the secondary-colour variable is what you need to update. The rest is just standard styling to make the calendar look somewhat pretty and spacing stuff to make sure it's aligned properly.
Conclusion
And that's it! As I mentioned above, I've put together a demo page which shows off the calendar in its final form. You can see it here. Thanks for reading!
this code is not working, i get an error in initializing month with a clone()
Hey, do you have the code in GitHub? I don’t get compilation in my app, can you help me? Tks.
Why there’s createClass between createClass, why not just create a normal function? I mean in line 116.