Skip to content

Commit 9708153

Browse files
committed
refactor: Added support for string-based property values for date based components
This commit adds support for passing in string based date values to date-bound properties of calendar, date-time input and date picker components. The component will now try to parse the passed in value without directly throwing an exception. Unified the type declarations between date-bound component.
1 parent 8b28c09 commit 9708153

File tree

9 files changed

+304
-98
lines changed

9 files changed

+304
-98
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/)
55
and this project adheres to [Semantic Versioning](http://semver.org/).
66

7+
## [Unreleased]
8+
### Changed
9+
- Calendar - allow passing a string value to the backing `value`, `values` and `activeDate` properties [#1467](https://github.com/IgniteUI/igniteui-webcomponents/issues/1467)
10+
- Date-time input - allow passing a string value to the backing `value`, `min` and `max` properties [#1467](https://github.com/IgniteUI/igniteui-webcomponents/issues/1467)
11+
- Date picker - allow passing a string value to the backing `value`, `min`, `max` and `activeDate` properties [#1467](https://github.com/IgniteUI/igniteui-webcomponents/issues/1467)
12+
713
## [5.1.2] - 2024-11-04
814
### Added
915
- Carousel component select method overload accepting index [#1457](https://github.com/IgniteUI/igniteui-webcomponents/issues/1457)

src/components/calendar/base.ts

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,8 @@ import { property, state } from 'lit/decorators.js';
44
import { blazorDeepImport } from '../common/decorators/blazorDeepImport.js';
55
import { blazorIndirectRender } from '../common/decorators/blazorIndirectRender.js';
66
import { watch } from '../common/decorators/watch.js';
7-
import {
8-
dateFromISOString,
9-
datesFromISOStrings,
10-
getWeekDayNumber,
11-
} from './helpers.js';
7+
import { first } from '../common/util.js';
8+
import { convertToDate, convertToDates, getWeekDayNumber } from './helpers.js';
129
import { CalendarDay } from './model.js';
1310
import type { DateRangeDescriptor, WeekDays } from './types.js';
1411

@@ -18,7 +15,7 @@ export class IgcCalendarBaseComponent extends LitElement {
1815
private _initialActiveDateSet = false;
1916

2017
protected get _hasValues() {
21-
return this._values.length > 0;
18+
return this._values && this._values.length > 0;
2219
}
2320

2421
protected get _isSingle() {
@@ -43,7 +40,7 @@ export class IgcCalendarBaseComponent extends LitElement {
4340
protected _activeDate = CalendarDay.today;
4441

4542
@state()
46-
protected _value?: CalendarDay;
43+
protected _value: CalendarDay | null = null;
4744

4845
@state()
4946
protected _values: CalendarDay[] = [];
@@ -54,8 +51,8 @@ export class IgcCalendarBaseComponent extends LitElement {
5451
@state()
5552
protected _disabledDates: DateRangeDescriptor[] = [];
5653

57-
public get value(): Date | undefined {
58-
return this._value ? this._value.native : undefined;
54+
public get value(): Date | null {
55+
return this._value ? this._value.native : null;
5956
}
6057

6158
/* blazorSuppress */
@@ -65,9 +62,10 @@ export class IgcCalendarBaseComponent extends LitElement {
6562
*
6663
* @attr value
6764
*/
68-
@property({ converter: dateFromISOString })
69-
public set value(value) {
70-
this._value = value ? CalendarDay.from(value) : undefined;
65+
@property({ converter: convertToDate })
66+
public set value(value: Date | string | null) {
67+
const converted = convertToDate(value);
68+
this._value = converted ? CalendarDay.from(converted) : null;
7169
}
7270

7371
public get values(): Date[] {
@@ -81,9 +79,10 @@ export class IgcCalendarBaseComponent extends LitElement {
8179
*
8280
* @attr values
8381
*/
84-
@property({ converter: datesFromISOStrings })
85-
public set values(values) {
86-
this._values = values ? values.map((v) => CalendarDay.from(v)) : [];
82+
@property({ converter: convertToDates })
83+
public set values(values: Date[] | string | null) {
84+
const converted = convertToDates(values);
85+
this._values = converted ? converted.map((v) => CalendarDay.from(v)) : [];
8786
}
8887

8988
public get activeDate(): Date {
@@ -92,10 +91,13 @@ export class IgcCalendarBaseComponent extends LitElement {
9291

9392
/* blazorSuppress */
9493
/** Get/Set the date which is shown in view and is highlighted. By default it is the current date. */
95-
@property({ attribute: 'active-date', converter: dateFromISOString })
96-
public set activeDate(value) {
94+
@property({ attribute: 'active-date', converter: convertToDate })
95+
public set activeDate(value: Date | string) {
9796
this._initialActiveDateSet = true;
98-
this._activeDate = value ? CalendarDay.from(value) : CalendarDay.today;
97+
const converted = convertToDate(value);
98+
this._activeDate = converted
99+
? CalendarDay.from(converted)
100+
: CalendarDay.today;
99101
}
100102

101103
/**
@@ -154,7 +156,7 @@ export class IgcCalendarBaseComponent extends LitElement {
154156
@watch('selection', { waitUntilFirstUpdate: true })
155157
protected selectionChanged() {
156158
this._rangePreviewDate = undefined;
157-
this._value = undefined;
159+
this._value = null;
158160
this._values = [];
159161
}
160162

@@ -166,7 +168,7 @@ export class IgcCalendarBaseComponent extends LitElement {
166168
if (this._isSingle) {
167169
this.activeDate = this.value ?? this.activeDate;
168170
} else {
169-
this.activeDate = this.values[0] ?? this.activeDate;
171+
this.activeDate = first(this.values) ?? this.activeDate;
170172
}
171173
}
172174
}

src/components/calendar/calendar.interaction.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,17 @@ describe('Calendar interactions', () => {
4646
expect(date.equalTo(calendar.value!)).to.be.true;
4747
});
4848

49+
it('setting `value` - string property binding', async () => {
50+
const date = new CalendarDay({ year: 2022, month: 0, date: 19 });
51+
calendar.value = date.native.toISOString();
52+
53+
expect(date.equalTo(calendar.value!)).to.be.true;
54+
55+
// Invalid date
56+
calendar.value = new Date('s');
57+
expect(calendar.value).to.be.null;
58+
});
59+
4960
it('setting `values` attribute', async () => {
5061
const date_1 = new CalendarDay({ year: 2022, month: 0, date: 19 });
5162
const date_2 = date_1.set({ date: 22 });
@@ -61,6 +72,25 @@ describe('Calendar interactions', () => {
6172
expect(date_2.equalTo(last(calendar.values))).to.be.true;
6273
});
6374

75+
it('setting `values` - string property binding', async () => {
76+
const date_1 = new CalendarDay({ year: 2022, month: 0, date: 19 });
77+
const date_2 = date_1.set({ date: 22 });
78+
79+
calendar.selection = 'multiple';
80+
calendar.values = `${date_1.native.toISOString()}, ${date_2.native.toISOString()}`;
81+
82+
expect(calendar.values).lengthOf(2);
83+
expect(date_1.equalTo(first(calendar.values))).to.be.true;
84+
expect(date_2.equalTo(last(calendar.values))).to.be.true;
85+
86+
// Invalid dates
87+
calendar.values = 'nope, nope again';
88+
expect(calendar.values).is.empty;
89+
90+
calendar.values = '';
91+
expect(calendar.values).is.empty;
92+
});
93+
6494
it('clicking previous/next buttons in days view', async () => {
6595
const { previous, next } = getCalendarDOM(calendar).navigation;
6696

src/components/calendar/helpers.ts

Lines changed: 92 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
asNumber,
33
findElementFromEventPath,
44
first,
5+
isString,
56
last,
67
modulo,
78
} from '../common/util.js';
@@ -36,18 +37,99 @@ const DaysMap = {
3637

3738
/* Converter functions */
3839

39-
export function dateFromISOString(value: string | null) {
40-
return value ? new Date(value) : null;
40+
/**
41+
* Converts the given value to a Date object.
42+
*
43+
* If the value is already a valid Date object, it is returned directly.
44+
* If the value is a string, it is parsed into a Date object.
45+
* If the value is null or undefined, null is returned.
46+
* If the parsing fails, null is returned.
47+
*
48+
* @param value The value to convert.
49+
* @returns The converted Date object, or null if the conversion fails.
50+
*
51+
* @example
52+
* ```typescript
53+
* const dateString = '2023-11-11T12:34:56Z';
54+
* const dateObject = new Date('2023-11-11T12:34:56Z');
55+
* const nullValue = null;
56+
57+
* const result1 = convertToDate(dateString); // Date object
58+
* const result2 = convertToDate(dateObject); // Date object
59+
* const result3 = convertToDate(nullValue); // null
60+
* const result4 = convertToDate('invalid-date-string'); // null
61+
* ```
62+
*/
63+
export function convertToDate(value: Date | string | null): Date | null {
64+
if (!value) {
65+
return null;
66+
}
67+
68+
const converted = isString(value) ? new Date(value) : value;
69+
return Number.isNaN(converted.valueOf()) ? null : converted;
70+
}
71+
72+
/**
73+
* Converts a Date object to an ISO 8601 string.
74+
*
75+
* If the `value` is a `Date` object, it is converted to an ISO 8601 string.
76+
* If the `value` is null or undefined, null is returned.
77+
*
78+
* @param value The Date object to convert.
79+
* @returns The ISO 8601 string representation of the Date object, or null if the value is null or undefined.
80+
*
81+
* @example
82+
* ```typescript
83+
* const dateObject = new Date('2023-11-11T12:34:56Z');
84+
* const nullValue = null;
85+
86+
* const result1 = getDateFormValue(dateObject); // "2023-11-11T12:34:56.000Z"
87+
* const result2 = getDateFormValue(nullValue); // null
88+
* ```
89+
*/
90+
export function getDateFormValue(value: Date | null) {
91+
return value ? value.toISOString() : null;
4192
}
4293

43-
export function datesFromISOStrings(value: string | null) {
44-
return value
45-
? value
46-
.split(',')
47-
.map((v) => v.trim())
48-
.filter((v) => v)
49-
.map((v) => new Date(v))
50-
: null;
94+
/**
95+
* Converts an array of Date objects or a comma-separated string of ISO 8601 dates into an array of Date objects.
96+
97+
* If the `value` is an array of `Date` objects, it is returned directly.
98+
* If the `value` is a string, it is split by commas and each part is parsed into a `Date` object.
99+
* If the `value` is null or undefined, null is returned.
100+
* If the parsing fails for any date, it is skipped.
101+
102+
* @param value The value to convert.
103+
* @returns An array of Date objects, or null if the conversion fails for all values.
104+
105+
* @example
106+
* ```typescript
107+
* const dateStrings = '2023-11-11T12:34:56Z,2023-12-12T13:45:00Z';
108+
* const dateObjects = [new Date('2023-11-11T12:34:56Z'), new Date('2023-12-12T13:45:00Z')];
109+
* const nullValue = null;
110+
111+
* const result1 = convertToDates(dateStrings); // [Date, Date]
112+
* const result2 = convertToDates(dateObjects); // [Date, Date]
113+
* const result3 = convertToDates(nullValue); // null
114+
* const result4 = convertToDates('invalid-date-string,2023-11-11T12:34:56Z'); // [Date]
115+
* ```
116+
*/
117+
export function convertToDates(value: Date[] | string | null) {
118+
if (!value) {
119+
return null;
120+
}
121+
122+
const values: Date[] = [];
123+
const iterator = isString(value) ? value.split(',') : value;
124+
125+
for (const each of iterator) {
126+
const date = convertToDate(isString(each) ? each.trim() : each);
127+
if (date) {
128+
values.push(date);
129+
}
130+
}
131+
132+
return values;
51133
}
52134

53135
/**

src/components/calendar/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,5 @@ export type WeekDays =
2323
| 'saturday';
2424

2525
export interface IgcCalendarComponentEventMap {
26-
igcChange: CustomEvent<Date | Date[]>;
26+
igcChange: CustomEvent<Date | Date[] | null>;
2727
}

src/components/date-picker/date-picker.spec.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,31 @@ describe('Date picker', () => {
223223
checkDatesEqual(dateTimeInput.value!, expectedValue);
224224
});
225225

226+
it('should be successfully initialized with a string property binding - issue 1467', async () => {
227+
const value = new CalendarDay({ year: 2000, month: 0, date: 25 });
228+
picker = await fixture<IgcDatePickerComponent>(html`
229+
<igc-date-picker .value=${value.native.toISOString()}></igc-date-picker>
230+
`);
231+
232+
expect(CalendarDay.from(picker.value!).equalTo(value)).to.be.true;
233+
});
234+
235+
it('should not set an invalid date object as a value', async () => {
236+
picker = await fixture<IgcDatePickerComponent>(html`
237+
<igc-date-picker value="invalid date"></igc-date-picker>
238+
`);
239+
240+
expect(picker.value).to.be.null;
241+
});
242+
243+
it('should not set an invalid date object as a value through property binding', async () => {
244+
picker = await fixture<IgcDatePickerComponent>(html`
245+
<igc-date-picker .value=${new Date('s')}></igc-date-picker>
246+
`);
247+
248+
expect(picker.value).to.be.null;
249+
});
250+
226251
it('should be successfully initialized in open state in dropdown mode', async () => {
227252
picker = await fixture<IgcDatePickerComponent>(
228253
html`<igc-date-picker open></igc-date-picker>`
@@ -455,7 +480,7 @@ describe('Date picker', () => {
455480
checkDatesEqual(picker.activeDate, currentDate);
456481
expect(picker.value).to.be.null;
457482
checkDatesEqual(calendar.activeDate, currentDate);
458-
expect(calendar.value).to.be.undefined;
483+
expect(calendar.value).to.be.null;
459484
});
460485

461486
it('should initialize activeDate = value when it is not set, but value is', async () => {
@@ -959,6 +984,31 @@ describe('Date picker', () => {
959984
spec.submitValidates();
960985
});
961986

987+
it('should enforce min value constraint with string property', async () => {
988+
spec.element.min = new Date(2025, 0, 1).toISOString();
989+
await elementUpdated(spec.element);
990+
spec.submitFails();
991+
992+
spec.element.value = new Date(2022, 0, 1).toISOString();
993+
await elementUpdated(spec.element);
994+
spec.submitFails();
995+
996+
spec.element.value = new Date(2025, 0, 2).toISOString();
997+
await elementUpdated(spec.element);
998+
spec.submitValidates();
999+
});
1000+
1001+
it('should enforce max value constraint with string property', async () => {
1002+
spec.element.max = new Date(2020, 0, 1).toISOString();
1003+
spec.element.value = today.native;
1004+
await elementUpdated(spec.element);
1005+
spec.submitFails();
1006+
1007+
spec.element.value = new Date(2020, 0, 1).toISOString();
1008+
await elementUpdated(spec.element);
1009+
spec.submitValidates();
1010+
});
1011+
9621012
it('should invalidate the component if a disabled date is typed in the input', async () => {
9631013
const minDate = new Date(2024, 1, 1);
9641014
const maxDate = new Date(2024, 1, 28);

0 commit comments

Comments
 (0)