Trailer
I decided to write this to help someone who may need to build a simple horizontal calendar picker for your mobile app. Today we are going to do so without having to use the cumbersome third party packages.
Why?
Well, suppose you're building an app that requires you(or the user) to select a date and other events that would be dependent on the date selected e.g. movie booking, event planner, travel/event planner, flight/bus booking or even an appointment manager(which I'll be launching in a few weeks)
Okay, that's enough foreplay; lets dive into the cool stuff. I'll add a repo at the end.
Let the fun begin
Create a new project.
Initialize a new expo app - We are using Expo because its 2023 and its freaking cool.
npx create-expo-app HzCalendar
cd HzCalendar
Now you can run any of the following commands depending on which device/simulator you're testing with. As an added bonus, you can run it on web too(expo is that good)
npm run android
npm run ios
npm run web
Install dependencies
We're only going to need only moment for working with dates. You may choose to use others like date-fns if you wish.
npm install --save moment
1. Calendar Component
We are going to have a calendar component that takes two props, an onSelectDate and selected. The selected
prop is so we change styles later. Lets create a components
folder, then create a Calendar
component.
- we are going to track 3 states: an empty
date
to start with, ascrollPosition
and acurrentMonth
. - later we will use the
scrollPosition
to track the current month.
First we are gonna use a for loop
to generate the next 10 days. We will then parse the days with moment and update the dates state.
At this point I assume you understand the basic javascript functions, and how react works when passing props and state data. I'll also add a few styles, so I hope you understand how styling works in React Native.
Then we will display the dates in a horizontal scrollview.
2. Date Component
We need to abstract the date component. Lets create a Date
component in the Components
folder
- This will be a simple
TouchableOpacity
button that will callonSelectDate
- Our
Date
component will take 3 props, adate
that will be displayed, theonSelectDate
function we passed to calendar, and theselected
prop too. - We'll use the
selected
prop to give the selected date different styling.
import { StyleSheet, Text, View, TouchableOpacity } from 'react-native'
import moment from 'moment'
const Date = ({ date, onSelectDate, selected }) => {
/**
* use moment to compare the date to today
* if today, show 'Today'
* if not today, show day of the week e.g 'Mon', 'Tue', 'Wed'
*/
const day = moment(date).format('YYYY-MM-DD') === moment().format('YYYY-MM-DD') ? 'Today' : moment(date).format('ddd')
// get the day number e.g 1, 2, 3, 4, 5, 6, 7
const dayNumber = moment(date).format('D')
// get the full date e.g 2021-01-01 - we'll use this to compare the date to the selected date
const fullDate = moment(date).format('YYYY-MM-DD')
return (
<TouchableOpacity
onPress={() => onSelectDate(fullDate)}
style={[styles.card, selected === fullDate && { backgroundColor: "#6146c6" }]}
>
<Text
style={[styles.big, selected === fullDate && { color: "#fff" }]}
>
{day}
</Text>
<View style={{ height: 10 }} />
<Text
style={[
styles.medium,
selected === fullDate && { color: "#fff", fontWeight: 'bold', fontSize: 24 },
]}
>
{dayNumber}
</Text>
</TouchableOpacity>
)
}
export default Date
const styles = StyleSheet.create({
card: {
backgroundColor: '#eee',
borderRadius: 10,
borderColor: '#ddd',
padding: 10,
marginVertical: 10,
alignItems: 'center',
height: 90,
width: 80,
marginHorizontal: 5,
},
big: {
fontWeight: 'bold',
fontSize: 20,
},
medium: {
fontSize: 16,
},
})
Then we call the Date
component in the Calendar
component and pass each date in the scrollview.
import { useState, useEffect } from 'react'
import { StyleSheet, Text, View, ScrollView } from 'react-native'
import moment from 'moment'
import Date from './Date'
const Calendar = ({ onSelectDate, selected }) => {
const [dates, setDates] = useState([])
const [scrollPosition, setScrollPosition] = useState(0)
const [currentMonth, setCurrentMonth] = useState()
// get the dates from today to 10 days from now, format them as strings and store them in state
const getDates = () => {
const _dates = []
for (let i = 0; i < 10; i++) {
const date = moment().add(i, 'days')
_dates.push(date)
}
setDates(_dates)
}
useEffect(() => {
getDates()
}, [])
return (
<>
<View style={styles.centered}>
<Text style={styles.title}>Current month</Text>
</View>
<View style={styles.dateSection}>
<View style={styles.scroll}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
>
{dates.map((date, index) => (
<Date
key={index}
date={date}
onSelectDate={onSelectDate}
selected={selected}
/>
))}
</ScrollView>
</View>
</View>
</>
)
}
export default Calendar
const styles = StyleSheet.create({
centered: {
justifyContent: 'center',
alignItems: 'center',
},
title: {
fontSize: 20,
fontWeight: 'bold',
},
dateSection: {
width: '100%',
padding: 20,
},
scroll: {
height: 150,
},
})
Now we update our App.js
file to display the Calendar
component.
- We'll add a
selectedDate
state that we shall pass toCalendar
props.
import { StatusBar } from 'expo-status-bar';
import { useState } from 'react';
import { StyleSheet, View } from 'react-native';
import Calendar from './components/Calendar';
export default function App() {
const [selectedDate, setSelectedDate] = useState(null);
return (
<View style={styles.container}>
<Calendar onSelectDate={setSelectedDate} selected={selectedDate} />
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
Now we have a working Horizontal scroll calendar component that looks pretty much like this.(if you select a date, you'll notice the styling changes - that's that Stylesheet magic we added in the Date
component)
Bonus - Current month
Now let's use the scrollPosition
to determine the month to be displayed above our dates.
- We'll update our
Calendar
component and track the scroll position on the horizontal scrollview and update thescrollPosition
state. - Then we'll use the
scrollPosition
andmoment
to generate the current month of the date in view.
PS: this
onScroll
functionality may not be the best implementation. However I challenge you to come up with a better one. I know there's one. If you do, don't hesitate to open a PR on the repo.
import { useState, useEffect } from 'react'
import { StyleSheet, Text, View, ScrollView } from 'react-native'
import moment from 'moment'
import Date from './Date'
const Calendar = ({ onSelectDate, selected }) => {
const [dates, setDates] = useState([])
const [scrollPosition, setScrollPosition] = useState(0)
const [currentMonth, setCurrentMonth] = useState()
// ... same as before
/**
* scrollPosition is the number of pixels the user has scrolled
* we divide it by 60 because each date is 80 pixels wide and we want to get the number of dates
* we add the number of dates to today to get the current month
* we format it as a string and set it as the currentMonth
*/
const getCurrentMonth = () => {
const month = moment(dates[0]).add(scrollPosition / 60, 'days').format('MMMM')
setCurrentMonth(month)
}
useEffect(() => {
getCurrentMonth()
}, [scrollPosition])
return (
<>
<View style={styles.centered}>
<Text style={styles.title}>{currentMonth}</Text>
</View>
<View style={styles.dateSection}>
<View style={styles.scroll}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
// onScroll is a native event that returns the number of pixels the user has scrolled
scrollEventThrottle={16}
onScroll={(e) => setScrollPosition(e.nativeEvent.contentOffset.x)}
>
{dates.map((date, index) => (
<Date
key={index}
date={date}
onSelectDate={onSelectDate}
selected={selected}
/>
))}
</ScrollView>
</View>
</View>
</>
)
}
export default Calendar
const styles = StyleSheet.create({
centered: {
justifyContent: 'center',
alignItems: 'center',
},
title: {
fontSize: 20,
fontWeight: 'bold',
},
dateSection: {
width: '100%',
padding: 20,
},
scroll: {
height: 150,
},
})
Now we can see the month and when you scroll to a new month, it should change the month displayed above.
Fun exercise...
Try using the Animated
API to center selected date or even make it feel like a wheel.
You can find all the code in this repo. PRs are welcome.
Top comments (0)