Search code examples
angularngx-bootstrap

Angular - ngx-datepicker validation not working as expected


In Angular-15 application, I am using ngx-datepicker of ngx-bootstrap

Currently I have this:

  1. StartDate and EndDate it enables the current date back to the last one year, then the rest disabled
  2. Between the StartDate and EndDate, the user should not be able to select more than a month, even though it enables a year
  3. When the Search button clicked and the validation is violated, it displays this: Date range cannot exceed one month

All the three (3) above works, but I have this problem:

When the user clicks on search button, whenever the validation is violated. that is when user selects more than a month range between StartDate and EndDate, after the search button clicked, It disables all the other dates, and enables only the date selected on StartDate for both StartDate and EndDate

For instance, if the user selects

StartDate : 01-Jul-2024 EndDate : 01-NOV-2024

and clicks search

After showing the validation warning, it will only enable

01-Jul-2024

for both StartDate and EndDate

datepicker_issue

Kindly help resolve it.

MAIN CODE:

created-date-transactions:

import { Component, TemplateRef } from '@angular/core';
import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
import { PaginatePipe } from 'ngx-pagination';
import { ToastrService } from 'ngx-toastr';
import { TransactionService } from 'src/app/features/admin/services/transaction.service';
import {
  ITransactionCreatedDateList,
  ICreatedPageResult,
  ICreatedResponse,
  ICreatedPagingFilter,
} from 'src/app/features/admin/models/transaction/transaction-created-date-list.model';
import { DatePipe, CurrencyPipe } from '@angular/common';
import { OrderPipe } from 'ngx-order-pipe';
import { saveAs } from 'file-saver';
import { interval, Subscription } from 'rxjs';
import { startWith, switchMap } from 'rxjs/operators';

@Component({
  selector: 'app-created-date-transactions',
  templateUrl: './created-date-transactions.component.html',
  styleUrls: ['./created-date-transactions.component.scss'],
})
export class CreatedDateTransactionsComponent {
  bsModalRef?: BsModalRef;
  search = 'Search';
  transactionList: any;
  columns: any[] = [];
  columnsWithFeatures: any;
  isLoading = false;
  showModal!: boolean;
  selectedSearchCriteria: any;
  order: string = 'createDate';
  reverse: boolean = false;
  allTransactionList!: ITransactionCreatedDateList[];
  pageResult!: ICreatedPageResult<ITransactionCreatedDateList[]>;
  filter: ICreatedPagingFilter = {
    searchQuery: '',
    sortBy: '',
    isSortAscending: true,
    startDate: null,
    endDate: null,
    pageNumber: 1,
    pageSize: 10,
    exportToExcel: false,
  };
  minStartDate!: Date;
  minEndDate!: Date;
  maxStartDate!: Date;
  maxEndDate!: Date;
  startingIndex: number = 0; // Initialize with 0
  searchButtonClicked = false;
  private refreshSubscription!: Subscription;

  constructor(
    private transactionService: TransactionService,
    private datePipe: DatePipe,
    private currencyPipe: CurrencyPipe,
    private toastr: ToastrService,
    private modalService: BsModalService,
    private orderPipe: OrderPipe,
    private paginatePipe: PaginatePipe
  ) {
    const today = new Date();
    this.maxStartDate = today;
    this.minStartDate = new Date();
    this.minStartDate.setFullYear(today.getFullYear() - 1);
    this.maxEndDate = today;
    this.minEndDate = new Date();
    this.minEndDate.setFullYear(today.getFullYear() - 1);
  }

  ngOnInit(): void {
    this.isLoading = true;
  }

  validateDateRange(startDate: Date, endDate: Date): boolean {
    const oneMonthInMilliseconds = 30 * 24 * 60 * 60 * 1000; // Approx. one month
    return Math.abs(endDate.getTime() - startDate.getTime()) <= oneMonthInMilliseconds;
  }  


  validateSearchDateRange() {
    if (this.filter.startDate && this.filter.endDate) {
      const startDate = new Date(this.filter.startDate);
      const endDate = new Date(this.filter.endDate);
      
      // Calculate the difference in months
      const monthDifference = (endDate.getFullYear() - startDate.getFullYear()) * 12 + 
                               (endDate.getMonth() - startDate.getMonth());
      
      if (Math.abs(monthDifference) > 1) {
        this.toastr.warning('Date range cannot exceed one month');
        
        // Reset end date to exactly one month after start date
        const adjustedEndDate = new Date(startDate);
        adjustedEndDate.setMonth(startDate.getMonth() + 1);
        
        this.filter.endDate = adjustedEndDate;
        
        // Recalculate min and max dates for both inputs
        this.minEndDate = startDate;
        this.maxEndDate = adjustedEndDate;
        this.minStartDate = new Date(startDate.getFullYear() - 1, startDate.getMonth(), startDate.getDate());
        this.maxStartDate = new Date();
      }
    }
  } 

