Favorite Datepicker for Alpine JS

Ok Everyone,

What is your favorite datepicker to use with Alpine JS. I used to use this Boostrap Datepicker:
https://bootstrap-datepicker.readthedocs.io/en/latest/

but it requires JQuery, and I’m trying not to use that in my project.

The Alpine Docs: https://laravel-livewire.com/docs/alpine-js

use Pikaday as an example. But their github: https://github.com/Pikaday/Pikaday
shows the most recent commit being from Nov 2018, which may be fine, but concerns me.

I need a datepicker that works well with Alpine/Livewire/Laravel but I want it to last as those 3 grow and change.

Thoughts??

Maybe take a look at flatpickr

3 Likes

Flatpickr is great: https://flatpickr.js.org/

1 Like

I wrote my own datepicker using alpine and tailwind so that I am able use it with livewire. To start with I thought it would quite difficult but it turns out that for my use case, it was not so.

Before I share the codes, my use case: I have only one place in which I require a datepicker (Okay two - one in create and one in update of the same field but on different pages). The purpose is to set a booking date for some meeting etc., the booking date can be set from one week from the current day till another 90 days (so startingDate and endingDate) and saturdays are OK (satudaySelectable = true), but sundays it is closed - so no booking on sunday (sundaySelectable = false). Also on official holidays, booking is closed ( excludedDates = [ array of dates ] ). This would be supplied by Laravel from table of holidays. I am not showing them here.

Here is the blade component (datepicker.blade.php)

<div 
      class="relative cursor-pointer "
      x-data="datepicker()" 
      x-init=" 
           dateString='{{ $initDateString }}';
           wireModelName='{{ $wireModel }}';
           startDate='{{ $startDate }}';
           endDate='{{ $endDate }}';
           saturdaySelectable={{ $saturdaySelectable }};
           sundaySelectable={{ $sundaySelectable }};
           excludedDates={{ $excludedDates }};
           initApp();"
>  
     <div 
           @click="showDatepicker = !showDatepicker"
           @keydown.escape="showDatepicker = false"
      >
          {{ $slot }}
     </div>

     <div 
          class="ml-4 mt-16 bg-purple-900 border-white border rounded-lg shadow p-4 absolute top-0 left-0" 
          style="width:22rem;"
          x-cloak
          x-show.transition="showDatepicker"
          @click.away="showDatepicker = false"
     >

          <div class="flex justify-between items-center mb-2">
               <div>
                    <span x-text="MONTH_NAMES[month]" class="text-lg font-bold text-purple-100"></span>
                    <span x-text="year" class="ml-1 text-lg text-purple-300 font-normal"></span>
               </div>
               <div>
                    <button 
                         type="button"
                         class="transition ease-in-out duration-100 inline-flex cursor-pointer hover:bg-purple-200 p-1 rounded-full" 
                         :class="{'cursor-not-allowed opacity-25': !isPrevMonthValid(month) }"
                         :disabled="!isPrevMonthValid(month)"
                         @click="decrementMonth();  getMonthData()"
                    >
                         <svg class="h-6 w-6 text-purple-500 inline-flex"  fill="none" viewBox="0 0 24 24" stroke="currentColor">
                              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
                         </svg>  
                    </button>
                    <button 
                         type="button"
                         class="transition ease-in-out duration-100 inline-flex cursor-pointer hover:bg-purple-200 p-1 rounded-full" 
                         :class="{'cursor-not-allowed opacity-25': !isNextMonthValid(month) }"
                         :disabled="!isNextMonthValid(month)"
                         @click="incrementMonth(); getMonthData()"
                    >
                         <svg class="h-6 w-6 text-purple-500 inline-flex"  fill="none" viewBox="0 0 24 24" stroke="currentColor">
                              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
                         </svg>									  
                    </button>
               </div>
          </div>

          <div class="w-auto grid grid-cols-7 row-gap-2 col-gap-4">
               <template x-for="(day, index) in days" :key="index">	
                    <div x-text="day" class="text-gray-100 font-medium text-center text-sm mb-3"></div>
               </template>

               <template x-for="blankday in [...Array(startOfMonthOnDay).keys()]">
                    <div class="border p-1 border-transparent"></div>
               </template>            
        
               <template x-for="dateIndex in [...Array(numDaysInMonth).keys()]" :key="dateIndex">

                    <div
                         @click="if (isDateSelectable(dateIndex+1)) { selectDate(dateIndex+1);  showDatepicker = false; @this.set(wireModelName, dateString) }"
                         x-text="(dateIndex+1)"
                         class="px-2 py-1 cursor-pointer text-center text-sm leading-none rounded-full leading-loose transition ease-in-out duration-100"
                         :class="{'bg-purple-100 text-purple-800': isDateSelected(dateIndex+1), 'cursor-not-allowed opacity-25 text-purple-700': ( !isDateSelected(dateIndex+1)  && !isDateSelectable(dateIndex+1)),  'text-gray-200 hover:bg-purple-700': (isDateSelectable(dateIndex+1) && !isDateSelected(dateIndex+1)) }"
                    ></div>
               </template>
          </div>
     </div>
