Membuat Komponen Kalender untuk ReactJS (2)

Lanjutan dari Bagian 1.

Tanggal yang Dipilih di Awal

Prop selectedDates bisa dipake untuk nentuin tanggal yang udah terpilih di awal secara individual, nggak berurutan atau berurutan (range).

Kita buat dulu styling-nya.

&.sunday, &.specials {
  /*...kode yang lain*/
}

/* untuk tanggal yang terpilih */
&.selected {
  button {
    background: lightseagreen;
    color: white;
  }
}

Terus di blok yang nge-render tabel, kita tambah kode (#1) untuk tambah kelas selected kalo day.selected=true.

<tbody>              
  {
    cal.weeks.map( (week, i) => {                
      return <tr key={i} className="week">
        {
          week.map( (day, j) => {                  

            console.log(day)

            let cname = 'day';

            if(day.today) {
              cname += ' today'
            }               

            if(j === 0) {
              cname += ' sunday'
            }

            if(props.specials?.length > 0) {
              const sameDate = props.specials.find( sp => isEqual(sp.date, day.date));
              if(sameDate) {
                cname += ' specials'
              }
            }
            
            // #1
            if(day.selected){
              cname += ' selected'
            }

            let disabled = false;
            if(props.disabledDates) {
              const sameDate = props.disabledDates.find( dt => isEqual(dt, day.date));
              
              disabled = !!sameDate;
            }

            if(props.disablePreviousDates && !day.today) {
              disabled = isBefore(day.date, today)
            }

            return <td key={j} className={cname}>{
              day ? <button disabled={disabled}>{day.date.getDate()}</button> : null
            }</td>
          })
        }
      </tr>    
    })
  }
</tbody>

Tanggal Individual

<MyReactCalendar              
  selectedDates={[
    new Date('2021, 11, 22'),
    new Date('2021, 11, 29'),
    new Date('2021, 12, 11'),
    new Date('2021, 12, 2')
  ]} 
  />

Range Tanggal

Untuk range tanggal sedikit lebih rumit, tapi kita bisa pake beberapa fungsi yang disediain date-fns. Kita buat dulu fungsi getSelectedDatesFromArrayOrRange(). Kalo argumen source berbentuk array, langsung balikin (#2). Tapi kalo argumennya berbentuk objek { from: ..., to:...}, dikonversi dulu jadi array.

Di #3 kita hitung ada berapa hari antara from sampai to pake fungsi differenceInCalendarDays() dari date-fns, jumlahnya kita tambah 1 karena fungsi itu menghitung selisih hari, jadi tanggal from nggak ikut dihitung.

Di #5, kita buat array kosong yang jumlah elemennya sebanyak jumlah hari. Kita pake untuk membuat tanggal (  source + indeks ).

// #1
const getSelectedDatesFromArrayOrRange = source => {
  
  // #2
  if(Array.isArray(source) ){
    return source;
  }

  // #3 hitung jumlah total hari yang harus dipilih  
  let dayCount = differenceInCalendarDays(source.to, source.from) + 1;

  // konversi range.from & range.to ke array tanggal
  // #5
  const arr = [...Array(dayCount)].map( (d,i) => {
    return addDays(source.from,i)
  })

  return arr;

}

function MyReactCalendar(props) {

  const monthNames = MONTHS[props.lang];
  const dayNames = DAYS[props.lang];

  const {calendars, getDateProps, getBackProps, getForwardProps} = useDayzed({
    monthsToDisplay: props.numberOfMonths,
    minDate: props.minDate,
    maxDate: props.maxDate,
    date: props.initialDate,
    // #6
    selected: getSelectedDatesFromArrayOrRange(props.selectedDates)
  })

// ... kode lainnya

Array balikan dari fungsi getSelectedDatesFromArrayOrRange() kita pake untuk inisialisasi useDayzed() (#6).

Ubah <MyReactCalendar> di  index.jsx , ganti selectedDates pake objek range.

<MyReactCalendar              
  selectedDates={{
    from: new Date('2021, 11, 10'),
    to: new Date('2021, 11, 21')
  }} 
  />

Callback

Sampai di sini kita baru membuat kalender yang sifatnya hanya nampilin data, kita belum punya kode untuk milih tanggal, menghapus pilihan, merespon pergantian bulan atau perubahan tanggal yang dipilih.

Ubah Pilihan Tanggal

Kita buat state selection untuk simpan tanggal-tanggal yang dipilih. Argumen untuk useDayzed() kita ubah untuk baca state itu (#2).

// #1
const [selection, setSelection] = useState([]);

const {calendars, getDateProps, getBackProps, getForwardProps} = useDayzed({
  monthsToDisplay: props.numberOfMonths,
  minDate: props.minDate,
  maxDate: props.maxDate,
  date: props.initialDate,
  // #2
  selected: selection
})

// #3
useEffect(()=>{
  setSelection(getSelectedDatesFromArrayOrRange(props.selectedDates))
},[props.selectedDates]);

Di #3, setiap kali props.selectedDates berubah, kita reset state selection dengan array baru.

onSelectionChange

Buat dulu callback-nya di App.

<MyReactCalendar              
  selectedDates={{
    from: new Date('2021, 11, 10'),
    to: new Date('2021, 11, 21')
  }} 
  onSelectionChange={ selection => console.log('selection', selection)}
  />

Untuk menghandel klik di setiap tanggal, kita harus tambahin onDateSelected di argumen useDayzed() 👇 (#1). Field ini berisi fungsi yang menerima dua argumen objek day & event (#2). Kita hanya perlu  day saja.

Di #3, kita cek apa tanggal yang diklik udah ada di selection. Kalo udah ada, hapus dari selection (#4). Kalo belum ada, tambahin (#5).

// #2
const onDateSelected = (day)=>{
  // #3
  const isInSelection = selection.find( dt => {
    return isEqual(dt, day.date);
  })

  // #4
  if(isInSelection){
    
    setSelection( curr => {
      const newArr = [...curr];

      const idx = selection.indexOf(isInSelection);

      newArr.splice(idx, 1);

      return newArr;
    })

  } else {
    // #5
    setSelection(curr => {

      const newArr = [...curr];
      
      newArr.push(day.date);

      return newArr;
    })
  }

}

const {calendars, getDateProps, getBackProps, getForwardProps} = useDayzed({
  monthsToDisplay: props.numberOfMonths,
  minDate: props.minDate,
  maxDate: props.maxDate,
  date: props.initialDate,
  selected: selection,
  // #1
  onDateSelected: onDateSelected
})  

useEffect(()=>{
  setSelection(getSelectedDatesFromArrayOrRange(props.selectedDates))
},[props.selectedDates]);

// #6
useEffect(()=>{
  if(props.onSelectionChange){
    props.onSelectionChange(selection)
  }
},[selection]);

Setiap kali selection berubah, kita kirim datanya ke callback onSelectionChange() (#6).

onMonthChange

Ini callback yang terakhir. Dipanggil setiap kali bulan yang ditampilin berubah. Di komponen kita tambahin satu useEffect() untuk manggil callback setiap kali calendars berubah.

useEffect(()=>{
  props.onMonthChange(calendars)
},[calendars]);

Di App:

<MyReactCalendar              
  selectedDates={{
    from: new Date('2021, 11, 10'),
    to: new Date('2021, 11, 21')
  }} 
  onSelectionChange={ selection => console.log('selection', selection)}
  onMonthChange={ months => console.log('months', months)}
  />

Sekian tutorialnya. Fail proyek akhirnya bisa diunduh di My-React-Calendar-Final. Moga-moga bermanfaat.