  updateBsConfig(): void {
    this.filter.startDate
      ? (this.maxEndDate = this.filter.startDate)
      : (this.maxEndDate = new Date());
    this.filter.endDate
      ? (this.maxStartDate = this.filter.endDate)
      : (this.maxStartDate = new Date());
  }

  loadAllTransactions() {
    this.updateBsConfig();
    this.transactionService
      .getAllTransactionsByCreatedDateFilter(this.filter)
      .subscribe((result) => {
        this.pageResult = result;
        this.allTransactionList = result.pageItems;
        this.isLoading = false;
      });
  }

  onSearch() {
    this.validateSearchDateRange();
    this.searchButtonClicked = true;
    this.filter.pageNumber = 1;
    this.loadAllTransactions();
  }


  onDateRangeSelected(startDate: Date | null, endDate: Date | null) {
    if (startDate && endDate) {
      if (this.validateDateRange(startDate, endDate)) {
        this.filter.startDate = startDate;
        this.filter.endDate = endDate;
        this.loadAllTransactions();
      } else {
        this.toastr.error('The selected date range cannot exceed one month.');
  
        // Reset the filter dates to `null` to clear invalid selections
        this.filter.startDate = null;
        this.filter.endDate = null;
      }
    } else {
      this.toastr.error('Invalid date selection. Please select valid start and end dates.');
  
      // Reset the filter dates to avoid further issues
      this.filter.startDate = null;
      this.filter.endDate = null;
    }
  }
}

This is the html aspect:

created-date-transactions.html:

<div class="content-header">
  <div class="container-fluid">
    <div class="row mb-2">
      <div class="col-sm-6">
        <h1 class="m-0">Admin Dashboard: {{ pageTitle }}</h1>
      </div>
      <div class="col-sm-6">
        <ol class="breadcrumb float-sm-right">
          <li class="breadcrumb-item">
            <a [routerLink]="['/admin-dashboard']">Dashboard</a>
          </li>
          <li class="breadcrumb-item active">{{ pageTitle }}</li>
        </ol>
      </div>
    </div>
  </div>
</div>
<section class="content">
  <div class="container-fluid">
    <div class="row">
      <div class="col-sm-12 col-xs-12 col-12">
        <div class="card card-danger">
          <div class="card-header">
            <h3 class="card-title">{{ search }}</h3>
            <div class="card-tools">
              <button
                type="button"
                class="btn btn-tool"
                data-card-widget="collapse"
              >
                <i class="fas fa-minus"></i>
              </button>
            </div>
          </div>
          <div class="card-body">
            <div class="row">
              <div class="col-md-6">
                <div class="form-group">
                  <label for="startDate">Start Date:<span style="color: red">*</span></label>
                  <div class="input-group">
                    <div class="input-group-prepend">
                      <span class="input-group-text"
                        ><i class="far fa-calendar-alt"></i
                      ></span>
                    </div>
                    <input
                      type="text"
                      placeholder="DD-MM-YYYY"
                      class="form-control"
                      bsDatepicker
                      [minDate]="minStartDate"
                      [maxDate]="maxStartDate"
                      [(ngModel)]="filter.startDate"
                      [bsConfig]="{
                        isAnimated: true,
                        dateInputFormat: 'DD-MM-YYYY',
                        returnFocusToInput: true,
                        showClearButton: true,
                        clearPosition: 'right',
                        maxDate: maxStartDate
                      }"
                      (change)="filter.startDate && filter.endDate && onDateRangeSelected(filter.startDate, filter.endDate)"
                      required
                    />
                  </div>
                </div>
              </div>
              <div class="col-md-6">
                <div class="form-group">
                  <label for="endDate">End Date:<span style="color: red">*</span></label>
                  <div class="input-group">
                    <div class="input-group-prepend">
                      <span class="input-group-text"
                        ><i class="far fa-calendar-alt"></i
                      ></span>
                    </div>
                    <input
                      type="text"
                      placeholder="DD-MM-YYYY"
                      class="form-control"
                      bsDatepicker
                      [minDate]="minEndDate"
                      [maxDate]="maxEndDate"
                      [(ngModel)]="filter.endDate"
                      [bsConfig]="{
                        isAnimated: true,
                        dateInputFormat: 'DD-MM-YYYY',
                        returnFocusToInput: true,
                        showClearButton: true,
                        clearPosition: 'right',
                        maxDate: maxEndDate
                      }"
                      (change)="filter.startDate && filter.endDate && onDateRangeSelected(filter.startDate, filter.endDate)"
                      required
                    />
                  </div>
                </div>
              </div>
              <div class="col-md-12">
                <div class="form-group">
                  <div class="input-group">
                    <input
                      type="text"
                      placeholder="Search By: Merchant, Description, Channel ..."
                      class="form-control"
                      id="searchInput"
                      [(ngModel)]="filter.searchQuery"
                    />
                    <div class="input-group-append">
                      <button
                        class="btn btn-primary"
                        type="button"
                        (click)="onSearch()"
                        [disabled]="!filter.startDate || !filter.endDate"
                      >
                        Search
                      </button>
                    </div>
                  </div>
                </div>
              </div>
            </div>
            <div class="modal-footer">
              <button
                type="button"
                class="btn btn-success"
                title="Export Excel Data"
                (click)="onExportToExcel()"
                [disabled]="!filter.startDate || !filter.endDate"
              >
                <i class="fa fa-file-excel-o" aria-hidden="true"></i> Export to
                Excel
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
    </div>
  </div>
