Search code examples
angularionic-frameworkionic4ionic5

Disabled dates in ion-datetime (Ionic 4, Ionic 5)


The scenario I have implemented my ion-datetime into requires that certain dates can be disabled. Blockout dates. This is something that's fairly common in apps in general, like Airbnb.

The ion-datetime module that ships with Ionic does not support disabled dates as far as I can tell and I could not find an external library that supported date and time selection that could be implemented into a modern iteration of Ionic and work.

Has anyone tackled this issue in an elegant way or have any solution to make the [Ionic] native datepicker disable certain picker-opt days from the select wheel?


Solution

  • I have had to adapt my own solution in the end, having had no leads. I thought I'd share this in case anyone else faces a similar situation (until Ionic inevitably adds something to its core).

    These instructions make the assumption that you already have a solution to set blocked dates, I recommend using ion2-calendar (& its multi pick mode)

    So in your HTML you would have something like:

    <ion-datetime
        #orderDatePicker
        (click)="setUpListeners()"
        (ionChange)="setSelectedDateTime( orderDatePicker.value )"
        displayFormat="DD MMM YYYY HH:mm"
        max="2021"
        minuteValues="0,15,30,45"
        [min]="todaysDate"
        placeholder="SELECT"></ion-datetime>
    

    Some required imports in your TS (underscore):

    import * as _ from 'underscore/underscore';
    

    And in your TS file you'd have to add above the constructor your default variables to use pervasively:

    @ViewChild('orderDatePicker', {read: ElementRef}) public orderDatePicker: ElementRef;
    dayChangeObserver: any = null;
    monthChangeObserver: any = null;
    current_month_blockout_dates: any = null;
    

    You need a method to filter down to the blocked dates of a selected month.

    checkMonth( monthNumber: number ) {
        let dates = this.product.blockout_dates;
        if(dates) {
          let current_dates = dates.filter(dateObj => dateObj.months == monthNumber);
          return current_dates;
        }
        return [];
    }
    

    Then you will notice I have a method set on the click event of the ion-datetime, so it will set up some listeners, mutation observers to be exact.

    I have commented the code to explain it, but essentially the scrollers use style to transform their X axis up and down, so the mutation observers watch these events to know when changes are being made - as the native available ionChange event only fires on the selection process.

    The Done button is shown or hidden as a result. I am using jQuery to make javascript selection a little quicker (for me at least). I am also using the underscore utility belt to debounce on the mutation observers, otherwise they fire too many times while scrolling.

    setUpListeners() {
        const self = this;
        if(!this.monthChangeObserver && this.current_month_blockout_dates.length > 0) {
          setTimeout(() => {
            const day_scroller    = $('.picker-col:nth-of-type(1) .picker-opt-selected');
            const month_scroller  = $('.picker-col:nth-of-type(2) .picker-opt-selected');
    
            // Whenever a month is scrolled
            this.monthChangeObserver = new MutationObserver(_.debounce(function() {
              let monthNumeric = (1+Number($('.picker-col:nth-of-type(2) .picker-opt-selected').attr('opt-index')));
    
              // Retrieve the blocked dates for the selected month
              this.current_month_blockout_dates = self.checkMonth(monthNumeric); // uses 'self' to step outside of mutation scope
              let bdates = this.current_month_blockout_dates;
    
              // Enable all days for the current month
              $(`.picker-col:nth-of-type(1) button.disabled`).removeClass('disabled');
    
              // Create an array of the blocked days
              let bdays = [];
              if(bdates.length > 0){
                bdates.reduce(function(previousVal, currentVal) { bdays.push(currentVal.date); }, 0);
              }
    
              // Cycle through that array and disable those day dates (visually)
              if(bdays.length > 0){
                bdays.reduce(function(prv, cur) {
                  $(`.picker-col:nth-of-type(1) .picker-opt[opt-index=${cur}]`).addClass('disabled');
                }, 0);
              }
    
              // Disable or enable the "Done" button depending on whether or not the selected day is 'disabled' (after month is scrolled)
              if($(`.picker-col:nth-of-type(1) button.picker-opt-selected`).hasClass('disabled')) {
                $('.picker-toolbar-button:not(.picker-toolbar-cancel)').hide();
              } else {
                $('.picker-toolbar-button:not(.picker-toolbar-cancel)').show();
              }
            }, 100));
    
            // Whenever a day is scrolled
            this.dayChangeObserver = new MutationObserver(_.debounce(function() {
              let selected_day = $('.picker-col:nth-of-type(1) .picker-opt-selected');
              let dayNumeric = (1+Number(selected_day.attr('opt-index')));
    
              // Disable or enable the "Done" button depending on whether or not the selected day is 'disabled'
              if(selected_day.hasClass('disabled')) {
                $('.picker-toolbar-button:not(.picker-toolbar-cancel)').hide();
              } else {
                $('.picker-toolbar-button:not(.picker-toolbar-cancel)').show();
              }
            }, 100));
    
            this.dayChangeObserver.observe(day_scroller[0], { attributes : true, attributeFilter : ['style'], childList: false, characterData:false , subtree:false });
            this.monthChangeObserver.observe(month_scroller[0], { attributes : true, attributeFilter : ['style'], childList: false, characterData:false , subtree:false });
          }, 100); // Slight delay to allow the DOM to render
        }
      }
    

    Lastly just add the CSS to your global.scss file:

    ion-picker-column {
        .picker-opts {
            .picker-opt {
                &.disabled {
                    opacity: 0.25;
                    text-decoration: line-through !important;
                }
            }
        }
    }
    

    crossed out day (date) and Done button hidden