</div>

Now the script (datepicker.js)

const MONTH_NAMES = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];

function datepicker() {
     return {
          dateString: '', 
          startDate: '',
          endDate: '',
          wireModelName: '',

          showDatepicker: false,
          validStartTime: '',
          validEndTime: '',

          validStartMonth: '',
          validEndMonth: '',

          saturdaySelectable: true,
          sundaySelectable: false,
          excludedDates: [],

          month: '',
          year: '',

          numDaysInMonth: '',
          startOfMonthOnDay: '',

          days: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],

          initApp() {                    

               let day = new Date (this.startDate);
               this.validStartTime = day.getTime() + day.getTimezoneOffset()*60*1000;
               this.validStartMonth = day.getFullYear()*12+day.getMonth();
        
               day = new Date(this.endDate);
               this.validEndTime = day.getTime()+ day.getTimezoneOffset()*60*1000;
               this.validEndMonth = day.getFullYear()*12+day.getMonth();

               day = new Date();
               this.month = day.getMonth();
               this.year = day.getFullYear();
        
               this.getMonthData(); 
          },
          getFormattedDateString (day) {
               return day.getFullYear()+'-'+('0'+(day.getMonth()+1)).slice(-2)+'-'+('0'+day.getDate()).slice(-2);
          },

          incrementMonth() {
               this.year = parseInt((this.year*12 + this.month+1)/12);
               this.month = (this.month+1)%12;
          },

          decrementMonth() {
               this.year = parseInt((this.year*12 + this.month-1)/12);
               this.month = (this.month+11)%12;
          },

          isDateSelected(date) {
               let d = new Date(this.year, this.month, date);
               return this.getFormattedDateString(d) ===  this.dateString;
          },

          selectDate(date) {      
               selectedDate = new Date(this.year, this.month, date);
               this.dateString = this.getFormattedDateString(selectedDate);    
          },
    
          isDateSelectable(date) {
               let d = new Date(this.year, this.month, date);
               if (d.getTime() < this.validStartTime) return false;
               if (d.getTime() > this.validEndTime) return false;

               if ( (!this.saturdaySelectable) && (d.getDay() === 6)) return false;
               if ( (!this.sundaySelectable) && (d.getDay() === 0)) return false;

               if (this.excludedDates.includes(this.getFormattedDateString(d))) return false;

               return true;
          },

          isNextMonthValid(month) {
               return (this.year*12 + month+1 <= this.validEndMonth) ;
          },

          isPrevMonthValid(month) {
               return (this.year*12 + month-1 >= this.validStartMonth) ;
          },

          getMonthData() {
               this.numDaysInMonth = new Date(this.year, this.month + 1, 0).getDate();
               this.startOfMonthOnDay = new Date(this.year, this.month).getDay();
          }
     }
}

Datepicker.php which is straightforward (only bits I am putting). Just the constructor and render.

public function __construct($wireModel, $startDate, $endDate, 
          $initDateString = '', $saturdaySelectable = true, $sundaySelectable = false,
          $excludedDates = '[]')
{
     ....
}

public function render()
{
     return view('components.datepicker');
}

Now to call the compoent from livewire view and associate it with the variable “booking_date”

<x-datepicker
     :start-date="$startDate"
     :end-date="$endDate"
     wire-model="booking_date"
     :init-date-string="$booking_date"
>

     <div class="flex items-center justify-between">

          <input 
               readonly
               id="booking_date"
               class="form-input text-purple-300 placeholder-gray-400"
               type="text" 
               wire:model='booking_date'
               wire:ignore
               placeholder="Click to set date"
          >
          <svg 
               class="h-6 w-6 mr-3" 
               fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor"
          >
               <path d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
          </svg>
     </div>

</x-datepicker>

This of course is not a fll-fledged datepicker, but if anyone is interested they can add those functions, and if these codes are any indication, those will also be quite straightforward.

Just one last point - the theme I am using is a dark one (bg-purple-900) so you might need to adjust the css.

Just wish their docs where a bit more clear on how to use it.