</section>


Solution

  • After some trial and error, I noticed that when using [minDate] and [maxDate] directives, the rendered HTML for the <input> element will show both "ng-reflect-min-date" and "ng-reflect-max-date" attributes with minEndDate value. I suspected that it may be a bug.

    This can be solved by working with the [bsConfig] directive and providing the BsDatepickerConfig containing minDate and maxDate.

    import {
      BsDatepickerConfig,
      BsDatepickerModule
    } from 'ngx-bootstrap/datepicker';
    
    endDateBsConfig?: Partial<BsDatepickerConfig> = {
      isAnimated: true,
      dateInputFormat: 'DD-MM-YYYY',
      returnFocusToInput: true,
      showClearButton: true,
      clearPosition: 'right'
    };
    
    setEndDateDatepickerBsConfig() {
      this.endDateBsConfig = {
        ...this.endDateBsConfig,
        minDate: this.minEndDate,
        maxDate: this.maxEndDate,
      };
    }
    

    Make sure that you are calling the setEndDateDatepickerBsConfig method when initializing/updating the BsDatepickerConfig instance for the end date.

    constructor(
      private transactionService: TransactionService,
      private datePipe: DatePipe,
      private currencyPipe: CurrencyPipe,
      private toastr: ToastrService,
      private modalService: BsModalService, 
      private orderPipe: OrderPipe, 
      private paginatePipe: PaginatePipe
    ) {
      ...
    
      this.setEndDateDatepickerBsConfig();
    }
    
    validateSearchDateRange() {
      if (this.filter.startDate && this.filter.endDate) {
        const startDate = new Date(this.filter.startDate);
        const endDate = new Date(this.filter.endDate);
    
        // Calculate the difference in months
        const monthDifference =
          (endDate.getFullYear() - startDate.getFullYear()) * 12 +
          (endDate.getMonth() - startDate.getMonth());
    
        if (Math.abs(monthDifference) > 1) {
          this.toastr.warning('Date range cannot exceed one month');
    
          // Reset end date to exactly one month after start date
          const adjustedEndDate = new Date(startDate);
          adjustedEndDate.setMonth(startDate.getMonth() + 1);
    
          this.filter.endDate = adjustedEndDate;
    
          // Recalculate min and max dates for both inputs
          this.minEndDate = startDate;
          this.maxEndDate = adjustedEndDate;
          this.minStartDate = new Date(
            startDate.getFullYear() - 1,
            startDate.getMonth(),
            startDate.getDate()
          );
          this.maxStartDate = new Date();
    
          this.setEndDateDatepickerBsConfig();
        }
      }
    }
    

    Remove the minDate and maxDate directives, and supply the endDateBsConfig instance to the [bsConfig] directive.

    <input
      type="text"
      placeholder="DD-MM-YYYY"
      class="form-control"
      bsDatepicker
      [(ngModel)]="filter.endDate"
      [bsConfig]="endDateBsConfig"
      (change)="
        filter.startDate &&
        filter.endDate &&
        onDateRangeSelected(filter.startDate, filter.endDate)
      "
      required
    />
    

    Demo @ StackBlitz