Skip to content

Commit 1fa3976

Browse files
authored
refactor: support string-based property values for date components (#1470)
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 date-bound component type declarations.
1 parent 6c0897d commit 1fa3976

File tree

12 files changed

+345
-126
lines changed

12 files changed

+345
-126
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: 28 additions & 26 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,24 +51,21 @@ 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;
59-
}
60-
6154
/* blazorSuppress */
6255
/**
6356
* The current value of the calendar.
6457
* Used when selection is set to single
6558
*
6659
* @attr value
6760
*/
68-
@property({ converter: dateFromISOString })
69-
public set value(value) {
70-
this._value = value ? CalendarDay.from(value) : undefined;
61+
@property({ converter: convertToDate })
62+
public set value(value: Date | string | null | undefined) {
63+
const converted = convertToDate(value);
64+
this._value = converted ? CalendarDay.from(converted) : null;
7165
}
7266

73-
public get values(): Date[] {
74-
return this._values ? this._values.map((v) => v.native) : [];
67+
public get value(): Date | null {
68+
return this._value ? this._value.native : null;
7569
}
7670

7771
/* blazorSuppress */
@@ -81,21 +75,29 @@ export class IgcCalendarBaseComponent extends LitElement {
8175
*
8276
* @attr values
8377
*/
84-
@property({ converter: datesFromISOStrings })
85-
public set values(values) {
86-
this._values = values ? values.map((v) => CalendarDay.from(v)) : [];
78+
@property({ converter: convertToDates })
79+
public set values(values: (Date | string)[] | string | null | undefined) {
80+
const converted = convertToDates(values);
81+
this._values = converted ? converted.map((v) => CalendarDay.from(v)) : [];
8782
}
8883

89-
public get activeDate(): Date {
90-
return this._activeDate.native;
84+
public get values(): Date[] {
85+
return this._values ? this._values.map((v) => v.native) : [];
9186
}
9287

9388
/* blazorSuppress */
9489
/** 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) {
90+
@property({ attribute: 'active-date', converter: convertToDate })
91+
public set activeDate(value: Date | string | null | undefined) {
9792
this._initialActiveDateSet = true;
98-
this._activeDate = value ? CalendarDay.from(value) : CalendarDay.today;
93+
const converted = convertToDate(value);
94+
this._activeDate = converted
95+
? CalendarDay.from(converted)
96+
: CalendarDay.today;
97+
}
98+
99+
public get activeDate(): Date {
100+
return this._activeDate.native;
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: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,19 @@ 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+
for (const each of [new Date('s'), '', null, undefined]) {
57+
calendar.value = each;
58+
expect(calendar.value).to.be.null;
59+
}
60+
});
61+
4962
it('setting `values` attribute', async () => {
5063
const date_1 = new CalendarDay({ year: 2022, month: 0, date: 19 });
5164
const date_2 = date_1.set({ date: 22 });
@@ -61,6 +74,57 @@ describe('Calendar interactions', () => {
6174
expect(date_2.equalTo(last(calendar.values))).to.be.true;
6275
});
6376

77+
it('setting `values` - string property binding', async () => {
78+
const date_1 = new CalendarDay({ year: 2022, month: 0, date: 19 });
79+
const date_2 = date_1.set({ date: 22 });
80+
81+
const date_1_str = date_1.native.toISOString();
82+
const date_2_str = date_2.native.toISOString();
83+
84+
calendar.selection = 'multiple';
85+
calendar.values = `${date_1_str}, ${date_2_str}`;
86+
87+
expect(calendar.values).lengthOf(2);
88+
expect(date_1.equalTo(first(calendar.values))).to.be.true;
89+
expect(date_2.equalTo(last(calendar.values))).to.be.true;
90+
91+
// Valid date combinations
92+
const validDates = [
93+
[date_1_str, date_2_str],
94+
[date_1.native, date_2.native],
95+
[date_1_str, date_2.native],
96+
];
97+
98+
for (const each of validDates) {
99+
calendar.values = each;
100+
expect(calendar.values).lengthOf(2);
101+
expect(date_1.equalTo(first(calendar.values))).to.be.true;
102+
expect(date_2.equalTo(last(calendar.values))).to.be.true;
103+
}
104+
105+
// Mixed date combinations
106+
calendar.values = [date_1.native, new Date(), new Date('s'), date_1_str];
107+
expect(calendar.values).lengthOf(3);
108+
109+
calendar.values = ['invalid', date_1_str, date_2_str, date_2.native];
110+
expect(calendar.values).lengthOf(3);
111+
112+
// Invalid date combinations
113+
const invalidDates = [
114+
'',
115+
null,
116+
undefined,
117+
[new Date('s'), 'abc'],
118+
'abcde, abcde',
119+
['a', 'b', 'c', new Date('invalid')],
120+
];
121+
122+
for (const each of invalidDates) {
123+
calendar.values = each;
124+
expect(calendar.values).is.empty;
125+
}
126+
});
127+
64128
it('clicking previous/next buttons in days view', async () => {
65129
const { previous, next } = getCalendarDOM(calendar).navigation;
66130

src/components/calendar/calendar.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -700,7 +700,7 @@ export default class IgcCalendarComponent extends EventEmitterMixin<
700700
}
701701

702702
this.emitEvent('igcChange', {
703-
detail: this._isSingle ? this.value : this.values,
703+
detail: this._isSingle ? (this.value as Date) : this.values,
704704
});
705705
}
706706

src/components/calendar/helpers.ts

Lines changed: 68 additions & 11 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,23 +37,79 @@ const DaysMap = {
3637

3738
/* Converter functions */
3839

39-
export function dateFromISOString(value: string | null) {
40-
return value ? new Date(value) : null;
40+
export function isValidDate(date: Date) {
41+
return Number.isNaN(date.valueOf()) ? null : date;
4142
}
4243

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;
44+
export function parseISODate(string: string) {
45+
if (/^\d{4}/.test(string)) {
46+
const time = !string.includes('T') ? 'T00:00:00' : '';
47+
return isValidDate(new Date(`${string}${time}`));
48+
}
49+
50+
if (/^\d{2}/.test(string)) {
51+
const date = first(new Date().toISOString().split('T'));
52+
return isValidDate(new Date(`${date}T${string}`));
53+
}
54+
55+
return null;
5156
}
5257

5358
/**
54-
* Returns the value of the selected/activated element (day/month/year) in the calendar view.
59+
* Converts the given value to a Date object.
60+
*
61+
* If the value is already a valid Date object, it is returned directly.
62+
* If the value is a string, it is parsed into a Date object.
63+
* If the value is null or undefined, null is returned.
64+
* If the parsing fails, null is returned.
65+
*/
66+
export function convertToDate(value?: Date | string | null): Date | null {
67+
if (!value) {
68+
return null;
69+
}
70+
71+
return isString(value) ? parseISODate(value) : isValidDate(value);
72+
}
73+
74+
/**
75+
* Converts a Date object to an ISO 8601 string.
76+
*
77+
* If the `value` is a `Date` object, it is converted to an ISO 8601 string.
78+
* If the `value` is null or undefined, null is returned.
79+
*/
80+
export function getDateFormValue(value: Date | null) {
81+
return value ? value.toISOString() : null;
82+
}
83+
84+
/**
85+
* Converts a comma-separated string of ISO 8601 dates or an array of Date objects | ISO 8601 strings into
86+
* an array of Date objects.
5587
*
88+
* If the `value` is null or undefined, null is returned.
89+
* If the `value` is an array of `Date` objects, a filtered array of valid `Date` objects is returned.
90+
* If the `value` is a string, it is split by commas and each part is parsed into a `Date` object.
91+
* If the parsing fails for any date, it is skipped.
92+
*/
93+
export function convertToDates(value?: (Date | string)[] | string | null) {
94+
if (!value) {
95+
return null;
96+
}
97+
98+
const values: Date[] = [];
99+
const iterator = isString(value) ? value.split(',') : value;
100+
101+
for (const each of iterator) {
102+
const date = convertToDate(isString(each) ? each.trim() : each);
103+
if (date) {
104+
values.push(date);
105+
}
106+
}
107+
108+
return values;
109+
}
110+
111+
/**
112+
* Returns the value of the selected/activated element (day/month/year) in the calendar view.
56113
*/
57114
export function getViewElement(event: Event) {
58115
const element = findElementFromEventPath<HTMLElement>('[data-value]', event);

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

Lines changed: 52 additions & 2 deletions
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>`
@@ -261,7 +286,7 @@ describe('Date picker', () => {
261286
it('should set the value trough attribute correctly', async () => {
262287
expect(picker.value).to.be.null;
263288
const expectedValue = new CalendarDay({ year: 2024, month: 2, date: 1 });
264-
picker.setAttribute('value', expectedValue.native.toDateString());
289+
picker.setAttribute('value', expectedValue.native.toISOString());
265290
await elementUpdated(picker);
266291

267292
checkDatesEqual(picker.value!, expectedValue);
@@ -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)