Subscribe to my email list now at http://jauyeung.net/subscribe/
Follow me on Twitter at https://twitter.com/AuMayeung
Many more articles at https://medium.com/@hohanga
For many applications, recording dates is an important feature. Having a calendar is often a handy feature to have. Fortunately, many developers have made calendar components that other developers can easily add to their apps.
React has many calendar widgets that we can add to our apps. One of them is React Big Calendar. It has a lot of features. It has a month, week, and day calendar. Also, you can navigate easily to today or any other days with back and next buttons. You can also drag over a date range in the calendar to select the date range. With that, you can do any manipulation you want with the dates.
In this article, we will make a simple calendar app where users can drag over a date range and add a calendar entry. Users can also click on an existing calendar entry and edit the entry. Existing entries can also be deleted. The form for adding and editing the calendar entry will have date and time pickers to select the date and time.
We will save the data on the back end in a JSON file.
We will use React to build our app. To start, we run:
npx create-react-app calendar-app
to create the project.
Next we have to install a few packages. We will use Axios for HTTP requests to our back end, Bootstrap for styling, MobX for simple state management, React Big Calendar for our calendar component, React Datepicker for the date and time picker in our form , and React Router for routing.
To install them, we run:
npm i axios bootstrap mobx mobx-react moment react-big-calendar react-bootstrap react-datepicker react-router-dom
With all the packages installed, we can start writing the code. First, we replace the existing code in App.js
with:
import React from "react";
import { Router, Route } from "react-router-dom";
import HomePage from "./HomePage";
import { createBrowserHistory as createHistory } from "history";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import "./App.css";
import "react-big-calendar/lib/css/react-big-calendar.css";
import "react-datepicker/dist/react-datepicker.css";
const history = createHistory();
function App({ calendarStore }) {
return (
<div>
<Router history={history}>
<Navbar bg="primary" expand="lg" variant="dark">
<Navbar.Brand href="#home">Calendar App</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="mr-auto">
<Nav.Link href="/">Home</Nav.Link>
</Nav>
</Navbar.Collapse>
</Navbar>
<Route
path="/"
exact
component={props => (
<HomePage {...props} calendarStore={calendarStore} />
)}
/>
</Router>
</div>
);
}
export default App;
We add the React Bootstrap top bar in here with a link to the home page. Also, we add the route for the home page in here with the MobX calendarStore
passed in.
Also, we import the styles for the date picker and calendar here so that we can use them throughout the app.
Next in App.css
, replace the existing code with:
.page {
padding: 20px;
}
.form-control.react-datepicker-ignore-onclickoutside,
.react-datepicker-wrapper {
width: 465px !important;
}
.react-datepicker__current-month,
.react-datepicker-time__header,
.react-datepicker-year-header,
.react-datepicker__day-name,
.react-datepicker__day,
[class^="react-datepicker__day--*"],
.react-datepicker__time-list-item {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
"Droid Sans", "Helvetica Neue", sans-serif;
}
to add some padding to our page, change the width of the datepicker input and change the font of the datepicker.
Next create a file called CalendarForm.js
in the src
folder and add:
import React from "react";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import DatePicker from "react-datepicker";
import Button from "react-bootstrap/Button";
import {
addCalendar,
editCalendar,
getCalendar,
deleteCalendar
} from "./requests";
import { observer } from "mobx-react";
const buttonStyle = { marginRight: 10 };
function CalendarForm({ calendarStore, calendarEvent, onCancel, edit }) {
const [start, setStart] = React.useState(null);
const [end, setEnd] = React.useState(null);
const [title, setTitle] = React.useState("");
const [id, setId] = React.useState(null);
React.useEffect(() => {
setTitle(calendarEvent.title);
setStart(calendarEvent.start);
setEnd(calendarEvent.end);
setId(calendarEvent.id);
}, [
calendarEvent.title,
calendarEvent.start,
calendarEvent.end,
calendarEvent.id
]);
const handleSubmit = async ev => {
ev.preventDefault();
if (!title || !start || !end) {
return;
}
if (+start > +end) {
alert("Start date must be earlier than end date");
return;
}
const data = { id, title, start, end };
if (!edit) {
await addCalendar(data);
} else {
await editCalendar(data);
}
const response = await getCalendar();
const evs = response.data.map(d => {
return {
...d,
start: new Date(d.start),
end: new Date(d.end)
};
});
calendarStore.setCalendarEvents(evs);
onCancel();
};
const handleStartChange = date => setStart(date);
const handleEndChange = date => setEnd(date);
const handleTitleChange = ev => setTitle(ev.target.value);
const deleteCalendarEvent = async () => {
await deleteCalendar(calendarEvent.id);
const response = await getCalendar();
const evs = response.data.map(d => {
return {
...d,
start: new Date(d.start),
end: new Date(d.end)
};
});
calendarStore.setCalendarEvents(evs);
onCancel();
};
return (
<Form noValidate onSubmit={handleSubmit}>
<Form.Row>
<Form.Group as={Col} md="12" controlId="title">
<Form.Label>Title</Form.Label>
<Form.Control
type="text"
name="title"
placeholder="Title"
value={title || ""}
onChange={handleTitleChange}
isInvalid={!title}
/>
<Form.Control.Feedback type="invalid">{!title}</Form.Control.Feedback>
</Form.Group>
</Form.Row>
<Form.Row>
<Form.Group as={Col} md="12" controlId="start">
<Form.Label>Start</Form.Label>
<br />
<DatePicker
showTimeSelect
className="form-control"
selected={start}
onChange={handleStartChange}
/>
</Form.Group>
</Form.Row>
<Form.Row>
<Form.Group as={Col} md="12" controlId="end">
<Form.Label>End</Form.Label>
<br />
<DatePicker
showTimeSelect
className="form-control"
selected={end}
onChange={handleEndChange}
/>
</Form.Group>
</Form.Row>
<Button type="submit" style={buttonStyle}>
Save
</Button>
<Button type="button" style={buttonStyle} onClick={deleteCalendarEvent}>
Delete
</Button>
<Button type="button" onClick={onCancel}>
Cancel
</Button>
</Form>
);
}
export default observer(CalendarForm);
This is the form for adding and editing the calendar entries. We add the React Bootstrap form in here by adding the Form
component. The Form.Control
is also from the same library. We use it for the title
text input.
The other 2 fields are the start and end dates. We use React Datepicker in here to let use select the start and end dates of a calendar entry. In addition, we enable the time picker to let users pick the time.
There are change handlers in each field to update the values in the state so users can see what they entered and let them submit the data later. The change handlers are handleStartChange
, handleEndChange
and handleTitleChange
. We set the states with the setter functions generated by the useState
hooks.
We use the useEffect
callback to set the fields in the calendarEvent
prop to the states. We pass all the fields we want to set to the array in the second argument of the useEffect
function so that the states will be updated whenever the latest value of the calendarEvent
prop is passed in.
In the handleSubmit
function, which is called when the form Save button is clicked. we have to call ev.preventDefault
so that we can use Ajax to submit our form data.
If data validation passes, then we submit the data and get the latest and store them in our calendarStore
MobX store.
We wrap observer
outside the CalendarForm
component so that we always get the latest values from calendarStore
.
Next we create our home page. Create a HomePage.js
file in the src
folder and add:
import React from "react";
import { Calendar, momentLocalizer } from "react-big-calendar";
import moment from "moment";
import Modal from "react-bootstrap/Modal";
import CalendarForm from "./CalendarForm";
import { observer } from "mobx-react";
import { getCalendar } from "./requests";
const localizer = momentLocalizer(moment);
function HomePage({ calendarStore }) {
const [showAddModal, setShowAddModal] = React.useState(false);
const [showEditModal, setShowEditModal] = React.useState(false);
const [calendarEvent, setCalendarEvent] = React.useState({});
const [initialized, setInitialized] = React.useState(false);
const hideModals = () => {
setShowAddModal(false);
setShowEditModal(false);
};
const getCalendarEvents = async () => {
const response = await getCalendar();
const evs = response.data.map(d => {
return {
...d,
start: new Date(d.start),
end: new Date(d.end)
};
});
calendarStore.setCalendarEvents(evs);
setInitialized(true);
};
const handleSelect = (event, e) => {
const { start, end } = event;
const data = { title: "", start, end, allDay: false };
setShowAddModal(true);
setShowEditModal(false);
setCalendarEvent(data);
};
const handleSelectEvent = (event, e) => {
setShowAddModal(false);
setShowEditModal(true);
let { id, title, start, end, allDay } = event;
start = new Date(start);
end = new Date(end);
const data = { id, title, start, end, allDay };
setCalendarEvent(data);
};
React.useEffect(() => {
if (!initialized) {
getCalendarEvents();
}
});
return (
<div className="page">
<Modal show={showAddModal} onHide={hideModals}>
<Modal.Header closeButton>
<Modal.Title>Add Calendar Event</Modal.Title>
</Modal.Header>
<Modal.Body>
<CalendarForm
calendarStore={calendarStore}
calendarEvent={calendarEvent}
onCancel={hideModals.bind(this)}
edit={false}
/>
</Modal.Body>
</Modal>
<Modal show={showEditModal} onHide={hideModals}>
<Modal.Header closeButton>
<Modal.Title>Edit Calendar Event</Modal.Title>
</Modal.Header>
<Modal.Body>
<CalendarForm
calendarStore={calendarStore}
calendarEvent={calendarEvent}
onCancel={hideModals.bind(this)}
edit={true}
/>
</Modal.Body>
</Modal>
<Calendar
localizer={localizer}
events={calendarStore.calendarEvents}
startAccessor="start"
endAccessor="end"
selectable={true}
style={{ height: "70vh" }}
onSelectSlot={handleSelect}
onSelectEvent={handleSelectEvent}
/>
</div>
);
}
export default observer(HomePage);
We get the calendar entries and populate them in the calendar here. The entries are retrieved from back end and then saved into the store. In the useEffect
callback, we set get the items when the page loads. We only do it when initialized
is false so we won’t be reloading the data every time the page renders.
To open the modal for adding calendar entries, we set the onSelectSlot
prop with our handler so that we can call setShowAddModal
and setCalendarEvent
to set open the modal and set the dates before opening the add calendar event modal.
Similarly, we set the onSelectEvent
modal with the handleSelectEvent
handler function so that we open the edit modal and set the calendar event data of the existing entry.
Each Modal have the CalendarForm
component inside. We pass in the function for closing the modals into the form so that we can close them from the form. Also, we pass in the calendarStore
and calendarEvent
so that they can be manipulated in the CalendarForm
.
We wrap observer
outside the CalendarForm
component so that we always get the latest values from calendarStore
.
Next in index.js
, we replace the existing code with:
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { CalendarStore } from "./store";
const calendarStore = new CalendarStore();
ReactDOM.render(
<App calendarStore={calendarStore} />,
document.getElementById("root")
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
so that we can pass in the MobX calendarStore
into the root App
component.
Next create a requests.js
file in the src
folder and add:
const APIURL = "http://localhost:3000";
const axios = require("axios");
export const getCalendar = () => axios.get(`${APIURL}/calendar`);
export const addCalendar = data => axios.post(`${APIURL}/calendar`, data);
export const editCalendar = data =>
axios.put(`${APIURL}/calendar/${data.id}`, data);
export const deleteCalendar = id => axios.delete(`${APIURL}/calendar/${id}`);
These are the functions for making the HTTP calls to manipulate the calendar entries.
Next createstore.js
in the src
folder and add:
import { observable, action, decorate } from "mobx";
class CalendarStore {
calendarEvents = [];
setCalendarEvents(calendarEvents) {
this.calendarEvents = calendarEvents;
}
}
CalendarStore = decorate(CalendarStore, {
calendarEvents: observable,
setCalendarEvents: action
});
export { CalendarStore };
to save the items in the store for access by all of our components.
Next in index.html
, replace the existing code with:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Calendar App</title>
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
crossorigin="anonymous"
/>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
to add the Bootstrap CSS and rename the title.
Now all the hard work is done. All we have to do is use JSON Server NPM package located at https://github.com/typicode/json-server for our back end.
Install it by running:
npm i -g json-server
Then run it by running:
json-server --watch db.json
In db.json
, replace the existing content with:
{
"calendar": []
}
Next we run our app by running npm start
in our app’s project folder and when the program asks you to run in a different port, select yes.