From 0efbb26786ed24cfe2e6aad36203d0726010427e Mon Sep 17 00:00:00 2001 From: Keanan Date: Wed, 7 Feb 2024 15:27:14 -0600 Subject: [PATCH 1/4] feat(datepicker): implement apply button Implement apply and cancel button to datepicker. Modify behaviour to keep picker open until applied. Related #4203 --- apps/ngx-bootstrap-docs/src/ng-api-doc.ts | 30 +++++++++++++ .../src/lib/datepicker-section.list.ts | 15 +++++++ .../lib/demos/apply-button/apply-button.html | 9 ++++ .../lib/demos/apply-button/apply-button.ts | 8 ++++ .../datepicker/src/lib/demos/index.ts | 3 ++ .../base/bs-datepicker-container.ts | 13 +++++- src/datepicker/bs-datepicker.component.ts | 35 ++++++++++++++++ src/datepicker/bs-datepicker.config.ts | 42 +++++++++++++++++++ src/datepicker/bs-datepicker.scss | 17 +++++--- .../bs/bs-datepicker-container.component.ts | 15 +++++++ .../themes/bs/bs-datepicker-view.html | 30 +++++++------ src/datepicker/utils/scss/mixins.scss | 14 +++---- 12 files changed, 204 insertions(+), 27 deletions(-) create mode 100644 libs/doc-pages/datepicker/src/lib/demos/apply-button/apply-button.html create mode 100644 libs/doc-pages/datepicker/src/lib/demos/apply-button/apply-button.ts diff --git a/apps/ngx-bootstrap-docs/src/ng-api-doc.ts b/apps/ngx-bootstrap-docs/src/ng-api-doc.ts index 507066b4ad..fa48a4962d 100644 --- a/apps/ngx-bootstrap-docs/src/ng-api-doc.ts +++ b/apps/ngx-bootstrap-docs/src/ng-api-doc.ts @@ -546,6 +546,24 @@ export const ngdoc: any = { type: 'string[]', description: '

Set allowed positions of container.

\n' }, + { + name: 'applyButtonLabel', + defaultValue: 'Apply', + type: 'string', + description: '

Label for 'apply' button

\n' + }, + { + name: 'applyPosition', + defaultValue: 'right', + type: 'string', + description: '

Positioning of 'apply' buttons

\n' + }, + { + name: 'cancelButtonLabel', + defaultValue: 'Cancel', + type: 'string', + description: '

Label for 'cancel' button

\n' + }, { name: 'clearButtonLabel', defaultValue: 'Clear', @@ -690,6 +708,18 @@ export const ngdoc: any = { description: '

Allows select daterange as first and last day of week by click on week number (dateRangePicker only)

\n' }, + { + name: 'showApplyButton', + defaultValue: 'false', + type: 'boolean', + description: '

Shows apply and cancel button. Selected date is not sent from view to model until the user selects apply. The picker will remain open until the selection is applied, cancelled, or the picker is dismissed.

\n' + }, + { + name: 'showCancelButton', + defaultValue: 'true', + type: 'boolean', + description: '

Shows or hides the cancel button. This only applies if showApplyButton is true.

