diff --git a/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.html b/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.html
index 23217f01ab..1d49c59a9b 100644
--- a/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.html
+++ b/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.html
@@ -10,66 +10,78 @@
>
{{ property.label | translate }}
-
-
-
- @if (showProperty) {
- {{ property.displayValue }}
- } @else {
- {{ property.default | translate }}
- }
+ @if (property.allowManualInput) {
+
+ } @else {
+
+
+
+ @if (showProperty) {
+ {{ property.displayValue }}
+ } @else {
+ {{ property.default | translate }}
+ }
+
-
-
-
- @if (showClearAction) {
-
- }
-
-
-
+
+ }
+ @if (showClearAction) {
+
+ }
+
+
+
} @else {
diff --git a/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.spec.ts b/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.spec.ts
index 7406d16108..08f130bb58 100644
--- a/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.spec.ts
+++ b/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.spec.ts
@@ -22,12 +22,15 @@ import { CardViewDateItemComponent } from './card-view-dateitem.component';
import { ClipboardService } from '../../../clipboard/clipboard.service';
import { CardViewDatetimeItemModel } from '../../models/card-view-datetimeitem.model';
import { AppConfigService } from '../../../app-config/app-config.service';
-import { MatDatetimepickerInputEvent } from '@mat-datetimepicker/core';
+import { DatetimeAdapter, MatDatetimepickerInputEvent } from '@mat-datetimepicker/core';
+import { DateAdapter } from '@angular/material/core';
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { addMinutes } from 'date-fns';
import { UnitTestingUtils } from '../../../testing/unit-testing-utils';
import { MatFormField } from '@angular/material/form-field';
+import { AdfDateFnsAdapter } from '../../../common/utils/date-fns-adapter';
+import { AdfDateTimeFnsAdapter } from '../../../common/utils/datetime-fns-adapter';
describe('CardViewDateItemComponent', () => {
let loader: HarnessLoader;
@@ -302,6 +305,18 @@ describe('CardViewDateItemComponent', () => {
expect(component.property.value).toBeNull();
expect(component.property.default).toBeNull();
});
+
+ it('should not touch the form control when onDateClear is called and allowManualInput is false', () => {
+ component.editable = true;
+ component.property.editable = true;
+ component.property.value = new Date('Jul 10 2017');
+ fixture.detectChanges();
+
+ const spy = spyOn(component.cardViewDateTimeControl, 'setValue');
+ component.onDateClear();
+
+ expect(spy).not.toHaveBeenCalled();
+ });
});
it('should be possible update a date-time', async () => {
@@ -360,6 +375,581 @@ describe('CardViewDateItemComponent', () => {
expect(await chips[2].getText()).toBe('Jul 12, 2017, 0:01');
});
+ describe('allowManualInput', () => {
+ beforeEach(() => {
+ fixture.componentRef.setInput(
+ 'property',
+ new CardViewDateItemModel({
+ label: 'Date label',
+ value: new Date('07/10/2017'),
+ key: 'dateKey',
+ default: '',
+ format: 'yyyy-MM-dd',
+ editable: true,
+ allowManualInput: true
+ })
+ );
+ fixture.componentRef.setInput('editable', true);
+ });
+
+ it('should render a visible typeable input when allowManualInput is true', () => {
+ fixture.detectChanges();
+
+ const manualInput = testingUtils.getByDataAutomationId('datepicker-manual-input-dateKey');
+ expect(manualInput).not.toBeNull();
+ const invisibleInput = fixture.nativeElement.querySelector('.adf-invisible-date-input');
+ expect(invisibleInput).toBeNull();
+ });
+
+ it('should NOT render the span-button when allowManualInput is true', () => {
+ fixture.detectChanges();
+
+ const spanButton = testingUtils.getByDataAutomationId('datepicker-label-toggle-dateKey');
+ expect(spanButton).toBeNull();
+ });
+
+ it('should render the invisible input and span-button when allowManualInput is false', () => {
+ component.property = new CardViewDateItemModel({
+ label: 'Date label',
+ value: new Date('07/10/2017'),
+ key: 'dateKey',
+ default: '',
+ format: '',
+ editable: true,
+ allowManualInput: false
+ });
+ fixture.detectChanges();
+
+ const invisibleInput = fixture.nativeElement.querySelector('.adf-invisible-date-input');
+ expect(invisibleInput).not.toBeNull();
+ const spanButton = testingUtils.getByDataAutomationId('datepicker-label-toggle-dateKey');
+ expect(spanButton).not.toBeNull();
+ });
+
+ it('should still render the datepicker toggle and picker when allowManualInput is true', () => {
+ fixture.detectChanges();
+
+ const datePickerToggle = testingUtils.getByDataAutomationId(`datepickertoggle-${component.property.key}`);
+ const datePicker = testingUtils.getByDataAutomationId(`datepicker-${component.property.key}`);
+ expect(datePickerToggle).not.toBeNull();
+ expect(datePicker).not.toBeNull();
+ });
+
+ it('should still render the clear icon when allowManualInput is true and value exists', () => {
+ fixture.detectChanges();
+
+ const clearIcon = testingUtils.getByDataAutomationId(`datepicker-date-clear-${component.property.key}`);
+ expect(clearIcon).not.toBeNull();
+ });
+
+ it('should update the property when a date is changed via the picker', () => {
+ const cardViewUpdateService = TestBed.inject(CardViewUpdateService);
+ const itemUpdatedSpy = spyOn(cardViewUpdateService.itemUpdated$, 'next');
+ fixture.detectChanges();
+
+ const expectedDate = new Date('Jul 10 2018');
+ component.onDateChanged({ value: addMinutes(expectedDate, expectedDate.getTimezoneOffset()) } as MatDatetimepickerInputEvent);
+
+ expect(itemUpdatedSpy).toHaveBeenCalled();
+ });
+
+ it('should sync the form control value on init', () => {
+ fixture.detectChanges();
+
+ expect(component.cardViewDateTimeControl.value).not.toBeNull();
+ });
+
+ it('should clear the form control value when onDateClear is called', () => {
+ fixture.detectChanges();
+
+ component.onDateClear();
+ expect(component.cardViewDateTimeControl.value).toBeNull();
+ });
+
+ it('should disable the form control when property.editable is false', () => {
+ component.property = new CardViewDateItemModel({
+ label: 'Date label',
+ value: new Date('07/10/2017'),
+ key: 'dateKey',
+ default: '',
+ format: 'yyyy-MM-dd',
+ editable: false,
+ allowManualInput: true
+ });
+ component.editable = true;
+ fixture.detectChanges();
+
+ expect(component.cardViewDateTimeControl.disabled).toBeTrue();
+ });
+
+ it('should disable the form control when component editable is false', () => {
+ component.editable = false;
+ fixture.detectChanges();
+
+ expect(component.cardViewDateTimeControl.disabled).toBeTrue();
+ });
+
+ it('should disable the form control when editable input changes to false after init', () => {
+ fixture.detectChanges();
+ expect(component.cardViewDateTimeControl.disabled).toBeFalse();
+
+ fixture.componentRef.setInput('editable', false);
+ fixture.detectChanges();
+
+ expect(component.cardViewDateTimeControl.disabled).toBeTrue();
+ });
+
+ it('should re-enable the form control when editable input changes back to true after being disabled', () => {
+ fixture.componentRef.setInput('editable', false);
+ fixture.detectChanges();
+ expect(component.cardViewDateTimeControl.disabled).toBeTrue();
+
+ fixture.componentRef.setInput('editable', true);
+ fixture.detectChanges();
+
+ expect(component.cardViewDateTimeControl.disabled).toBeFalse();
+ });
+
+ it('should disable the form control when property changes to non-editable after init', () => {
+ fixture.detectChanges();
+ expect(component.cardViewDateTimeControl.disabled).toBeFalse();
+
+ fixture.componentRef.setInput(
+ 'property',
+ new CardViewDateItemModel({
+ label: 'Date label',
+ value: new Date('07/10/2017'),
+ key: 'dateKey',
+ default: '',
+ format: 'yyyy-MM-dd',
+ editable: false,
+ allowManualInput: true
+ })
+ );
+ fixture.detectChanges();
+
+ expect(component.cardViewDateTimeControl.disabled).toBeTrue();
+ });
+
+ it('should have null form control value when property has no value', () => {
+ component.property = new CardViewDateItemModel({
+ label: 'Date label',
+ value: null,
+ key: 'dateKey',
+ default: '',
+ format: 'yyyy-MM-dd',
+ editable: true,
+ allowManualInput: true
+ });
+ component.editable = true;
+ fixture.detectChanges();
+
+ expect(component.cardViewDateTimeControl.value).toBeNull();
+ });
+
+ it('should render placeholder with default value when property has default', () => {
+ component.property = new CardViewDateItemModel({
+ label: 'Date label',
+ value: null,
+ key: 'dateKey',
+ default: 'Select a date',
+ format: 'yyyy-MM-dd',
+ editable: true,
+ allowManualInput: true
+ });
+ component.editable = true;
+ fixture.detectChanges();
+
+ const manualInput = testingUtils.getByDataAutomationId('datepicker-manual-input-dateKey');
+ expect(manualInput.nativeElement.placeholder).toContain('Select a date');
+ });
+
+ it('should render empty placeholder when property has no default', () => {
+ fixture.detectChanges();
+
+ const manualInput = testingUtils.getByDataAutomationId('datepicker-manual-input-dateKey');
+ expect(manualInput.nativeElement.placeholder).toBe('');
+ });
+
+ it('should sync form control value with valueDate when date is changed via picker', () => {
+ fixture.detectChanges();
+
+ const expectedDate = new Date('Jul 10 2018');
+ component.onDateChanged({ value: addMinutes(expectedDate, expectedDate.getTimezoneOffset()) } as MatDatetimepickerInputEvent);
+
+ expect(component.cardViewDateTimeControl.value).not.toBeNull();
+ expect(component.cardViewDateTimeControl.value instanceof Date).toBeTrue();
+ });
+
+ it('should re-sync form control value when property input is replaced after init', () => {
+ fixture.detectChanges();
+
+ const newDate = new Date('2020-03-15');
+ fixture.componentRef.setInput(
+ 'property',
+ new CardViewDateItemModel({
+ label: 'Date label',
+ value: newDate,
+ key: 'dateKey',
+ format: 'yyyy-MM-dd',
+ editable: true,
+ allowManualInput: true
+ })
+ );
+ fixture.detectChanges();
+
+ expect(component.cardViewDateTimeControl.value).not.toBeNull();
+ expect(component.cardViewDateTimeControl.value.toDateString()).toBe(newDate.toDateString());
+ });
+
+ it('should set form control to null when replacement property has no value', () => {
+ fixture.detectChanges();
+
+ fixture.componentRef.setInput(
+ 'property',
+ new CardViewDateItemModel({
+ label: 'Date label',
+ value: null,
+ key: 'dateKey',
+ format: 'yyyy-MM-dd',
+ editable: true,
+ allowManualInput: true
+ })
+ );
+ fixture.detectChanges();
+
+ expect(component.cardViewDateTimeControl.value).toBeNull();
+ });
+
+ it('should re-sync form control and valueDate when updateItem$ emits for matching key', () => {
+ fixture.detectChanges();
+ const cardViewUpdateService = TestBed.inject(CardViewUpdateService);
+ const newDate = new Date('2021-06-01');
+
+ cardViewUpdateService.updateItem$.next({ key: 'dateKey', value: newDate } as any);
+ fixture.detectChanges();
+
+ expect(component.valueDate).not.toBeNull();
+ expect(component.cardViewDateTimeControl.value).not.toBeNull();
+ });
+
+ it('should not re-sync form control when updateItem$ emits for a different key', () => {
+ fixture.detectChanges();
+ const originalControlValue = component.cardViewDateTimeControl.value;
+ const cardViewUpdateService = TestBed.inject(CardViewUpdateService);
+
+ cardViewUpdateService.updateItem$.next({ key: 'otherKey', value: new Date('2021-06-01') } as any);
+ fixture.detectChanges();
+
+ expect(component.cardViewDateTimeControl.value).toEqual(originalControlValue);
+ });
+ });
+
+ describe('format changes', () => {
+ it('should re-apply format when property.format changes with allowManualInput', () => {
+ component.property = new CardViewDateItemModel({
+ label: 'Date label',
+ value: new Date('07/10/2017'),
+ key: 'dateKey',
+ format: 'dd/MM/yyyy',
+ editable: true,
+ allowManualInput: true
+ });
+ component.editable = true;
+ fixture.detectChanges();
+
+ const datetimeAdapter = fixture.debugElement.injector.get(DatetimeAdapter) as AdfDateTimeFnsAdapter;
+ expect(datetimeAdapter.displayFormat).toBe('dd/MM/yyyy');
+
+ component.property.format = 'yyyy-MM-dd';
+ fixture.detectChanges();
+
+ expect(datetimeAdapter.displayFormat).toBe('yyyy-MM-dd');
+ });
+
+ it('should not re-apply format when property.format has not changed', () => {
+ component.property = new CardViewDateItemModel({
+ label: 'Date label',
+ value: new Date('07/10/2017'),
+ key: 'dateKey',
+ format: 'dd/MM/yyyy',
+ editable: true,
+ allowManualInput: true
+ });
+ component.editable = true;
+ fixture.detectChanges();
+
+ const datetimeAdapter = fixture.debugElement.injector.get(DatetimeAdapter) as AdfDateTimeFnsAdapter;
+ const formatBefore = datetimeAdapter.displayFormat;
+ fixture.detectChanges();
+
+ expect(datetimeAdapter.displayFormat).toBe(formatBefore);
+ });
+
+ it('should reset adapters to defaults when custom format is removed in manual input mode', () => {
+ component.property = new CardViewDateItemModel({
+ label: 'Date label',
+ value: new Date('07/10/2017'),
+ key: 'dateKey',
+ format: 'dd/MM/yyyy',
+ editable: true,
+ allowManualInput: true
+ });
+ component.editable = true;
+ fixture.detectChanges();
+
+ const datetimeAdapter = fixture.debugElement.injector.get(DatetimeAdapter) as AdfDateTimeFnsAdapter;
+ const dateAdapter = fixture.debugElement.injector.get(DateAdapter) as AdfDateFnsAdapter;
+ expect(datetimeAdapter.displayFormat).toBe('dd/MM/yyyy');
+ expect(dateAdapter.displayFormat).toBe('dd/MM/yyyy');
+
+ component.property.format = '';
+ fixture.detectChanges();
+
+ expect(datetimeAdapter.displayFormat).toBeNull();
+ expect(dateAdapter.displayFormat).toBe('MMM dd');
+ });
+
+ it('should reset datetime adapter when custom format is removed for datetime property in manual input mode', () => {
+ component.property = new CardViewDatetimeItemModel({
+ label: 'Datetime label',
+ value: new Date('07/10/2017 10:15'),
+ key: 'datetimeKey',
+ format: 'yyyy-MM-dd HH:mm',
+ editable: true,
+ allowManualInput: true
+ });
+ component.editable = true;
+ fixture.detectChanges();
+
+ const datetimeAdapter = fixture.debugElement.injector.get(DatetimeAdapter) as AdfDateTimeFnsAdapter;
+ expect(datetimeAdapter.displayFormat).toBe('yyyy-MM-dd HH:mm');
+
+ component.property.format = '';
+ fixture.detectChanges();
+
+ expect(datetimeAdapter.displayFormat).toBeNull();
+ });
+
+ it('should fallback to adapter defaults when allowManualInput is false', () => {
+ component.property = new CardViewDateItemModel({
+ label: 'Date label',
+ value: new Date('07/10/2017'),
+ key: 'dateKey',
+ format: 'YYYY-MM-dd',
+ editable: true,
+ allowManualInput: false
+ });
+ component.editable = true;
+ fixture.detectChanges();
+
+ const dateAdapter = fixture.debugElement.injector.get(DateAdapter) as AdfDateFnsAdapter;
+ const datetimeAdapter = fixture.debugElement.injector.get(DatetimeAdapter) as AdfDateTimeFnsAdapter;
+
+ expect(dateAdapter.displayFormat).toBe('MMM dd');
+ expect(datetimeAdapter.displayFormat).toBeNull();
+ });
+
+ it('should re-set form control value when format changes and control has value', () => {
+ component.property = new CardViewDateItemModel({
+ label: 'Date label',
+ value: new Date('07/10/2017'),
+ key: 'dateKey',
+ format: 'dd/MM/yyyy',
+ editable: true,
+ allowManualInput: true
+ });
+ component.editable = true;
+ fixture.detectChanges();
+
+ const spy = spyOn(component.cardViewDateTimeControl, 'setValue');
+ component.property.format = 'yyyy-MM-dd';
+ fixture.detectChanges();
+
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it('should not re-set form control value when format changes but allowManualInput is false', () => {
+ component.property = new CardViewDateItemModel({
+ label: 'Date label',
+ value: new Date('07/10/2017'),
+ key: 'dateKey',
+ format: 'dd/MM/yyyy',
+ editable: true,
+ allowManualInput: false
+ });
+ component.editable = true;
+ fixture.detectChanges();
+
+ const spy = spyOn(component.cardViewDateTimeControl, 'setValue');
+ component.property.format = 'yyyy-MM-dd';
+ fixture.detectChanges();
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('CardViewDateItemModel allowManualInput', () => {
+ it('should default allowManualInput to false when not specified', () => {
+ const model = new CardViewDateItemModel({
+ label: 'Date label',
+ value: new Date(),
+ key: 'dateKey'
+ });
+ expect(model.allowManualInput).toBeFalse();
+ });
+
+ it('should set allowManualInput to true when specified', () => {
+ const model = new CardViewDateItemModel({
+ label: 'Date label',
+ value: new Date(),
+ key: 'dateKey',
+ allowManualInput: true
+ });
+ expect(model.allowManualInput).toBeTrue();
+ });
+
+ it('should keep allowManualInput false when explicitly set to false', () => {
+ const model = new CardViewDateItemModel({
+ label: 'Date label',
+ value: new Date(),
+ key: 'dateKey',
+ allowManualInput: false
+ });
+ expect(model.allowManualInput).toBeFalse();
+ });
+
+ it('should emit when format changes', () => {
+ const model = new CardViewDateItemModel({
+ label: 'Date label',
+ value: new Date(),
+ key: 'dateKey',
+ format: 'dd/MM/yyyy'
+ });
+ const formatChangeSpy = jasmine.createSpy('formatChangeSpy');
+ model.formatChanges$.subscribe(formatChangeSpy);
+
+ model.format = 'yyyy-MM-dd';
+
+ expect(formatChangeSpy).toHaveBeenCalledOnceWith('yyyy-MM-dd');
+ });
+
+ it('should not emit when format is assigned the same value', () => {
+ const model = new CardViewDateItemModel({
+ label: 'Date label',
+ value: new Date(),
+ key: 'dateKey',
+ format: 'dd/MM/yyyy'
+ });
+ const formatChangeSpy = jasmine.createSpy('formatChangeSpy');
+ model.formatChanges$.subscribe(formatChangeSpy);
+
+ model.format = 'dd/MM/yyyy';
+
+ expect(formatChangeSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('property input replacement', () => {
+ it('should re-sync valueDate when property input is replaced after init', () => {
+ fixture.componentRef.setInput(
+ 'property',
+ new CardViewDateItemModel({
+ label: 'Date label',
+ value: new Date('07/10/2017'),
+ key: 'dateKey',
+ format: '',
+ editable: true
+ })
+ );
+ fixture.detectChanges();
+
+ const newDate = new Date('2022-11-20');
+ fixture.componentRef.setInput(
+ 'property',
+ new CardViewDateItemModel({
+ label: 'Date label',
+ value: newDate,
+ key: 'dateKey',
+ format: '',
+ editable: true
+ })
+ );
+ fixture.detectChanges();
+
+ expect(component.valueDate).not.toBeNull();
+ expect(component.valueDate instanceof Date).toBeTrue();
+ });
+
+ it('should set valueDate to null when replacement property has no value', () => {
+ fixture.componentRef.setInput(
+ 'property',
+ new CardViewDateItemModel({
+ label: 'Date label',
+ value: new Date('07/10/2017'),
+ key: 'dateKey',
+ format: '',
+ editable: true
+ })
+ );
+ fixture.detectChanges();
+
+ fixture.componentRef.setInput(
+ 'property',
+ new CardViewDateItemModel({
+ label: 'Date label',
+ value: null,
+ key: 'dateKey',
+ format: '',
+ editable: true
+ })
+ );
+ fixture.detectChanges();
+
+ expect(component.valueDate).toBeNull();
+ });
+ });
+
+ describe('updateItem$ re-sync for multivalued', () => {
+ it('should re-sync valueDate when updateItem$ emits for a multivalued property', () => {
+ component.property = new CardViewDateItemModel({
+ label: 'Date label',
+ value: ['2020-01-01', '2020-02-01'],
+ key: 'dateKey',
+ editable: true,
+ multivalued: true
+ });
+ fixture.detectChanges();
+ const cardViewUpdateService = TestBed.inject(CardViewUpdateService);
+ const newDates = [new Date('2021-06-01'), new Date('2021-07-01')];
+
+ component.property.value = newDates;
+ cardViewUpdateService.updateItem$.next({ key: 'dateKey', value: newDates } as any);
+ fixture.detectChanges();
+
+ expect(component.valueDate).not.toBeNull();
+ expect(component.valueDate instanceof Date).toBeTrue();
+ });
+
+ it('should not update valueDate when updateItem$ emits for a different key on multivalued property', () => {
+ component.property = new CardViewDateItemModel({
+ label: 'Date label',
+ value: ['2020-01-01'],
+ key: 'dateKey',
+ editable: true,
+ multivalued: true
+ });
+ fixture.detectChanges();
+ const originalValueDate = component.valueDate;
+ const cardViewUpdateService = TestBed.inject(CardViewUpdateService);
+
+ cardViewUpdateService.updateItem$.next({ key: 'otherKey', value: [new Date('2021-06-01')] } as any);
+ fixture.detectChanges();
+
+ expect(component.valueDate).toEqual(originalValueDate);
+ });
+ });
+
describe('FloatLabel behavior', () => {
it('should set floatLabel to "always" when property has default value and is editable', async () => {
component.property = new CardViewDateItemModel({
diff --git a/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.ts b/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.ts
index 4f77c1e698..3a3fb6cab4 100644
--- a/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.ts
+++ b/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.ts
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-import { Component, effect, Input, OnInit, ViewChild, ViewEncapsulation, inject } from '@angular/core';
+import { Component, OnChanges, effect, Input, OnInit, SimpleChanges, ViewChild, ViewEncapsulation, inject, DestroyRef } from '@angular/core';
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
import {
DatetimeAdapter,
@@ -42,6 +42,10 @@ import { MatSnackBarModule } from '@angular/material/snack-bar';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { IconModule } from '../../../icon/icon.module';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { BehaviorSubject, EMPTY, filter, switchMap } from 'rxjs';
+
+const DEFAULT_DATE_FORMAT = 'MMM DD';
@Component({
providers: [
@@ -68,11 +72,13 @@ import { IconModule } from '../../../icon/icon.module';
encapsulation: ViewEncapsulation.None,
host: { class: 'adf-card-view-dateitem' }
})
-export class CardViewDateItemComponent extends BaseCardView implements OnInit {
+export class CardViewDateItemComponent extends BaseCardView implements OnInit, OnChanges {
private readonly dateAdapter = inject>(DateAdapter);
+ private readonly datetimeAdapter = inject>(DatetimeAdapter);
private readonly userPreferencesService = inject(UserPreferencesService);
private readonly clipboardService = inject(ClipboardService);
private readonly translateService = inject(TranslationService);
+ private readonly destroyRef = inject(DestroyRef);
@Input()
displayEmpty = true;
@@ -87,6 +93,8 @@ export class CardViewDateItemComponent extends BaseCardView = new FormControl(null);
+ private readonly property$ = new BehaviorSubject(undefined);
+
constructor() {
super();
// Use effect to react to locale signal changes (must be in injection context)
@@ -95,8 +103,49 @@ export class CardViewDateItemComponent extends BaseCardView prop?.formatChanges$ ?? EMPTY),
+ takeUntilDestroyed(this.destroyRef)
+ )
+ .subscribe(() => this.handleFormatChange());
+
+ this.cardViewUpdateService.updateItem$
+ .pipe(
+ filter((itemModel) => itemModel.key === this.property.key),
+ takeUntilDestroyed(this.destroyRef)
+ )
+ .subscribe(() => {
+ if (!this.property.multivalued) {
+ this.initSingleValueProperty();
+ } else {
+ this.initMultivaluedProperty();
+ }
+ });
+
+ this.applyFormat();
if (this.property.multivalued) {
this.initMultivaluedProperty();
@@ -105,6 +154,16 @@ export class CardViewDateItemComponent extends BaseCardView {
+ let properties: CardViewDateItemProperties;
+
+ beforeEach(() => {
+ properties = {
+ label: 'Date label',
+ value: new Date('2025-03-07T15:30:00.000Z'),
+ key: 'dateKey'
+ };
+ });
+
+ it('should initialize with date type and default allowManualInput', () => {
+ const itemModel = new CardViewDateItemModel(properties);
+
+ expect(itemModel.type).toBe('date');
+ expect(itemModel.allowManualInput).toBeFalse();
+ });
+
+ it('should initialize format, locale and allowManualInput from properties', () => {
+ const itemModel = new CardViewDateItemModel({
+ ...properties,
+ format: 'fullDate',
+ locale: 'fr',
+ allowManualInput: true
+ });
+
+ expect(itemModel.format).toBe('fullDate');
+ expect(itemModel.locale).toBe('fr');
+ expect(itemModel.allowManualInput).toBeTrue();
+ });
+
+ describe('displayValue', () => {
+ it('should prepare and transform a single date value', () => {
+ const itemModel = new CardViewDateItemModel(properties);
+ const preparedDate = new Date('2025-03-07T00:00:00.000');
+
+ spyOn(DateFnsUtils, 'forceLocal').and.returnValue(preparedDate);
+ spyOn(itemModel, 'transformDate').and.returnValue('formatted-date');
+
+ expect(itemModel.displayValue).toBe('formatted-date');
+ expect(DateFnsUtils.forceLocal).toHaveBeenCalledOnceWith(properties.value as Date);
+ expect(itemModel.transformDate).toHaveBeenCalledOnceWith(preparedDate);
+ });
+
+ it('should prepare and transform each value when multivalued', () => {
+ const firstDate = new Date('2025-03-07T15:30:00.000Z');
+ const secondDate = new Date('2025-03-08T15:30:00.000Z');
+ const itemModel = new CardViewDateItemModel({
+ ...properties,
+ multivalued: true,
+ value: [firstDate, secondDate]
+ });
+
+ spyOn(DateFnsUtils, 'forceLocal').and.callFake((date) => date as Date);
+ spyOn(itemModel, 'transformDate').and.callFake((date) => `${(date as Date).toISOString()}-formatted`);
+
+ expect(itemModel.displayValue).toEqual([`${firstDate.toISOString()}-formatted`, `${secondDate.toISOString()}-formatted`]);
+ expect(DateFnsUtils.forceLocal).toHaveBeenCalledTimes(2);
+ });
+
+ it('should return the default value when the single value is not present', () => {
+ const itemModel = new CardViewDateItemModel({
+ ...properties,
+ value: null,
+ default: 'No date selected'
+ });
+
+ expect(itemModel.displayValue).toBe('No date selected');
+ });
+
+ it('should return the default value as an array when the multivalued value is not present', () => {
+ const itemModel = new CardViewDateItemModel({
+ ...properties,
+ multivalued: true,
+ value: null,
+ default: 'No dates selected'
+ });
+
+ expect(itemModel.displayValue).toEqual(['No dates selected']);
+ });
+
+ it('should return an empty array when the multivalued value and default are not present', () => {
+ const itemModel = new CardViewDateItemModel({
+ ...properties,
+ multivalued: true,
+ value: null
+ });
+
+ expect(itemModel.displayValue).toEqual([]);
+ });
+ });
+
+ describe('transformDate', () => {
+ it('should use the provided format and locale', () => {
+ const value = new Date('2025-03-07T15:30:00.000Z');
+ const itemModel = new CardViewDateItemModel({
+ ...properties,
+ format: 'fullDate',
+ locale: 'fr'
+ });
+ const expectedValue = new DatePipe('fr').transform(value, 'fullDate');
+
+ expect(itemModel.transformDate(value)).toBe(expectedValue as string);
+ });
+
+ it('should use mediumDate and en-US when format and locale are not provided', () => {
+ const value = new Date('2025-03-07T15:30:00.000Z');
+ const itemModel = new CardViewDateItemModel(properties);
+ const expectedValue = new DatePipe('en-US').transform(value, 'mediumDate');
+
+ expect(itemModel.transformDate(value)).toBe(expectedValue as string);
+ });
+ });
+
+ describe('formatChanges$', () => {
+ it('should emit when format changes', () => {
+ const itemModel = new CardViewDateItemModel({
+ ...properties,
+ format: 'dd/MM/yyyy'
+ });
+ const formatChangeSpy = jasmine.createSpy('formatChangeSpy');
+
+ itemModel.formatChanges$.subscribe(formatChangeSpy);
+ itemModel.format = 'yyyy-MM-dd';
+
+ expect(formatChangeSpy).toHaveBeenCalledOnceWith('yyyy-MM-dd');
+ });
+
+ it('should not emit when the same format is assigned', () => {
+ const itemModel = new CardViewDateItemModel({
+ ...properties,
+ format: 'dd/MM/yyyy'
+ });
+ const formatChangeSpy = jasmine.createSpy('formatChangeSpy');
+
+ itemModel.formatChanges$.subscribe(formatChangeSpy);
+ itemModel.format = 'dd/MM/yyyy';
+
+ expect(formatChangeSpy).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/lib/core/src/lib/card-view/models/card-view-dateitem.model.ts b/lib/core/src/lib/card-view/models/card-view-dateitem.model.ts
index 398626a0dd..e1d7566e88 100644
--- a/lib/core/src/lib/card-view/models/card-view-dateitem.model.ts
+++ b/lib/core/src/lib/card-view/models/card-view-dateitem.model.ts
@@ -21,13 +21,29 @@ import { CardViewBaseItemModel } from './card-view-baseitem.model';
import { CardViewDateItemProperties } from '../interfaces/card-view.interfaces';
import { DatePipe } from '@angular/common';
import { DateFnsUtils } from '../../common/utils/date-fns-utils';
+import { Subject } from 'rxjs';
type DateItemType = Date | Date[] | null;
export class CardViewDateItemModel extends CardViewBaseItemModel implements CardViewItem, DynamicComponentModel {
type = 'date';
- format: string;
locale: string;
+ allowManualInput = false;
+
+ private readonly formatChangesSubject = new Subject();
+ readonly formatChanges$ = this.formatChangesSubject.asObservable();
+ private _format: string;
+
+ get format(): string {
+ return this._format;
+ }
+
+ set format(value: string) {
+ if (this._format !== value) {
+ this._format = value;
+ this.formatChangesSubject.next(value);
+ }
+ }
constructor(cardViewDateItemProperties: CardViewDateItemProperties) {
super(cardViewDateItemProperties);
@@ -39,6 +55,10 @@ export class CardViewDateItemModel extends CardViewBaseItemModel i
if (cardViewDateItemProperties.locale) {
this.locale = cardViewDateItemProperties.locale;
}
+
+ if (cardViewDateItemProperties.allowManualInput !== undefined) {
+ this.allowManualInput = cardViewDateItemProperties.allowManualInput;
+ }
}
get displayValue(): string | string[] {
diff --git a/lib/core/src/lib/card-view/models/card-view-datetimeitem.model.spec.ts b/lib/core/src/lib/card-view/models/card-view-datetimeitem.model.spec.ts
new file mode 100644
index 0000000000..b29637a65f
--- /dev/null
+++ b/lib/core/src/lib/card-view/models/card-view-datetimeitem.model.spec.ts
@@ -0,0 +1,59 @@
+/*!
+ * @license
+ * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { CardViewDatetimeItemModel } from './card-view-datetimeitem.model';
+import { CardViewDateItemProperties } from '../interfaces/card-view.interfaces';
+import { DateFnsUtils } from '../../common/utils/date-fns-utils';
+
+describe('CardViewDatetimeItemModel', () => {
+ let properties: CardViewDateItemProperties;
+
+ beforeEach(() => {
+ properties = {
+ label: 'Datetime label',
+ value: new Date('2025-03-07T15:30:00.000Z'),
+ key: 'datetimeKey'
+ };
+ });
+
+ it('should initialize with datetime type and default datetime format', () => {
+ const itemModel = new CardViewDatetimeItemModel(properties);
+
+ expect(itemModel.type).toBe('datetime');
+ expect(itemModel.format).toBe('MMM d, y, H:mm');
+ });
+
+ it('should preserve the provided format', () => {
+ const itemModel = new CardViewDatetimeItemModel({
+ ...properties,
+ format: 'yyyy-MM-dd HH:mm'
+ });
+
+ expect(itemModel.format).toBe('yyyy-MM-dd HH:mm');
+ });
+
+ it('should transform the original date value without forcing it to local midnight', () => {
+ const itemModel = new CardViewDatetimeItemModel(properties);
+
+ spyOn(DateFnsUtils, 'forceLocal');
+ spyOn(itemModel, 'transformDate').and.returnValue('formatted-datetime');
+
+ expect(itemModel.displayValue).toBe('formatted-datetime');
+ expect(DateFnsUtils.forceLocal).not.toHaveBeenCalled();
+ expect(itemModel.transformDate).toHaveBeenCalledOnceWith(properties.value as Date);
+ });
+});
diff --git a/lib/core/src/lib/card-view/models/card-view-datetimeitem.model.ts b/lib/core/src/lib/card-view/models/card-view-datetimeitem.model.ts
index 9f9a3b7511..fa04db2551 100644
--- a/lib/core/src/lib/card-view/models/card-view-datetimeitem.model.ts
+++ b/lib/core/src/lib/card-view/models/card-view-datetimeitem.model.ts
@@ -22,13 +22,11 @@ import { CardViewDateItemProperties } from '../interfaces/card-view.interfaces';
export class CardViewDatetimeItemModel extends CardViewDateItemModel implements CardViewItem, DynamicComponentModel {
type = 'datetime';
- format = 'MMM d, y, H:mm';
constructor(cardViewDateItemProperties: CardViewDateItemProperties) {
super(cardViewDateItemProperties);
-
- if (cardViewDateItemProperties.format) {
- this.format = cardViewDateItemProperties.format;
+ if (!cardViewDateItemProperties.format) {
+ this.format = 'MMM d, y, H:mm';
}
}
}
diff --git a/lib/core/src/lib/common/utils/datetime-fns-adapter.ts b/lib/core/src/lib/common/utils/datetime-fns-adapter.ts
index a9e7610d5d..0afaebed0f 100644
--- a/lib/core/src/lib/common/utils/datetime-fns-adapter.ts
+++ b/lib/core/src/lib/common/utils/datetime-fns-adapter.ts
@@ -134,7 +134,7 @@ export class AdfDateTimeFnsAdapter extends DatetimeAdapter {
override format(date: Date, displayFormat: any): string {
displayFormat = DateFnsUtils.convertMomentToDateFnsFormat(displayFormat);
- if (this.displayFormat && displayFormat === this.formats?.display?.datetimeInput) {
+ if (this.displayFormat && (displayFormat === this.formats?.display?.datetimeInput || displayFormat === this.formats?.display?.dateInput)) {
return this._delegate.format(date, this.displayFormat || displayFormat);
}