Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

## Unreleased

### Fixed

- `ngx-forms`: Implemented a fix for the datepicker component, so the correct date is always returned no matter which timezone you are in.

## [6.1.6] - 2025-10-06

### Fixed
Expand Down
21 changes: 13 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"@angular/cli": "^15.2.0",
"@angular/compiler-cli": "^15.2.0",
"@angular/language-service": "^15.2.0",
"@date-fns/tz": "^1.4.1",
"@types/jasmine": "~3.3.8",
"@types/jasminewd2": "~2.0.3",
"@types/marked": "^4.0.8",
Expand All @@ -78,7 +79,7 @@
"@typescript-eslint/parser": "^5.48.1",
"chalk": "^3.0.0",
"cli-progress": "^3.8.2",
"date-fns": "^2.22.1",
"date-fns": "^4.1.0",
"jasmine-core": "~4.6.0",
"jasmine-marbles": "^0.6.0",
"jasmine-spec-reporter": "~4.2.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { Component } from '@angular/core';
import { TZDate } from '@date-fns/tz';
import { DateRange, DateHelper } from '@acpaas-ui/ngx-utils';

import { CalendarModule } from '../../calendar.module';
Expand Down Expand Up @@ -215,10 +216,11 @@ describe('The Calendar Component', () => {
});