\n' + }, { name: 'showClearButton', defaultValue: 'false', 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 8544ebeb82..07d3f492a5 100644 --- a/libs/doc-pages/datepicker/src/lib/datepicker-section.list.ts +++ b/libs/doc-pages/datepicker/src/lib/datepicker-section.list.ts @@ -48,12 +48,14 @@ import { DemoDateRangePickerMaxDateRangeComponent } from './demos/max-date-range import { DemoDateRangePickerDisplayOneMonth } from './demos/daterangepicker-display-one-month/display-one-month'; import { DemoDatepickerTodayButtonComponent } from './demos/today-button/today-button'; import { DemoDatepickerClearButtonComponent } from './demos/clear-button/clear-button'; +import { DemoDatepickerApplyButtonComponent } from './demos/apply-button/apply-button'; 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 { DatepickerCloseBehaviorComponent } from './demos/closeBehaviour/datepicker-close-behavior'; import { KeepDatesOutOfRulesComponent } from './demos/keep-dates-out-of-rules/keep-dates-out-of-rules.component'; + export const demoComponentContent: ContentSection[] = [ { name: 'Overview', @@ -438,6 +440,14 @@ export const demoComponentContent: ContentSection[] = [ description: `

Display an optional 'Clear' button that will automatically clear date.

`, outlet: DemoDatepickerClearButtonComponent }, + { + title: 'Show Apply Button', + anchor: 'datepicker-show-apply-button', + component: require('!!raw-loader!./demos/apply-button/apply-button.ts'), + html: require('!!raw-loader!./demos/apply-button/apply-button.html'), + description: `

Display an 'Apply' and 'Cancel' button. The datepicker will not update the model unless the 'Apply' button is pressed. If the 'Cancel' button is pressed the model value will not be updated and the datepicker will be closed. The datepicker will remain open until it is applied, cancelled, or dismissed.

`, + outlet: DemoDatepickerApplyButtonComponent + }, { title: 'Start view', anchor: 'start-view', @@ -712,6 +722,11 @@ export const demoComponentContent: ContentSection[] = [ anchor: 'datepicker-show-clear-button-ex', outlet: DemoDatepickerClearButtonComponent }, + { + title: 'Show Apply Button', + anchor: 'datepicker-show-apply-button-ex', + outlet: DemoDatepickerApplyButtonComponent + }, { title: 'Start view', anchor: 'start-view-ex', diff --git a/libs/doc-pages/datepicker/src/lib/demos/apply-button/apply-button.html b/libs/doc-pages/datepicker/src/lib/demos/apply-button/apply-button.html new file mode 100644 index 0000000000..be68ea86bf --- /dev/null +++ b/libs/doc-pages/datepicker/src/lib/demos/apply-button/apply-button.html @@ -0,0 +1,9 @@ +
+
+ +
+
diff --git a/libs/doc-pages/datepicker/src/lib/demos/apply-button/apply-button.ts b/libs/doc-pages/datepicker/src/lib/demos/apply-button/apply-button.ts new file mode 100644 index 0000000000..0a49681b8a --- /dev/null +++ b/libs/doc-pages/datepicker/src/lib/demos/apply-button/apply-button.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'demo-datepicker-apply-button', + templateUrl: './apply-button.html' +}) +export class DemoDatepickerApplyButtonComponent {} diff --git a/libs/doc-pages/datepicker/src/lib/demos/index.ts b/libs/doc-pages/datepicker/src/lib/demos/index.ts index 7d7932d522..4fffb0bd5d 100644 --- a/libs/doc-pages/datepicker/src/lib/demos/index.ts +++ b/libs/doc-pages/datepicker/src/lib/demos/index.ts @@ -40,12 +40,14 @@ import { DemoDateRangePickerMaxDateRangeComponent } from './max-date-range/max-d import { DemoDateRangePickerDisplayOneMonth } from './daterangepicker-display-one-month/display-one-month'; import { DemoDatepickerTodayButtonComponent } from './today-button/today-button'; import { DemoDatepickerClearButtonComponent } from './clear-button/clear-button'; +import { DemoDatepickerApplyButtonComponent } from './apply-button/apply-button'; import { DemoDatepickerStartViewComponent } from "./start-view/start-view"; import { DemoDatepickerPreventChangeToNextMonthComponent } from './prevent-change-to-next-month/prevent-change-to-next-month.component'; import { DemoDatepickerWithTimepickerComponent } from './with-timepicker/with-timepicker'; import { DatepickerCloseBehaviorComponent } from './closeBehaviour/datepicker-close-behavior'; import { KeepDatesOutOfRulesComponent } from './keep-dates-out-of-rules/keep-dates-out-of-rules.component'; + export const DEMO_COMPONENTS = [ DemoDatePickerAdaptivePositionComponent, DemoDatePickerAnimatedComponent, @@ -84,6 +86,7 @@ export const DEMO_COMPONENTS = [ DemoDatePickerVisibilityEventsComponent, DemoDatepickerTodayButtonComponent, DemoDatepickerClearButtonComponent, + DemoDatepickerApplyButtonComponent, DemoDateRangePickerShowPreviousMonth, DemoDateRangePickerMaxDateRangeComponent, DemoDatepickerPreventChangeToNextMonthComponent, diff --git a/src/datepicker/base/bs-datepicker-container.ts b/src/datepicker/base/bs-datepicker-container.ts index 0d8ce5073e..f84424288d 100644 --- a/src/datepicker/base/bs-datepicker-container.ts +++ b/src/datepicker/base/bs-datepicker-container.ts @@ -26,7 +26,12 @@ export abstract class BsDatepickerAbstractComponent { showClearBtn?: boolean; clearBtnLbl?: string; clearPos?: string; - + showApplyBtn?: boolean; + showCancelBtn?: boolean; + cancelBtnLbl?: string; + applyBtnLbl?: string; + applyPos?: string; + _effects?: BsDatepickerEffects; customRanges: BsCustomDates[] = []; customRangeBtnLbl?: string; @@ -130,6 +135,12 @@ export abstract class BsDatepickerAbstractComponent { // eslint-disable-next-line clearDate(): void {} + // eslint-disable-next-line + apply(): void {} + + // eslint-disable-next-line + cancel(): void {} + // eslint-disable-next-line @typescript-eslint/no-explicit-any _stopPropagation(event: any): void { event.stopPropagation(); diff --git a/src/datepicker/bs-datepicker.component.ts b/src/datepicker/bs-datepicker.component.ts index c3a1c69e67..7a7072f85c 100644 --- a/src/datepicker/bs-datepicker.component.ts +++ b/src/datepicker/bs-datepicker.component.ts @@ -107,6 +107,7 @@ export class BsDatepickerDirective implements OnInit, OnDestroy, OnChanges, Afte private _datepicker: ComponentLoader; private _datepickerRef?: ComponentRef; private readonly _dateInputFormat$ = new Subject(); + private _externalValue?: Date; constructor(public _config: BsDatepickerConfig, private _elementRef: ElementRef, @@ -159,6 +160,12 @@ export class BsDatepickerDirective implements OnInit, OnDestroy, OnChanges, Afte this.initPreviousValue(); this._bsValue = value; + + // if apply button is show don't update external source (model -> view) + if(this.bsConfig?.showApplyButton){ + return; + } + this.bsValueChange.emit(value); } @@ -240,6 +247,7 @@ export class BsDatepickerDirective implements OnInit, OnDestroy, OnChanges, Afte this._subs.push( this.bsValueChange.subscribe((value: Date) => { if (this._datepickerRef) { + this._externalValue = value; this._datepickerRef.instance.value = value; } }) @@ -255,13 +263,40 @@ export class BsDatepickerDirective implements OnInit, OnDestroy, OnChanges, Afte return; } + if(this.bsConfig?.showApplyButton){ + return; + } + this.hide(); }) ); + + // if apply button is shown update external source (view -> model) + if(this.bsConfig?.showApplyButton){ + this._subs.push( + this._datepickerRef.instance.valueApplied.subscribe(() => { + this.bsValueChange.emit(this._bsValue); + + this.hide(); + }) + ); + + // if cancel is pressed reset picker value to external value + this._subs.push( + this._datepickerRef.instance.valueCancelled.subscribe(() => { + if (this._datepickerRef) { + this._datepickerRef.instance.value = this._externalValue; + } + + this.hide(); + }) + ); + } } } keepDatepickerModalOpened(): boolean { + if (!previousDate || !this.bsConfig?.keepDatepickerOpened || !this._config.withTimepicker) { return false; } diff --git a/src/datepicker/bs-datepicker.config.ts b/src/datepicker/bs-datepicker.config.ts index ac735988ac..af74619654 100644 --- a/src/datepicker/bs-datepicker.config.ts +++ b/src/datepicker/bs-datepicker.config.ts @@ -24,44 +24,54 @@ export class BsDatepickerConfig implements DatepickerRenderOptions { isAnimated = false; value?: Date | Date[]; isDisabled?: boolean; + /** * Default min date for all date/range pickers */ minDate?: Date; + /** * Default max date for all date/range pickers */ maxDate?: Date; + /** * The view that the datepicker should start in */ startView: BsDatepickerViewMode = 'day'; + /** * Default date custom classes for all date/range pickers */ dateCustomClasses?: DatepickerDateCustomClasses[]; + /** * Default tooltip text for all date/range pickers */ dateTooltipTexts?: DatepickerDateTooltipText[]; + /** * Disable specific days, e.g. [0,6] will disable all Saturdays and Sundays */ daysDisabled?: number[]; + /** * Disable specific dates */ datesDisabled?: Date[]; + /** * Show one months for special cases (only for dateRangePicker) * 1. maxDate is equal to today's date * 2. minDate's month is equal to maxDate's month */ displayOneMonthRange?: boolean; + /** * Enable specific dates */ datesEnabled?: Date[]; + /** * Makes dates from other months active */ @@ -109,14 +119,17 @@ export class BsDatepickerConfig implements DatepickerRenderOptions { // DatepickerRenderOptions displayMonths = 1; + /** * Allows to hide week numbers in datepicker */ showWeekNumbers = true; dateInputFormat = 'L'; + // range picker rangeSeparator = ' - '; + /** * Date format for date range input field */ @@ -150,6 +163,16 @@ export class BsDatepickerConfig implements DatepickerRenderOptions { */ showClearButton = false; + /** + * Shows 'apply' and 'cancel' button, date is not sent from view to model until user presses apply. Picker remains opened until applied, cancelled or dismissed (clicked off of) + */ + showApplyButton = false; + + /** + * Shows 'cancel' button this is only if showApplyButton is also true + */ + showCancelButton = true; + /** * Positioning of 'today' button */ @@ -159,6 +182,11 @@ export class BsDatepickerConfig implements DatepickerRenderOptions { * Positioning of 'clear' button */ clearPosition = 'right'; + + /** + * Positioning of 'apply' button + */ + applyPosition = 'right'; /** * Label for 'today' button @@ -170,6 +198,16 @@ export class BsDatepickerConfig implements DatepickerRenderOptions { */ clearButtonLabel = 'Clear'; + /** + * Label for 'cancel' button + */ + cancelButtonLabel = 'Cancel'; + + /** + * Label for 'apply' button + */ + applyButtonLabel = 'Apply'; + /** * Label for 'custom range' button */ @@ -179,18 +217,22 @@ export class BsDatepickerConfig implements DatepickerRenderOptions { * Shows timepicker under datepicker */ withTimepicker = false; + /** * Set current hours, minutes, seconds and milliseconds for bsValue */ initCurrentTime?: boolean; + /** * Set allowed positions of container. */ allowedPositions = ['top', 'bottom']; + /** * Set rule for datepicker closing. If value is true datepicker closes only if date is changed, if user changes only time datepicker doesn't close. It is available only if property withTimepicker is set true * */ keepDatepickerOpened = false; + /** * Allows keep invalid dates in range. Can be used with minDate, maxDate * */ diff --git a/src/datepicker/bs-datepicker.scss b/src/datepicker/bs-datepicker.scss index c756262d85..9c56b663d0 100644 --- a/src/datepicker/bs-datepicker.scss +++ b/src/datepicker/bs-datepicker.scss @@ -427,6 +427,7 @@ .bs-media-container { display: flex; + justify-content: center; @media(max-width: 768px) { flex-direction: column; } @@ -470,28 +471,32 @@ flex-flow: row wrap; justify-content: flex-end; padding-top: 10px; - border-top: 1px solid $border-color; + gap: 5px; .btn-default { - margin-left: 10px; + margin-right: 5px; } - .btn-today-wrapper { + .btn-today-wrapper, + .btn-apply-wrapper{ display: flex; flex-flow: row wrap; } .clear-right, - .today-right { + .today-right, + .apply-right { flex-grow: 0; } .clear-left, - .today-left { + .today-left, + .apply-left { flex-grow: 1; } .clear-center, - .today-center { + .today-center, + .apply-center { flex-grow: 0.5; } } diff --git a/src/datepicker/themes/bs/bs-datepicker-container.component.ts b/src/datepicker/themes/bs/bs-datepicker-container.component.ts index 51162c53d0..d2f85d2b05 100644 --- a/src/datepicker/themes/bs/bs-datepicker-container.component.ts +++ b/src/datepicker/themes/bs/bs-datepicker-container.component.ts @@ -42,6 +42,8 @@ export class BsDatepickerContainerComponent implements OnInit, AfterViewInit, OnDestroy { valueChange: EventEmitter = new EventEmitter(); + valueApplied: EventEmitter = new EventEmitter(); + valueCancelled: EventEmitter = new EventEmitter(); animationState = 'void'; override isRangePicker = false; _subs: Subscription[] = []; @@ -113,6 +115,11 @@ export class BsDatepickerContainerComponent this.showClearBtn = this._config.showClearButton; this.clearBtnLbl = this._config.clearButtonLabel; this.clearPos = this._config.clearPosition; + this.showApplyBtn = this._config.showApplyButton; + this.showCancelBtn = this._config.showCancelButton; + this.applyBtnLbl = this._config.applyButtonLabel; + this.applyPos = this._config.applyPosition; + this.cancelBtnLbl = this._config.cancelButtonLabel; this.customRangeBtnLbl = this._config.customRangeButtonLabel; this.withTimepicker = this._config.withTimepicker; this._effects @@ -229,6 +236,14 @@ export class BsDatepickerContainerComponent this._store.dispatch(this._actions.select(undefined)); } + override apply(): void { + this.valueApplied.emit(); + } + + override cancel(): void { + this.valueCancelled.emit(); + } + ngOnDestroy(): void { for (const sub of this._subs) { sub.unsubscribe(); diff --git a/src/datepicker/themes/bs/bs-datepicker-view.html b/src/datepicker/themes/bs/bs-datepicker-view.html index 4fbfea672a..92952ec453 100644 --- a/src/datepicker/themes/bs/bs-datepicker-view.html +++ b/src/datepicker/themes/bs/bs-datepicker-view.html @@ -54,28 +54,32 @@ - -
- - -
-
+
+ [class.today-left]="todayPos === 'left'" + [class.today-right]="todayPos === 'right'" + [class.today-center]="todayPos === 'center'" + *ngIf="showTodayBtn">
-
- -
+ +
+ +
+ + +
diff --git a/src/datepicker/utils/scss/mixins.scss b/src/datepicker/utils/scss/mixins.scss index 227038fa99..afddd71f9d 100644 --- a/src/datepicker/utils/scss/mixins.scss +++ b/src/datepicker/utils/scss/mixins.scss @@ -6,7 +6,7 @@ background-color: $color; } - .btn-today-wrapper, .btn-clear-wrapper { + .btn-today-wrapper, .btn-clear-wrapper, .btn-apply-wrapper { .btn-success { background-color: $color; border-color: $color; @@ -22,7 +22,7 @@ } @if $name == 'green' { - .btn-today-wrapper, .btn-clear-wrapper { + .btn-today-wrapper, .btn-clear-wrapper, .btn-apply-wrapper { .btn-success:not(:disabled):not(.disabled):active { background-color: $active-theme-green; border-color: $active-theme-green; @@ -36,7 +36,7 @@ } @if $name == 'blue' { - .btn-today-wrapper, .btn-clear-wrapper { + .btn-today-wrapper, .btn-clear-wrapper, .btn-apply-wrapper { .btn-success:not(:disabled):not(.disabled):active { background-color: $active-theme-blue; border-color: $active-theme-blue; @@ -50,7 +50,7 @@ } @if $name == 'dark-blue' { - .btn-today-wrapper, .btn-clear-wrapper { + .btn-today-wrapper, .btn-clear-wrapper, .btn-apply-wrapper { .btn-success:not(:disabled):not(.disabled):active { background-color: $active-theme-dark-blue; border-color: $active-theme-dark-blue; @@ -64,7 +64,7 @@ } @if $name == 'orange' { - .btn-today-wrapper, .btn-clear-wrapper { + .btn-today-wrapper, .btn-clear-wrapper, .btn-apply-wrapper { .btn-success:not(:disabled):not(.disabled):active { background-color: $active-theme-orange; border-color: $active-theme-orange; @@ -78,7 +78,7 @@ } @if $name == 'red' { - .btn-today-wrapper, .btn-clear-wrapper { + .btn-today-wrapper, .btn-clear-wrapper, .btn-apply-wrapper { .btn-success:not(:disabled):not(.disabled):active { background-color: $active-theme-red; border-color: $active-theme-red; @@ -92,7 +92,7 @@ } @if $name == 'default' { - .btn-today-wrapper, .btn-clear-wrapper { + .btn-today-wrapper, .btn-clear-wrapper, .btn-apply-wrapper { .btn-success:not(:disabled):not(.disabled):active { background-color: $active-theme-default; border-color: $active-theme-default; From 470435b161060410a8bdeb1217209a67efe34159 Mon Sep 17 00:00:00 2001 From: Keanan Date: Wed, 7 Feb 2024 15:44:39 -0600 Subject: [PATCH 2/4] feat(datepicker): change eventEmitter to subject --- .../themes/bs/bs-datepicker-container.component.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/datepicker/themes/bs/bs-datepicker-container.component.ts b/src/datepicker/themes/bs/bs-datepicker-container.component.ts index d2f85d2b05..fa30fee64a 100644 --- a/src/datepicker/themes/bs/bs-datepicker-container.component.ts +++ b/src/datepicker/themes/bs/bs-datepicker-container.component.ts @@ -11,7 +11,7 @@ import { } from '@angular/core'; import { take } from 'rxjs/operators'; -import { Subscription } from 'rxjs'; +import { Subject, Subscription } from 'rxjs'; import { getFullYear, getMonth } from 'ngx-bootstrap/chronos'; import { PositioningService } from 'ngx-bootstrap/positioning'; @@ -42,8 +42,8 @@ export class BsDatepickerContainerComponent implements OnInit, AfterViewInit, OnDestroy { valueChange: EventEmitter = new EventEmitter(); - valueApplied: EventEmitter = new EventEmitter(); - valueCancelled: EventEmitter = new EventEmitter(); + valueApplied: Subject = new Subject(); + valueCancelled: Subject = new Subject(); animationState = 'void'; override isRangePicker = false; _subs: Subscription[] = []; @@ -237,11 +237,11 @@ export class BsDatepickerContainerComponent } override apply(): void { - this.valueApplied.emit(); + this.valueApplied.next(); } override cancel(): void { - this.valueCancelled.emit(); + this.valueCancelled.next(); } ngOnDestroy(): void { From 7a3504edadf74bad2781835762616ec2316925cc Mon Sep 17 00:00:00 2001 From: Keanan Date: Fri, 9 Feb 2024 15:54:23 -0600 Subject: [PATCH 3/4] feat(datepicker): add apply button tests Add tests for new config options related to apply button Adjusted doc wording --- apps/ngx-bootstrap-docs/src/ng-api-doc.ts | 5 +- src/datepicker/testing/bs-datepicker.spec.ts | 93 ++++++++++++++++++++ 2 files changed, 96 insertions(+), 2 deletions(-) diff --git a/apps/ngx-bootstrap-docs/src/ng-api-doc.ts b/apps/ngx-bootstrap-docs/src/ng-api-doc.ts index fa48a4962d..7e7224f341 100644 --- a/apps/ngx-bootstrap-docs/src/ng-api-doc.ts +++ b/apps/ngx-bootstrap-docs/src/ng-api-doc.ts @@ -712,13 +712,14 @@ export const ngdoc: any = { name: 'showApplyButton', defaultValue: 'false', type: 'boolean', - description: '

Shows apply and cancel button. Selected date is not sent from view to model until the user selects apply. The picker will remain open until the selection is applied, cancelled, or the picker is dismissed.

\n' + description: + '

Shows 'apply' and 'cancel' button, date is not sent from view to model until user presses apply. Picker remains opened until applied, cancelled or dismissed (clicked off of)

\n' }, { name: 'showCancelButton', defaultValue: 'true', type: 'boolean', - description: '

Shows or hides the cancel button. This only applies if showApplyButton is true.

\n' + description: '

Shows 'cancel' button (only if showApplyButton is also true)

\n' }, { name: 'showClearButton', diff --git a/src/datepicker/testing/bs-datepicker.spec.ts b/src/datepicker/testing/bs-datepicker.spec.ts index 560a5abac3..c5e6dfde2d 100644 --- a/src/datepicker/testing/bs-datepicker.spec.ts +++ b/src/datepicker/testing/bs-datepicker.spec.ts @@ -178,6 +178,72 @@ describe('datepicker:', () => { expect(buttonText.filter(button => button === clearBtnCustomLbl).length).toEqual(1); }); + + it('should show the apply button when showApplyButton config is true', fakeAsync(() => { + const datepickerDirective = getDatepickerDirective(fixture); + datepickerDirective.bsConfig = { + showApplyButton: true + }; + showDatepicker(fixture); + tick(); + fixture.whenStable().then(() => { + const buttonText: string[] = []; + Array.from(document.body.getElementsByTagName('button')) + .forEach(button => buttonText.push(button.textContent)); + expect(buttonText.filter(button => button === 'Apply').length).toEqual(1); + }); + expect(true).toBeTruthy(); + })); + + it('should hide the cancel button when showApplyButton config is true and showCancelbutton config is false', fakeAsync(() => { + const datepickerDirective = getDatepickerDirective(fixture); + datepickerDirective.bsConfig = { + showApplyButton: true, + showCancelButton: false + }; + showDatepicker(fixture); + tick(); + fixture.whenStable().then(() => { + const buttonText: string[] = []; + Array.from(document.body.getElementsByTagName('button')) + .forEach(button => buttonText.push(button.textContent)); + expect(buttonText.filter(button => button === 'Cancel').length).toEqual(0); + }); + expect(true).toBeTruthy(); + })); + + it('should show custom label for apply button if set in config', () => { + const applyBtnCustomLbl = 'Apply date'; + const datepickerDirective = getDatepickerDirective(fixture); + datepickerDirective.bsConfig = { + applyButtonLabel: applyBtnCustomLbl, + showApplyButton: true + }; + showDatepicker(fixture); + + const buttonText: string[] = []; + // fixture.debugElement.queryAll(By.css('button')) + Array.from(document.body.getElementsByTagName('button')) + .forEach(button => buttonText.push(button.textContent)); + expect(buttonText.filter(button => button === applyBtnCustomLbl).length).toEqual(1); + }); + + it('should show custom label for cancel button if set in config', () => { + const cancelBtnCustomLbl = 'Cancel selection'; + const datepickerDirective = getDatepickerDirective(fixture); + datepickerDirective.bsConfig = { + cancelButtonLabel: cancelBtnCustomLbl, + showApplyButton: true, + }; + showDatepicker(fixture); + + const buttonText: string[] = []; + // fixture.debugElement.queryAll(By.css('button')) + Array.from(document.body.getElementsByTagName('button')) + .forEach(button => buttonText.push(button.textContent)); + expect(buttonText.filter(button => button === cancelBtnCustomLbl).length).toEqual(1); + }); + describe('should start with', () => { const parameters = [ @@ -228,6 +294,33 @@ describe('datepicker:', () => { }); }); + + it('should not emit date until applied', () => { + + const datepickerDirective = getDatepickerDirective(fixture); + datepickerDirective.bsConfig = { + showApplyButton: true + }; + const datepicker = showDatepicker(fixture); + const datepickerContainerInstance = getDatepickerContainer(datepicker); + + let emitValue: Date; + + const sub = datepicker.bsValueChange.subscribe(val => { + emitValue = val; + }); + + datepickerContainerInstance.setToday(); + fixture.detectChanges(); + expect(emitValue).toBe(undefined); + + datepickerContainerInstance.apply(); + fixture.detectChanges(); + expect(emitValue).not.toBe(undefined); + + sub.unsubscribe(); + }); + it('should set today date', () => { const datepicker = showDatepicker(fixture); const datepickerContainerInstance = getDatepickerContainer(datepicker); From 17460bcfaf84c537e42e811550a789e76b92923c Mon Sep 17 00:00:00 2001 From: Keanan Date: Tue, 20 Feb 2024 15:20:35 -0600 Subject: [PATCH 4/4] feat(datepicker): fix apply button with bsValue Fix issue where initial bsValue is not set correctly when show apply button is true --- src/datepicker/bs-datepicker.component.ts | 46 ++++++++++++----------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/datepicker/bs-datepicker.component.ts b/src/datepicker/bs-datepicker.component.ts index 7a7072f85c..7b2879d507 100644 --- a/src/datepicker/bs-datepicker.component.ts +++ b/src/datepicker/bs-datepicker.component.ts @@ -108,6 +108,7 @@ export class BsDatepickerDirective implements OnInit, OnDestroy, OnChanges, Afte private _datepickerRef?: ComponentRef; private readonly _dateInputFormat$ = new Subject(); private _externalValue?: Date; + private _unappliedValue?: Date; constructor(public _config: BsDatepickerConfig, private _elementRef: ElementRef, @@ -161,11 +162,6 @@ export class BsDatepickerDirective implements OnInit, OnDestroy, OnChanges, Afte this.initPreviousValue(); this._bsValue = value; - // if apply button is show don't update external source (model -> view) - if(this.bsConfig?.showApplyButton){ - return; - } - this.bsValueChange.emit(value); } @@ -255,27 +251,33 @@ export class BsDatepickerDirective implements OnInit, OnDestroy, OnChanges, Afte // if date changes from picker (view -> model) if (this._datepickerRef) { - this._subs.push( - this._datepickerRef.instance.valueChange.subscribe((value: Date) => { - this.initPreviousValue(); - this.bsValue = value; - if (this.keepDatepickerModalOpened()) { - return; - } - - if(this.bsConfig?.showApplyButton){ - return; - } - - this.hide(); - }) - ); + + if(!this.bsConfig?.showApplyButton){ + this._subs.push( + this._datepickerRef.instance.valueChange.subscribe((value: Date) => { + this.initPreviousValue(); + this.bsValue = value; + if (this.keepDatepickerModalOpened()) { + return; + } + + this.hide(); + }) + ); + } - // if apply button is shown update external source (view -> model) + // if apply button is shown update unappliedValue if(this.bsConfig?.showApplyButton){ + this._subs.push( + this._datepickerRef.instance.valueChange.subscribe((value: Date) => { + this._unappliedValue = value; + }) + ); + + // if apply button is pressed update external source (view -> model) this._subs.push( this._datepickerRef.instance.valueApplied.subscribe(() => { - this.bsValueChange.emit(this._bsValue); + this.bsValue = this._unappliedValue; this.hide(); })