In Angular-15 application, I am using ngx-datepicker of ngx-bootstrap
Currently I have this:
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
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>
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
/>