it('emits the picked date and the completion status', () => {
const date = new Date();
const year = date.getFullYear();
const month = date.getMonth();
const day = date.getDate();
const date = new Date('2017-10-03T12:00:00Z');
const brusselsDate = new TZDate(date, 'Europe/Brussels');
const year = brusselsDate.getFullYear();
const month = brusselsDate.getMonth();
const day = brusselsDate.getDate();
const expectedDate = new Date(DateHelper.toUtcMidnightInBrussels(year, month, day));

calendar.pickDate(date);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from '@angular/core';

import { DateHelper, DateRange } from '@acpaas-ui/ngx-utils';
import { TZDate } from '@date-fns/tz';

import {
CALENDAR_DEFAULT_MONTH_LABELS,
Expand Down Expand Up @@ -61,7 +62,7 @@ export class CalendarComponent implements OnInit, OnChanges {
constructor(
@Inject(CALENDAR_MONTH_LABELS) public moduleMonthLabels = CALENDAR_DEFAULT_MONTH_LABELS,
@Inject(CALENDAR_WEEKDAY_LABELS) public moduleWeekdayLabels = CALENDAR_DEFAULT_WEEKDAY_LABELS,
private calendarService: CalendarService,
private calendarService: CalendarService
) {}

public ngOnInit() {
Expand Down Expand Up @@ -154,9 +155,10 @@ export class CalendarComponent implements OnInit, OnChanges {
const complete = this.activeView === CALENDAR_VIEW_MONTH;

if (complete) {
const year = date.getFullYear();
const month = date.getMonth();
const day = date.getDate();
const brusselsDate = new TZDate(date, 'Europe/Brussels');
const year = brusselsDate.getFullYear();
const month = brusselsDate.getMonth();
const day = brusselsDate.getDate();
const timezoneAwareDate = new Date(DateHelper.toUtcMidnightInBrussels(year, month, day));

this.selectDate.emit({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,7 @@
[ngClass]="{
'is-faded': !day.date || day.padding,
'is-selected':
!day.padding &&
selectedDate &&
day.date === selectedDay &&
selectedDay &&
selectedDate.getFullYear() === activeDate.getFullYear(),
!day.padding && selectedDate && day.date === selectedDay && selectedDay && selectedYear === activeYear,
'is-current': !day.padding && day.date === current,
'is-unavailable': !day.available
}"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, DebugElement } from '@angular/core';
import { TZDate } from '@date-fns/tz';

import { CalendarMonthComponent } from './month.component';
import { CALENDAR_DEFAULT_WEEKDAY_LABELS, CALENDAR_WEEKDAY_LABELS } from '../../calendar.conf';
Expand Down Expand Up @@ -88,7 +89,8 @@ describe('The Calendar Month Component', () => {

it('should set the current date', () => {
const now = new Date();
expect(comp.current).toEqual(now.getDate());
const brusselsDate = new TZDate(now, 'Europe/Brussels');
expect(comp.current).toEqual(brusselsDate.getDate());
});

it('should set the current date to -1 if the activeDate is in another month', () => {
Expand All @@ -101,11 +103,13 @@ describe('The Calendar Month Component', () => {
});

it('should set the selectedDay', () => {
const now = new Date();
const now = new Date('2017-10-15T12:00:00Z');
wrapper.selectedDate = now;
wrapper.activeDate = new Date('2017-10-01T12:00:00Z');
fixture.detectChanges();

expect(comp.selectedDay).toEqual(now.getDate());
const brusselsDate = new TZDate(now, 'Europe/Brussels');
expect(comp.selectedDay).toEqual(brusselsDate.getDate());
});

it('should set the selectedDay to -1 if the selectedDate is not set', () => {
Expand Down
26 changes: 24 additions & 2 deletions packages/ngx-calendar/src/lib/components/month/month.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ import {
SimpleChanges,
} from '@angular/core';
import { get } from 'lodash-es';
import { TZDate } from '@date-fns/tz';
import { DateHelper, DateRange, Day, Month } from '@acpaas-ui/ngx-utils';
import { CALENDAR_DEFAULT_WEEKDAY_LABELS, CALENDAR_WEEKDAY_LABELS } from '../../calendar.conf';
import { CalendarService } from '../../services/calendar.service';
import { DateRangeMap, WeekdayLabelsConfig } from '../../types/calendar.types';
import { Interval } from '@acpaas-ui/ngx-utils';

const BRUSSELS_TIMEZONE = 'Europe/Brussels';

@Component({
selector: 'aui-calendar-month',
templateUrl: './month.component.html',
Expand All @@ -33,6 +36,8 @@ export class CalendarMonthComponent implements OnInit, OnChanges {
public dates: Month = [];
public selectedDay = -1;
public current: number;
public selectedYear = -1;
public activeYear = -1;

constructor(
@Inject(CALENDAR_WEEKDAY_LABELS) private moduleWeekdayLabels = CALENDAR_DEFAULT_WEEKDAY_LABELS,
Expand All @@ -50,10 +55,27 @@ export class CalendarMonthComponent implements OnInit, OnChanges {
const monthChanged =
activeDateChanged &&
!DateHelper.datesAreEqual([changes.activeDate.currentValue, changes.activeDate.previousValue], 'M');
const selectedDayChanged = this.selectedDate && this.activeDate.getMonth() === this.selectedDate.getMonth();

let selectedDayChanged = false;
if (this.activeDate) {
const activeDateBrussels = new TZDate(this.activeDate, BRUSSELS_TIMEZONE);
this.activeYear = activeDateBrussels.getFullYear();

if (this.selectedDate) {
const selectedDateBrussels = new TZDate(this.selectedDate, BRUSSELS_TIMEZONE);
selectedDayChanged = selectedDateBrussels.getMonth() === activeDateBrussels.getMonth();
this.selectedYear = selectedDateBrussels.getFullYear();
} else {
this.selectedYear = -1;
}
} else {
this.activeYear = -1;
this.selectedYear = -1;
}

this.current = this.getCurrentDate();
this.selectedDay = selectedDayChanged ? this.selectedDate.getDate() : -1;
this.selectedDay =
selectedDayChanged && this.selectedDate ? new TZDate(this.selectedDate, BRUSSELS_TIMEZONE).getDate() : -1;

let newDates = [];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
FormsModule,
ReactiveFormsModule,
} from '@angular/forms';
import { DateRange } from '@acpaas-ui/ngx-utils';
import { DateRange, DateHelper } from '@acpaas-ui/ngx-utils';
import { CalendarModule } from '@acpaas-ui/ngx-calendar';
import { FlyoutModule } from '@acpaas-ui/ngx-flyout';
import { IconModule } from '@acpaas-ui/ngx-icon';
Expand Down Expand Up @@ -132,11 +132,11 @@ describe('The Datepicker Component', () => {
});

it('should update the model if the value is a valid date', () => {
const date = new Date();
const date = new Date('2018-01-10T00:00:00Z');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would new Date() not suffice? Or in other words, is this implementation going to be a breaking change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was done in order to have the same result every time the test ran. Changing this back to new Date() would not cause the test to fail. However, in regards to the breaking change: actually, yes, it is a breaking change, I will adjust the PR description.
Users of the component will now always get back the Brussels timezone value. So if there are applications that were doing their own calculations in order to get the correct value for their specific timezone, those will need to check their implementations of the calendar and datepicker components.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, clear. In this case, I would not label it as a breaking change (in semantic versioning), unless the previous behaviour is fixed first in this major release. Agree?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that it should be kept as a bug/fix, but I would add a clear explanation in the release notes and recommend users to thoroughly test their date calculations after upgrading


picker.writeValue(date.toISOString());

expect(accessor.update).toHaveBeenCalled();
expect(picker.formControl.value).toBeTruthy();
});
});

Expand All @@ -146,7 +146,7 @@ describe('The Datepicker Component', () => {
});

it('should update the values', () => {
const date = new Date('2018-01-10');
const date = new Date('2018-01-10T00:00:00+01:00');
picker.selectDateFromCalendar({
date,
complete: true,
Expand Down Expand Up @@ -187,11 +187,13 @@ describe('The Datepicker Component', () => {
});

it('should return the invalid range error if the date was valid and in the set range', () => {
const range = [new Date().getDate()];
const testDateISO = new Date('2018-01-15T12:00:00Z').toISOString();
const parsedDate = DateHelper.parseDate(testDateISO);
const range = [parsedDate.getDate()];
spyOn(picker.calendarService, 'getRangeForDate').and.callFake(() => range);
picker.range = range;

const ctrl = new UntypedFormControl(new Date().toISOString());
const ctrl = new UntypedFormControl(testDateISO);

expect(picker.validate(ctrl)).toEqual({
range: DATEPICKER_DEFAULT_ERROR_LABELS.ERRORS_INVALID_RANGE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
NG_VALUE_ACCESSOR,
} from '@angular/forms';
import { DateHelper, DateRange } from '@acpaas-ui/ngx-utils';
import { TZDate } from '@date-fns/tz';
import { FlyoutDirective } from '@acpaas-ui/ngx-flyout';
import {
CALENDAR_DEFAULT_MONTH_LABELS,
Expand Down Expand Up @@ -94,7 +95,7 @@ export class DatepickerComponent implements OnInit, OnChanges, OnDestroy, Contro
@Inject(DATEPICKER_ERROR_LABELS) private errorLabels = DATEPICKER_DEFAULT_ERROR_LABELS,
public calendarService: CalendarService,
private formBuilder: UntypedFormBuilder,
private ref: ChangeDetectorRef,
private ref: ChangeDetectorRef
) {}

public ngOnInit(): void {
Expand All @@ -108,12 +109,12 @@ export class DatepickerComponent implements OnInit, OnChanges, OnDestroy, Contro
const date = DateHelper.parseDate(format, 'yyyy-MM-dd');
if (date) {
this.selectedDate = date;
const year = date.getFullYear();
const month = date.getMonth();
const day = date.getDate();
const brusselsDate = new TZDate(date, 'Europe/Brussels');
const year = brusselsDate.getFullYear();
const month = brusselsDate.getMonth();
const day = brusselsDate.getDate();
this.onChange(DateHelper.toUtcMidnightInBrussels(year, month, day));
} else {
// Change value with original value (and not null or '') so we can add an error in the validate function
this.onChange(value);
}
} else {
Expand Down Expand Up @@ -141,19 +142,25 @@ export class DatepickerComponent implements OnInit, OnChanges, OnDestroy, Contro
// Create an interval if min/max is filled in
this.interval = IntervalBuilder.dateInterval(
this.min ? new Date(this.min) : null,
this.max ? new Date(this.max) : null,
this.max ? new Date(this.max) : null
)
.not()
.build();
}

public writeValue(value: string | Date): void {
this.selectedDate =
typeof value === 'string'
? this.isISODateFormat(value)
? new Date(value)
: DateHelper.parseDate(value, 'dd/MM/yyyy')
: value;
if (typeof value === 'string') {
if (this.isISODateFormat(value)) {
this.selectedDate = DateHelper.parseDate(value);
} else {
this.selectedDate = DateHelper.parseDate(value, 'dd/MM/yyyy');
}
} else if (value instanceof Date) {
this.selectedDate = value;
} else {
this.selectedDate = null;
}

const dateString = this.selectedDate ? this.formatDate(this.selectedDate) : '';
this.formControl.setValue(dateString);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/ngx-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
},
"dependencies": {
"@acpaas-ui/ngx-icon": "^6.1.5",
"date-fns": "^2.30.0"
"date-fns": "^4.1.0",
"@date-fns/tz": "^1.0.0"
},
"peerDependencies": {
"@angular/common": ">=15.2.0",
Expand Down
Loading