DEV Community

Cover image for Horizontal Calendar - a simple date picker for React Native
Tony Kharioki
Tony Kharioki

Posted on

Horizontal Calendar - a simple date picker for React Native

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

First


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


Enter fullscreen mode Exit fullscreen mode

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, a scrollPosition and a currentMonth.
  • 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 call onSelectDate
  • Our Date component will take 3 props, a date that will be displayed, the onSelectDate function we passed to calendar, and the selected 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,
  },
})


Enter fullscreen mode Exit fullscreen mode

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,
  },
})


Enter fullscreen mode Exit fullscreen mode

Now we update our App.js file to display the Calendar component.

  • We'll add a selectedDate state that we shall pass to Calendar 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',
  },
});


Enter fullscreen mode Exit fullscreen mode

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)

almost there


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 the scrollPosition state.
  • Then we'll use the scrollPosition and moment 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,
  },
})


Enter fullscreen mode Exit fullscreen mode

Now we can see the month and when you scroll to a new month, it should change the month displayed above.

finally


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)