diff --git a/apps/ngx-bootstrap-docs/src/ng-api-doc.ts b/apps/ngx-bootstrap-docs/src/ng-api-doc.ts index 522c20721e..e73791cd83 100644 --- a/apps/ngx-bootstrap-docs/src/ng-api-doc.ts +++ b/apps/ngx-bootstrap-docs/src/ng-api-doc.ts @@ -838,6 +838,10 @@ export const ngdoc: any = { { "name": "bsValueChange", "description": "

Emits when datepicker value has been changed

\n" + }, + { + "name": "bsViewChange", + "description": "

Emits when datepicker view has been changed

\n" } ], "properties": [], @@ -971,6 +975,10 @@ export const ngdoc: any = { "name": "bsValueChange", "description": "

Emits when datepicker value has been changed

\n" }, + { + "name": "bsViewChange", + "description": "

Emits when datepicker view has been changed

\n" + }, { "name": "onHidden", "description": "

Emits an event when the datepicker is hidden

\n" @@ -1390,6 +1398,10 @@ export const ngdoc: any = { "name": "bsValueChange", "description": "

Emits when daterangepicker value has been changed

\n" }, + { + "name": "bsViewChange", + "description": "

Emits when datepicker view has been changed

\n" + }, { "name": "onHidden", "description": "

Emits an event when the daterangepicker is hidden

\n" diff --git a/libs/doc-pages/datepicker/src/lib/datepicker-section.list.ts b/libs/doc-pages/datepicker/src/lib/datepicker-section.list.ts index cb2f40ddae..4852b75ec6 100644 --- a/libs/doc-pages/datepicker/src/lib/datepicker-section.list.ts +++ b/libs/doc-pages/datepicker/src/lib/datepicker-section.list.ts @@ -51,6 +51,7 @@ import { DemoDatepickerClearButtonComponent } from './demos/clear-button/clear-b import { DemoDatepickerStartViewComponent } from "./demos/start-view/start-view"; import { DemoDatepickerPreventChangeToNextMonthComponent } from './demos/prevent-change-to-next-month/prevent-change-to-next-month.component'; import { DemoDatepickerWithTimepickerComponent } from './demos/with-timepicker/with-timepicker'; +import { DemoDatepickerViewChangeEventComponent } from './demos/view-change-event/view-change-event'; export const demoComponentContent: ContentSection[] = [ { @@ -320,6 +321,14 @@ export const demoComponentContent: ContentSection[] = [ description: `

You can subscribe to datepicker's value change event (bsValueChange).

`, outlet: DemoDatepickerValueChangeEventComponent }, + { + title: 'View change event', + anchor: 'view-change-event', + component: require('!!raw-loader!./demos/view-change-event/view-change-event.ts'), + html: require('!!raw-loader!./demos/view-change-event/view-change-event.html'), + description: `

You can subscribe to datepicker's view change event (bsViewChange).

`, + outlet: DemoDatepickerViewChangeEventComponent + }, { title: 'Config properties', anchor: 'config-object', @@ -624,6 +633,11 @@ export const demoComponentContent: ContentSection[] = [ anchor: 'value-change-event-ex', outlet: DemoDatepickerValueChangeEventComponent }, + { + title: 'View change event', + anchor: 'view-change-event-ex', + outlet: DemoDatepickerViewChangeEventComponent + }, { title: 'Config properties', anchor: 'config-object-ex', diff --git a/libs/doc-pages/datepicker/src/lib/demos/index.ts b/libs/doc-pages/datepicker/src/lib/demos/index.ts index 796d7574d4..cb036b90b1 100644 --- a/libs/doc-pages/datepicker/src/lib/demos/index.ts +++ b/libs/doc-pages/datepicker/src/lib/demos/index.ts @@ -32,6 +32,7 @@ import { DemoDatePickerSelectWeekRangeComponent } from './select-week-range/sele import { DemoDatepickerTriggersCustomComponent } from './triggers-custom/triggers-custom'; import { DemoDatepickerTriggersManualComponent } from './triggers-manual/triggers-manual'; import { DemoDatepickerValueChangeEventComponent } from './value-change-event/value-change-event'; +import { DemoDatepickerViewChangeEventComponent } from './view-change-event/view-change-event'; import { DemoDatePickerVisibilityEventsComponent } from './visibility-events/visibility-events'; import { DemoDatePickerQuickSelectRangesComponent } from './quick-select-ranges/quick-select-ranges'; import { DemoDateRangePickerShowPreviousMonth } from './daterangepicker-show-previous-month/show-previous-month'; @@ -77,6 +78,7 @@ export const DEMO_COMPONENTS = [ DemoDatepickerTriggersCustomComponent, DemoDatepickerTriggersManualComponent, DemoDatepickerValueChangeEventComponent, + DemoDatepickerViewChangeEventComponent, DemoDateRangePickerShowPreviousMonth, DemoDateRangePickerDisplayOneMonth, DemoDatePickerVisibilityEventsComponent, diff --git a/libs/doc-pages/datepicker/src/lib/demos/view-change-event/view-change-event.html b/libs/doc-pages/datepicker/src/lib/demos/view-change-event/view-change-event.html new file mode 100644 index 0000000000..6d1bea396d --- /dev/null +++ b/libs/doc-pages/datepicker/src/lib/demos/view-change-event/view-change-event.html @@ -0,0 +1,12 @@ +
+
+
+
$event.date: {{view.date | date}}
+
$event.mode: {{view.mode}}
+
+ +
+
diff --git a/libs/doc-pages/datepicker/src/lib/demos/view-change-event/view-change-event.ts b/libs/doc-pages/datepicker/src/lib/demos/view-change-event/view-change-event.ts new file mode 100644 index 0000000000..7ecf31191b --- /dev/null +++ b/libs/doc-pages/datepicker/src/lib/demos/view-change-event/view-change-event.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { BsDatepickerViewState } from 'src/datepicker/reducer/bs-datepicker.state'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'demo-datepicker-view-change-event', + templateUrl: './view-change-event.html' +}) +export class DemoDatepickerViewChangeEventComponent { + view?: BsDatepickerViewState; + + onViewChange(view: BsDatepickerViewState): void { + this.view = view; + } +} diff --git a/src/datepicker/base/bs-datepicker-container.ts b/src/datepicker/base/bs-datepicker-container.ts index 0d8ce5073e..0bbdcd3a16 100644 --- a/src/datepicker/base/bs-datepicker-container.ts +++ b/src/datepicker/base/bs-datepicker-container.ts @@ -16,6 +16,8 @@ import { WeekViewModel, YearsCalendarViewModel } from '../models'; +import { EventEmitter } from '@angular/core'; +import { BsDatepickerViewState } from '../reducer/bs-datepicker.state'; export abstract class BsDatepickerAbstractComponent { containerClass = ''; @@ -37,6 +39,8 @@ export abstract class BsDatepickerAbstractComponent { isRangePicker?: boolean; withTimepicker?: boolean; + viewChange: EventEmitter = new EventEmitter(); + set minDate(value: Date|undefined) { this._effects?.setMinDate(value); } diff --git a/src/datepicker/bs-datepicker-inline.component.ts b/src/datepicker/bs-datepicker-inline.component.ts index d47457dad8..c140a2a8de 100644 --- a/src/datepicker/bs-datepicker-inline.component.ts +++ b/src/datepicker/bs-datepicker-inline.component.ts @@ -23,6 +23,7 @@ import { DatepickerDateCustomClasses, DatepickerDateTooltipText } from './models import { BsDatepickerInlineContainerComponent } from './themes/bs/bs-datepicker-inline-container.component'; import { copyTime } from './utils/copy-time-utils'; import { checkBsValue, setCurrentTimeOnDateSelect } from './utils/bs-calendar-utils'; +import { BsDatepickerViewState } from './reducer/bs-datepicker.state'; @Directive({ selector: 'bs-datepicker-inline', @@ -61,6 +62,10 @@ export class BsDatepickerInlineDirective implements OnInit, OnDestroy, OnChanges * Enable specific dates */ @Input() datesDisabled?: Date[]; + /** + * Emits when datepicker view has been changed + */ + @Output() bsViewChange: EventEmitter = new EventEmitter(); /** * Emits when datepicker value has been changed */ @@ -96,14 +101,14 @@ export class BsDatepickerInlineDirective implements OnInit, OnDestroy, OnChanges return; } - if (!this._bsValue && value && !this._config.withTimepicker) { - const now = new Date(); - copyTime(value, now); - } + if (!this._bsValue && value && !this._config.withTimepicker) { + const now = new Date(); + copyTime(value, now); + } if (value && this.bsConfig?.initCurrentTime) { value = setCurrentTimeOnDateSelect(value); - } + } this._bsValue = value; this.bsValueChange.emit(value); @@ -130,6 +135,11 @@ export class BsDatepickerInlineDirective implements OnInit, OnDestroy, OnChanges this.bsValue = value; }) ); + this._subs.push( + this._datepickerRef.instance.viewChange.subscribe((view: BsDatepickerViewState) => { + this.bsViewChange.emit(view); + }) + ); } } diff --git a/src/datepicker/bs-datepicker.component.ts b/src/datepicker/bs-datepicker.component.ts index b8632e71a5..4d06f2d0e5 100644 --- a/src/datepicker/bs-datepicker.component.ts +++ b/src/datepicker/bs-datepicker.component.ts @@ -21,6 +21,7 @@ import { BsDatepickerViewMode, DatepickerDateCustomClasses, DatepickerDateToolti import { BsDatepickerContainerComponent } from './themes/bs/bs-datepicker-container.component'; import { copyTime } from './utils/copy-time-utils'; import { checkBsValue, setCurrentTimeOnDateSelect } from './utils/bs-calendar-utils'; +import { BsDatepickerViewState } from './reducer/bs-datepicker.state'; @Directive({ selector: '[bsDatepicker]', @@ -92,6 +93,10 @@ export class BsDatepickerDirective implements OnInit, OnDestroy, OnChanges, Afte * Date tooltip text */ @Input() dateTooltipTexts?: DatepickerDateTooltipText[]; + /** + * Emits when datepicker view has been changed + */ + @Output() bsViewChange: EventEmitter = new EventEmitter(); /** * Emits when datepicker value has been changed */ @@ -102,10 +107,10 @@ export class BsDatepickerDirective implements OnInit, OnDestroy, OnChanges, Afte private readonly _dateInputFormat$ = new Subject(); constructor(public _config: BsDatepickerConfig, - private _elementRef: ElementRef, - private _renderer: Renderer2, - _viewContainerRef: ViewContainerRef, - cis: ComponentLoaderFactory) { + private _elementRef: ElementRef, + private _renderer: Renderer2, + _viewContainerRef: ViewContainerRef, + cis: ComponentLoaderFactory) { // todo: assign only subset of fields Object.assign(this, this._config); this._datepicker = cis.createLoader( @@ -242,6 +247,11 @@ export class BsDatepickerDirective implements OnInit, OnDestroy, OnChanges, Afte this.hide(); }) ); + this._subs.push( + this._datepickerRef.instance.viewChange.subscribe((view: BsDatepickerViewState) => { + this.bsViewChange.emit(view); + }) + ); } } diff --git a/src/datepicker/bs-daterangepicker-inline.component.ts b/src/datepicker/bs-daterangepicker-inline.component.ts index 2570ced468..6457e16fa2 100644 --- a/src/datepicker/bs-daterangepicker-inline.component.ts +++ b/src/datepicker/bs-daterangepicker-inline.component.ts @@ -17,6 +17,7 @@ import { checkRangesWithMaxDate, setDateRangesCurrentTimeOnDateSelect } from './utils/bs-calendar-utils'; +import { BsDatepickerViewState } from './reducer/bs-datepicker.state'; @Directive({ selector: 'bs-daterangepicker-inline', @@ -73,6 +74,10 @@ export class BsDaterangepickerInlineDirective implements OnInit, OnDestroy, OnCh * Disable specific dates */ @Input() datesEnabled?: Date[]; + /** + * Emits when datepicker view has been changed + */ + @Output() bsViewChange: EventEmitter = new EventEmitter(); /** * Emits when daterangepicker value has been changed */ @@ -201,6 +206,11 @@ export class BsDaterangepickerInlineDirective implements OnInit, OnDestroy, OnCh this.bsValue = value; }) ); + this._subs.push( + this._datepickerRef.instance.viewChange.subscribe((view: BsDatepickerViewState) => { + this.bsViewChange.emit(view); + }) + ); } } diff --git a/src/datepicker/bs-daterangepicker.component.ts b/src/datepicker/bs-daterangepicker.component.ts index 03bfe13e68..504de4e7b9 100644 --- a/src/datepicker/bs-daterangepicker.component.ts +++ b/src/datepicker/bs-daterangepicker.component.ts @@ -1,4 +1,5 @@ -import { AfterViewInit, ComponentRef, +import { + AfterViewInit, ComponentRef, Directive, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, Renderer2, SimpleChanges, @@ -16,6 +17,7 @@ import { checkRangesWithMaxDate, setDateRangesCurrentTimeOnDateSelect } from './utils/bs-calendar-utils'; +import { BsDatepickerViewState } from './reducer/bs-datepicker.state'; @Directive({ selector: '[bsDaterangepicker]', @@ -64,7 +66,7 @@ export class BsDaterangepickerDirective */ @Output() onHidden: EventEmitter; - _bsValue?: (Date|undefined)[]; + _bsValue?: (Date | undefined)[]; isOpen$: BehaviorSubject; isDestroy$ = new Subject(); @@ -72,7 +74,7 @@ export class BsDaterangepickerDirective * Initial value of daterangepicker */ @Input() - set bsValue(value: (Date|undefined)[] | undefined) { + set bsValue(value: (Date | undefined)[] | undefined) { if (this._bsValue === value) { return; } @@ -118,10 +120,14 @@ export class BsDaterangepickerDirective * Enable specific dates */ @Input() datesEnabled?: Date[]; + /** + * Emits when datepicker view has been changed + */ + @Output() bsViewChange: EventEmitter = new EventEmitter(); /** * Emits when daterangepicker value has been changed */ - @Output() bsValueChange = new EventEmitter<((Date|undefined)[]|undefined)>(); + @Output() bsValueChange = new EventEmitter<((Date | undefined)[] | undefined)>(); get rangeInputFormat$(): Observable { return this._rangeInputFormat$; @@ -133,10 +139,10 @@ export class BsDaterangepickerDirective private readonly _rangeInputFormat$ = new Subject(); constructor(public _config: BsDaterangepickerConfig, - private _elementRef: ElementRef, - private _renderer: Renderer2, - _viewContainerRef: ViewContainerRef, - cis: ComponentLoaderFactory) { + private _elementRef: ElementRef, + private _renderer: Renderer2, + _viewContainerRef: ViewContainerRef, + cis: ComponentLoaderFactory) { this._datepicker = cis.createLoader( _elementRef, _viewContainerRef, @@ -251,6 +257,11 @@ export class BsDaterangepickerDirective this.hide(); }) ); + this._subs.push( + this._datepickerRef.instance.viewChange.subscribe((view: BsDatepickerViewState) => { + this.bsViewChange.emit(view); + }) + ); } } diff --git a/src/datepicker/testing/bs-datepicker.spec.ts b/src/datepicker/testing/bs-datepicker.spec.ts index 365d3cff1a..10394634a3 100644 --- a/src/datepicker/testing/bs-datepicker.spec.ts +++ b/src/datepicker/testing/bs-datepicker.spec.ts @@ -5,9 +5,9 @@ import { dispatchKeyboardEvent } from '@ngneat/spectator'; import { registerEscClick } from 'ngx-bootstrap/utils'; import { BsDatepickerDirective } from '../bs-datepicker.component'; import { BsDatepickerConfig } from '../bs-datepicker.config'; - import { BsDatepickerModule } from '../bs-datepicker.module'; -import { BsDatepickerViewMode, CalendarCellViewModel, WeekViewModel } from '../models'; +import { BsDatepickerViewMode, BsNavigationDirection, BsNavigationEvent, CalendarCellViewModel, WeekViewModel } from '../models'; +import { BsDatepickerViewState } from '../reducer/bs-datepicker.state'; import { BsDatepickerContainerComponent } from '../themes/bs/bs-datepicker-container.component'; @Component({ @@ -27,12 +27,19 @@ class TestComponent { type TestFixture = ComponentFixture; -function getDatepickerDirective(fixture: TestFixture): BsDatepickerDirective { - return fixture.componentInstance.datepicker; +function getDatepickerDirective(fixture: TestFixture, date?: Date): BsDatepickerDirective { + const datepicker = fixture.componentInstance.datepicker; + + if (date) { + datepicker.bsValue = date; + fixture.detectChanges(); + } + + return datepicker; } -function showDatepicker(fixture: TestFixture): BsDatepickerDirective { - const datepicker = getDatepickerDirective(fixture); +function showDatepicker(fixture: TestFixture, date?: Date): BsDatepickerDirective { + const datepicker = getDatepickerDirective(fixture, date); datepicker.show(); fixture.detectChanges(); @@ -55,12 +62,12 @@ describe('datepicker:', () => { let fixture: TestFixture; beforeEach( waitForAsync(() => TestBed.configureTestingModule({ - declarations: [TestComponent], - imports: [ - BsDatepickerModule.forRoot(), - BrowserAnimationsModule - ] - }).compileComponents() + declarations: [TestComponent], + imports: [ + BsDatepickerModule.forRoot(), + BrowserAnimationsModule + ] + }).compileComponents() )); beforeEach(() => { fixture = TestBed.createComponent(TestComponent); @@ -290,4 +297,53 @@ describe('datepicker:', () => { const timepickerZone = document.querySelector('.bs-timepicker-in-datepicker-container'); expect(timepickerZone).not.toBeTruthy(); }); + + describe('should emit bsViewChange', () => { + const initialDate = new Date(2022, 0, 1); + const parameters: { description: string, navigationEvent: BsNavigationEvent, expectedEmit: BsDatepickerViewState }[] = [ + { + description: 'when user navigates one month upwards', + navigationEvent: { step: { month: 1 }, direction: BsNavigationDirection.UP }, + expectedEmit: { date: new Date(2022, 1, 1), mode: 'day' } + }, + { + description: 'when user navigates one month downwards', + navigationEvent: { step: { month: -1 }, direction: BsNavigationDirection.DOWN }, + // navigating down one month from january 2022 should result in december 2021 + expectedEmit: { date: new Date(2021, 11, 1), mode: 'day' } + }, + { + description: 'when user navigates one year upwards', + navigationEvent: { step: { year: 1 }, direction: BsNavigationDirection.UP }, + expectedEmit: { date: new Date(2023, 0, 1), mode: 'day' } + }, + { + description: 'when user navigates one year downwards', + navigationEvent: { step: { year: -1 }, direction: BsNavigationDirection.DOWN }, + expectedEmit: { date: new Date(2021, 0, 1), mode: 'day' } + } + ]; + + parameters.forEach(parameter => { + it(parameter.description, () => { + const datepicker = showDatepicker(fixture, initialDate); + const datepickerContainerInstance = getDatepickerContainer(datepicker); + const spy = jest.spyOn(datepicker.bsViewChange, 'emit'); + + datepickerContainerInstance.navigateTo(parameter.navigationEvent); + fixture.detectChanges(); + expect(spy).toHaveBeenCalledWith(parameter.expectedEmit); + }); + }); + }); + + it('should emit bsViewChange when user changes the viewmode inside the datepicker', () => { + const datepicker = showDatepicker(fixture, new Date(2022, 0, 1)); + const datepickerContainerInstance = getDatepickerContainer(datepicker); + const spy = jest.spyOn(datepicker.bsViewChange, 'emit'); + + datepickerContainerInstance.setViewMode('month'); + fixture.detectChanges(); + expect(spy).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/datepicker/themes/bs/bs-datepicker-container.component.ts b/src/datepicker/themes/bs/bs-datepicker-container.component.ts index d468d13232..f141cc8dc6 100644 --- a/src/datepicker/themes/bs/bs-datepicker-container.component.ts +++ b/src/datepicker/themes/bs/bs-datepicker-container.component.ts @@ -23,6 +23,7 @@ import { CalendarCellViewModel, DayViewModel } from '../../models'; import { BsDatepickerActions } from '../../reducer/bs-datepicker.actions'; import { BsDatepickerEffects } from '../../reducer/bs-datepicker.effects'; import { BsDatepickerStore } from '../../reducer/bs-datepicker.store'; +import { BsDatepickerState } from '../../reducer/bs-datepicker.state'; @Component({ selector: 'bs-datepicker-container', @@ -39,7 +40,7 @@ import { BsDatepickerStore } from '../../reducer/bs-datepicker.store'; export class BsDatepickerContainerComponent extends BsDatepickerAbstractComponent implements OnInit, AfterViewInit, OnDestroy { - set value(value: Date|undefined) { + set value(value: Date | undefined) { this._effects?.setValue(value); } @@ -115,9 +116,11 @@ export class BsDatepickerContainerComponent extends BsDatepickerAbstractComponen // todo: move it somewhere else // on selected date change this._subs.push( - this._store.select((state: any) => state.selectedDate).subscribe((date: any) => this.valueChange.emit(date)) + this._store.select((state: BsDatepickerState) => state.selectedDate).subscribe((date: any) => this.valueChange.emit(date)) + ); + this._subs.push( + this._store.select((state: BsDatepickerState) => state.view).subscribe((view) => this.viewChange.emit(view)) ); - this._store.dispatch(this._actions.changeViewMode(this._config.startView)); } @@ -147,7 +150,7 @@ export class BsDatepickerContainerComponent extends BsDatepickerAbstractComponen override daySelectHandler(day: DayViewModel): void { if (!day) { - return; + return; } const isDisabled = this.isOtherMonthsActive ? day.isDisabled : (day.isOtherMonth || day.isDisabled); diff --git a/src/datepicker/themes/bs/bs-daterangepicker-container.component.ts b/src/datepicker/themes/bs/bs-daterangepicker-container.component.ts index 8e275bf48c..32560bb9bf 100644 --- a/src/datepicker/themes/bs/bs-daterangepicker-container.component.ts +++ b/src/datepicker/themes/bs/bs-daterangepicker-container.component.ts @@ -41,7 +41,7 @@ import { dayInMilliseconds } from '../../reducer/_defaults'; export class BsDaterangepickerContainerComponent extends BsDatepickerAbstractComponent implements OnInit, OnDestroy, AfterViewInit { - set value(value: (Date|undefined)[] | undefined) { + set value(value: (Date | undefined)[] | undefined) { this._effects?.setRangeValue(value); } @@ -123,6 +123,11 @@ export class BsDaterangepickerContainerComponent extends BsDatepickerAbstractCom this.chosenRange = dateRange || []; }) ); + this._subs.push( + this._store + .select(state => state.view) + .subscribe(view => this.viewChange.emit(view)) + ); } ngAfterViewInit(): void { @@ -226,7 +231,7 @@ export class BsDaterangepickerContainerComponent extends BsDatepickerAbstractCom this._rangeStack = day.date >= this._rangeStack[0] ? [this._rangeStack[0], day.date] - : [day.date]; + : [day.date]; } if (this._config.maxDateRange) { @@ -268,7 +273,7 @@ export class BsDaterangepickerContainerComponent extends BsDatepickerAbstractCom if (this._config.maxDate) { const maxDateValueInMilliseconds = this._config.maxDate.getTime(); - const maxDateRangeInMilliseconds = currentSelection.getTime() + ((this._config.maxDateRange || 0) * dayInMilliseconds ); + const maxDateRangeInMilliseconds = currentSelection.getTime() + ((this._config.maxDateRange || 0) * dayInMilliseconds); maxDateRange = maxDateRangeInMilliseconds > maxDateValueInMilliseconds ? new Date(this._config.maxDate) : new Date(maxDateRangeInMilliseconds);