Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Added support for string-based property values for date based components #1470

Merged
merged 9 commits into from
Nov 19, 2024
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Changed
- Calendar - allow passing a string value to the backing `value`, `values` and `activeDate` properties [#1467](https://github.com/IgniteUI/igniteui-webcomponents/issues/1467)
- 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)
- 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)

## [5.1.2] - 2024-11-04
### Added
- Carousel component select method overload accepting index [#1457](https://github.com/IgniteUI/igniteui-webcomponents/issues/1457)
Expand Down
42 changes: 22 additions & 20 deletions src/components/calendar/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@ import { property, state } from 'lit/decorators.js';
import { blazorDeepImport } from '../common/decorators/blazorDeepImport.js';
import { blazorIndirectRender } from '../common/decorators/blazorIndirectRender.js';
import { watch } from '../common/decorators/watch.js';
import {
dateFromISOString,
datesFromISOStrings,
getWeekDayNumber,
} from './helpers.js';
import { first } from '../common/util.js';
import { convertToDate, convertToDates, getWeekDayNumber } from './helpers.js';
import { CalendarDay } from './model.js';
import type { DateRangeDescriptor, WeekDays } from './types.js';

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

protected get _hasValues() {
return this._values.length > 0;
return this._values && this._values.length > 0;
}

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

@state()
protected _value?: CalendarDay;
protected _value: CalendarDay | null = null;

@state()
protected _values: CalendarDay[] = [];
Expand All @@ -54,8 +51,8 @@ export class IgcCalendarBaseComponent extends LitElement {
@state()
protected _disabledDates: DateRangeDescriptor[] = [];

public get value(): Date | undefined {
return this._value ? this._value.native : undefined;
public get value(): Date | null {
return this._value ? this._value.native : null;
}

/* blazorSuppress */
Expand All @@ -65,9 +62,10 @@ export class IgcCalendarBaseComponent extends LitElement {
*
* @attr value
*/
@property({ converter: dateFromISOString })
public set value(value) {
this._value = value ? CalendarDay.from(value) : undefined;
@property({ converter: convertToDate })
public set value(value: Date | string | null) {
const converted = convertToDate(value);
this._value = converted ? CalendarDay.from(converted) : null;
}

public get values(): Date[] {
Expand All @@ -81,9 +79,10 @@ export class IgcCalendarBaseComponent extends LitElement {
*
* @attr values
*/
@property({ converter: datesFromISOStrings })
public set values(values) {
this._values = values ? values.map((v) => CalendarDay.from(v)) : [];
@property({ converter: convertToDates })
public set values(values: Date[] | string | null) {
const converted = convertToDates(values);
this._values = converted ? converted.map((v) => CalendarDay.from(v)) : [];
}

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

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

/**
Expand Down Expand Up @@ -154,7 +156,7 @@ export class IgcCalendarBaseComponent extends LitElement {
@watch('selection', { waitUntilFirstUpdate: true })
protected selectionChanged() {
this._rangePreviewDate = undefined;
this._value = undefined;
this._value = null;
this._values = [];
}

Expand All @@ -166,7 +168,7 @@ export class IgcCalendarBaseComponent extends LitElement {
if (this._isSingle) {
this.activeDate = this.value ?? this.activeDate;
} else {
this.activeDate = this.values[0] ?? this.activeDate;
this.activeDate = first(this.values) ?? this.activeDate;
}
}
}
30 changes: 30 additions & 0 deletions src/components/calendar/calendar.interaction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ describe('Calendar interactions', () => {
expect(date.equalTo(calendar.value!)).to.be.true;
});

it('setting `value` - string property binding', async () => {
const date = new CalendarDay({ year: 2022, month: 0, date: 19 });
calendar.value = date.native.toISOString();

expect(date.equalTo(calendar.value!)).to.be.true;

// Invalid date
calendar.value = new Date('s');
expect(calendar.value).to.be.null;
});

it('setting `values` attribute', async () => {
const date_1 = new CalendarDay({ year: 2022, month: 0, date: 19 });
const date_2 = date_1.set({ date: 22 });
Expand All @@ -61,6 +72,25 @@ describe('Calendar interactions', () => {
expect(date_2.equalTo(last(calendar.values))).to.be.true;
});

it('setting `values` - string property binding', async () => {
const date_1 = new CalendarDay({ year: 2022, month: 0, date: 19 });
const date_2 = date_1.set({ date: 22 });

calendar.selection = 'multiple';
calendar.values = `${date_1.native.toISOString()}, ${date_2.native.toISOString()}`;

expect(calendar.values).lengthOf(2);
expect(date_1.equalTo(first(calendar.values))).to.be.true;
expect(date_2.equalTo(last(calendar.values))).to.be.true;

// Invalid dates
calendar.values = 'nope, nope again';
expect(calendar.values).is.empty;

calendar.values = '';
rkaraivanov marked this conversation as resolved.
Show resolved Hide resolved
expect(calendar.values).is.empty;
});

it('clicking previous/next buttons in days view', async () => {
const { previous, next } = getCalendarDOM(calendar).navigation;

Expand Down
102 changes: 92 additions & 10 deletions src/components/calendar/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
asNumber,
findElementFromEventPath,
first,
isString,
last,
modulo,
} from '../common/util.js';
Expand Down Expand Up @@ -36,18 +37,99 @@ const DaysMap = {

/* Converter functions */

export function dateFromISOString(value: string | null) {
return value ? new Date(value) : null;
/**
* Converts the given value to a Date object.
*
* If the value is already a valid Date object, it is returned directly.
* If the value is a string, it is parsed into a Date object.
* If the value is null or undefined, null is returned.
* If the parsing fails, null is returned.
*
* @param value The value to convert.
* @returns The converted Date object, or null if the conversion fails.
*
* @example
* ```typescript
* const dateString = '2023-11-11T12:34:56Z';
* const dateObject = new Date('2023-11-11T12:34:56Z');
* const nullValue = null;

* const result1 = convertToDate(dateString); // Date object
* const result2 = convertToDate(dateObject); // Date object
* const result3 = convertToDate(nullValue); // null
* const result4 = convertToDate('invalid-date-string'); // null
* ```
*/
rkaraivanov marked this conversation as resolved.
Show resolved Hide resolved
export function convertToDate(value: Date | string | null): Date | null {
if (!value) {
return null;
}

const converted = isString(value) ? new Date(value) : value;
return Number.isNaN(converted.valueOf()) ? null : converted;
}

/**
* Converts a Date object to an ISO 8601 string.
*
* If the `value` is a `Date` object, it is converted to an ISO 8601 string.
* If the `value` is null or undefined, null is returned.
*
* @param value The Date object to convert.
* @returns The ISO 8601 string representation of the Date object, or null if the value is null or undefined.
*
* @example
* ```typescript
* const dateObject = new Date('2023-11-11T12:34:56Z');
* const nullValue = null;

* const result1 = getDateFormValue(dateObject); // "2023-11-11T12:34:56.000Z"
* const result2 = getDateFormValue(nullValue); // null
* ```
*/
export function getDateFormValue(value: Date | null) {
return value ? value.toISOString() : null;
}

export function datesFromISOStrings(value: string | null) {
return value
? value
.split(',')
.map((v) => v.trim())
.filter((v) => v)
.map((v) => new Date(v))
: null;
/**
* Converts an array of Date objects or a comma-separated string of ISO 8601 dates into an array of Date objects.

* If the `value` is an array of `Date` objects, it is returned directly.
* If the `value` is a string, it is split by commas and each part is parsed into a `Date` object.
* If the `value` is null or undefined, null is returned.
* If the parsing fails for any date, it is skipped.

* @param value The value to convert.
* @returns An array of Date objects, or null if the conversion fails for all values.
rkaraivanov marked this conversation as resolved.
Show resolved Hide resolved

* @example
* ```typescript
* const dateStrings = '2023-11-11T12:34:56Z,2023-12-12T13:45:00Z';
* const dateObjects = [new Date('2023-11-11T12:34:56Z'), new Date('2023-12-12T13:45:00Z')];
* const nullValue = null;

* const result1 = convertToDates(dateStrings); // [Date, Date]
* const result2 = convertToDates(dateObjects); // [Date, Date]
* const result3 = convertToDates(nullValue); // null
* const result4 = convertToDates('invalid-date-string,2023-11-11T12:34:56Z'); // [Date]
* ```
*/
export function convertToDates(value: Date[] | string | null) {
if (!value) {
return null;
}

const values: Date[] = [];
const iterator = isString(value) ? value.split(',') : value;

for (const each of iterator) {
const date = convertToDate(isString(each) ? each.trim() : each);
if (date) {
values.push(date);
}
}

return values;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/components/calendar/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ export type WeekDays =
| 'saturday';

export interface IgcCalendarComponentEventMap {
igcChange: CustomEvent<Date | Date[]>;
igcChange: CustomEvent<Date | Date[] | null>;
rkaraivanov marked this conversation as resolved.
Show resolved Hide resolved
}
52 changes: 51 additions & 1 deletion src/components/date-picker/date-picker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,31 @@ describe('Date picker', () => {
checkDatesEqual(dateTimeInput.value!, expectedValue);
});

it('should be successfully initialized with a string property binding - issue 1467', async () => {
const value = new CalendarDay({ year: 2000, month: 0, date: 25 });
picker = await fixture<IgcDatePickerComponent>(html`
<igc-date-picker .value=${value.native.toISOString()}></igc-date-picker>
`);

expect(CalendarDay.from(picker.value!).equalTo(value)).to.be.true;
});

it('should not set an invalid date object as a value', async () => {
picker = await fixture<IgcDatePickerComponent>(html`
<igc-date-picker value="invalid date"></igc-date-picker>
`);

expect(picker.value).to.be.null;
});

it('should not set an invalid date object as a value through property binding', async () => {
picker = await fixture<IgcDatePickerComponent>(html`
<igc-date-picker .value=${new Date('s')}></igc-date-picker>
`);

expect(picker.value).to.be.null;
});

it('should be successfully initialized in open state in dropdown mode', async () => {
picker = await fixture<IgcDatePickerComponent>(
html`<igc-date-picker open></igc-date-picker>`
Expand Down Expand Up @@ -455,7 +480,7 @@ describe('Date picker', () => {
checkDatesEqual(picker.activeDate, currentDate);
expect(picker.value).to.be.null;
checkDatesEqual(calendar.activeDate, currentDate);
expect(calendar.value).to.be.undefined;
expect(calendar.value).to.be.null;
});

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

it('should enforce min value constraint with string property', async () => {
spec.element.min = new Date(2025, 0, 1).toISOString();
await elementUpdated(spec.element);
spec.submitFails();

spec.element.value = new Date(2022, 0, 1).toISOString();
await elementUpdated(spec.element);
spec.submitFails();

spec.element.value = new Date(2025, 0, 2).toISOString();
await elementUpdated(spec.element);
spec.submitValidates();
});

it('should enforce max value constraint with string property', async () => {
spec.element.max = new Date(2020, 0, 1).toISOString();
spec.element.value = today.native;
await elementUpdated(spec.element);
spec.submitFails();

spec.element.value = new Date(2020, 0, 1).toISOString();
await elementUpdated(spec.element);
spec.submitValidates();
});

it('should invalidate the component if a disabled date is typed in the input', async () => {
const minDate = new Date(2024, 1, 1);
const maxDate = new Date(2024, 1, 28);
Expand Down
Loading