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

Popup datepicker with custom date picking function closes automatically after value select #1984

Closed
marcin-j opened this issue Nov 23, 2017 · 12 comments

Comments

@marcin-j
Copy link

Bug description:

Popup datepicker with custom date picking function closes automatically after value select even with preventing event from propagation etc. I think there should be an option to prevent closing datepicker on date select (e.g. if I want to implement range datepicker in popup its problematic).

Link to minimally-working plunker that reproduces the issue:

http://plnkr.co/edit/McF4tVd9D2fXNNx9QQxe?p=preview

Version of Angular, ng-bootstrap, and Bootstrap:

Angular: 4.1.3

ng-bootstrap: 1.0.0-beta.2

Bootstrap: 4.0.0-beta

@pkozlowski-opensource
Copy link
Member

@maxokorokov would you mind having a look?

@maxokorokov
Copy link
Member

Yep, by default the popup is closed when the date is selected, because of this subscription.

We currently don't have a dedicated range picker; I'm not sure how you want to handle <input> ←→ datepicker interaction currently, because you'll have to select multiple values. What are you planning to display in the input?

Maybe it would be easier to put NgbDatepicker in your own popup and ignore NgbInputDatepicker. Ranges implementation vary widely from case to case.

Otherwise looks like a valid request for me:

  • either add a flag in the NgbInputDatepicker API
  • or add a proper cancellable NgbDatepickerSelectEvent in both NgbDatepicker and NgbInputDatepicker

@lbrooks
Copy link

lbrooks commented Nov 29, 2017

The least invasive solution would seem to be to add a flag to the API (ex: autoClose: default true) and wrap the current ref listener as such:

// date selection event handling
this._cRef.instance.registerOnChange((selectedDate) => {
  this.writeValue(selectedDate);
  this._onChange(selectedDate);
  if (this.autoClose) {
    this.close();
  }
});

This would keep backwards compatibility intact and allow users to control when the popup is closed when needed.

@marcin-j
Copy link
Author

@lbrooks Yeah, I also thought to do that; because of the backwards compatibility.

@lbrooks
Copy link

lbrooks commented Nov 29, 2017

In conjunction with this, it would probably be best to allow customization of the input's label text.
Line 218:

this._renderer.setElementProperty(this._elRef.nativeElement, 'value', this._parserFormatter.format(model));

Perhaps it could be as simple as adding a function input?
Supply a function matching:

(currentSelection: NgbDate, parserFormatter: NgbDateParserFormatter): string => { ... }

The default value would be:

(currentSelection: NgbDate, parserFormatter: NgbDateParserFormatter): string => parserFormatter.format(currentSelection);

Users would be defining this function in an area that they would have access to all dates selected, so there would be no need to add that complexity to the function or the input class itself

@lbrooks
Copy link

lbrooks commented Nov 29, 2017

Just wanted to add a very simple example of the implementing code:

@ViewChild('datePicker') datePicker: NgbInputDatepicker;
fromDate: NgbDate;
toDate: NgbDate;
onFirstSelection = true;

selectDate(date: NgbDate) {
  if(this.onFirstSelection){
    this.fromDate = date;
    this.onFirstSelection = false;
  } else {
    this.toDate = date;
    this.onFirstSelection = true;
    this.datePicker.close();
  }
}

formatInputText(currentSelection: NgbDate, parserFormatter: NgbDateParserFormatter) {
  return `${this.fromDate ? parserFormatter.format(this.fromDate) : ''} - ${this.toDate ? parserFormatter.format(this.toDate) : ''}`;
}
<div class="date-picker-wrapper">
  <input class="form-control" placeholder="yyyy-mm-dd" name="dp" ngModel ngbDatepicker #datePicker="ngbDatepicker" (ngModelChange)="selectDate($event)" [dayTemplate]="t" [autoClose]="false" (formatLabel)="formatInputText($event.date, $event.parser)">
  <ng-template #t let-date="date" let-focused="focused">
    <span class="custom-day"
          [class.range]="isDateFrom(date) || isDateTo(date) || isDateInside(date) || isDateHovered(date)"
          [class.faded]="isDateHovered(date) || isDateInside(date)"
          (mouseenter)="hoveredDate = date"
          (mouseleave)="hoveredDate = null">
      {{ date.day }}
    </span>
  </ng-template>
</div>
<button class="input-group-addon" (click)="datePicker.toggle()" type="button">
  <span class="ln-icon-triangle-down"></span>
</button>

@Saif03
Copy link

Saif03 commented Feb 21, 2018

any workaround or update on this?

@drichard1989
Copy link

I am also experiencing this issue. Would love to just have it close when endDate doesnt equal null if endDate is available.

@maxokorokov
Copy link
Member

Could anybody please review the changes in #2192?

<input ngbDatepicker #d="ngbDatepicker" [autoClose]="false"
    ngModel (ngModelChange)="onDateChange($event, d)">

You can call d.close() whenever necessary.

P.S. I'll also introduce the (dateSelect) event to avoid using (ngModelChange) after this one is merged. It's already present in the non-input datepicker.

@pkozlowski-opensource pkozlowski-opensource removed this from the 1.0.1 milestone Mar 2, 2018
@maxokorokov maxokorokov added this to the 1.1.0 milestone Mar 14, 2018
@changhuixu
Copy link

+1 for supporting [autoclose]="false"

@sev-it
Copy link

sev-it commented Jun 30, 2018

I found workaround by overloading datepicker-input.js.
First - add your custom input prop in NgbInputDatepicker.propDecorators [line 310]:
"formatLabel": [{ type: Input },],
Second - add your custom input prop to initialization array NgbInputDatepicker.prototype._applyDatepickerInputs [line 246]:

['dayTemplate', 'displayMonths', 'firstDayOfWeek', 'markDisabled', 'minDate', 'maxDate', 'navigation',
            'outsideDays', 'showNavigation', 'showWeekdays', 'showWeekNumbers', 'formatLabel']

Third - correct NgbInputDatepicker.prototype._writeModelValue [line 264]:
oldValue:
this._renderer.setProperty(this._elRef.nativeElement, 'value',this._parserFormatter.format(model));
new Value:
this._renderer.setProperty(this._elRef.nativeElement, 'value', this.formatLabel || this._parserFormatter.format(model));

Finally - component.ts:

import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { NgbDateStruct, NgbCalendar, NgbInputDatepicker, NgbDateParserFormatter } from '@ng-bootstrap/ng-bootstrap';
import { NgbDateFRParserFormatter } from "./parser-formatter/ngb-date-fr-parser-formatter"

const equals = (one: NgbDateStruct, two: NgbDateStruct) =>
    one && two && two.year === one.year && two.month === one.month && two.day === one.day;

const before = (one: NgbDateStruct, two: NgbDateStruct) =>
    !one || !two ? false : one.year === two.year ? one.month === two.month ? one.day === two.day
        ? false : one.day < two.day : one.month < two.month
        : one.year < two.year;

const after = (one: NgbDateStruct, two: NgbDateStruct) =>
    !one || !two ? false : one.year === two.year ? one.month === two.month ? one.day === two.day
    ? false : one.day > two.day : one.month > two.month : one.year > two.year;
    @Component({
        selector: 'range-date-picker',
        templateUrl: './range-date-picker.component.html',
        styleUrls: ['./range-date-picker.component.scss'],
        providers: [{provide: NgbDateParserFormatter, useClass: NgbDateFRParserFormatter}]
    })

    export class RangeDatePickerComponent implements OnInit, OnDestroy {
        @ViewChild('datePicker') datePicker: NgbInputDatepicker;
        parserFormatter = new NgbDateFRParserFormatter();
        hoveredDate: NgbDateStruct;
        fromDate: NgbDateStruct;
        toDate: NgbDateStruct;
        filteredDate: Array<[number, number]> = new Array<[number, number]>();
        headerLabel: string = '';
        constructor(
            calendar: NgbCalendar
        ) {
            this.fromDate = calendar.getToday();
            this.toDate = calendar.getNext(calendar.getToday(), 'd', 10);
         }

         onDateSelection(date: NgbDateStruct, value) {
             if (!this.fromDate && !this.toDate) {
                 this.fromDate = date;
             } else if (this.fromDate && !this.toDate && after(date, this.fromDate)) {
                 this.toDate = date;
                 // Update value before closing event triggered
                 this.headerLabel = this.formatInputText([this.fromDate, this.toDate]);
                 setTimeout(()=> {
                    this.datePicker.close();
                 }, 1);
             } else {
                 this.toDate = null;
                 this.fromDate = date;
             }
             this.headerLabel = this.formatInputText([this.fromDate, this.toDate]);
         }

         formatInputText(value:[NgbDateStruct, NgbDateStruct]) {
             let result = `${value[0] ? this.parserFormatter.format(value[0]) : ''} - ${value[1] ? this.parserFormatter.format(value[1]) : ''}`;
             return result;
         }

         isHovered = date => this.fromDate && !this.toDate && this.hoveredDate && after(date, this.fromDate) && before(date, this.hoveredDate);
         isInside = date => after(date, this.fromDate) && before(date, this.toDate);
         isFrom = date => equals(date, this.fromDate);
         isTo = date => equals(date, this.toDate);

        ngOnInit() {

        }

        ngOnDestroy() {

        }
    }

component.html:

<input
       fxFlex
       name="dp"
       ngModel
       [displayMonths]="2"
       [dayTemplate]="t"
       [showWeekNumbers]="false"
       ngbDatepicker
       #datePicker="ngbDatepicker"
       [autoClose]="false"
       [formatLabel]="headerLabel">
         <ng-template #t let-date="date" let-focused="focused">
             <span class="custom-day"
                 [class.focused]="focused"
                 [class.range]="isFrom(date) || isTo(date) || isInside(date) || isHovered(date)"
                 [class.faded]="isHovered(date) || isInside(date)"
                 (mouseenter)="hoveredDate = date"
                 (mouseleave)="hoveredDate = null"
                 (click)="onDateSelection(date)">
                 {{ date.day }}
             </span>
         </ng-template>
  <div class="input-group-append">
    <button class="btn btn-outline-secondary" (click)="datePicker.toggle()" type="button">
      <img src="img/calendar-icon.svg" style="width: 1.2rem; height: 1rem; cursor: pointer;"/>
    </button>
  </div>

As you can see, my formatted date value is passed through a variable headerLabel:
[formatLabel]="headerLabel"

@DebuBabai
Copy link

$("#datetimepicker1").click(function () {
$(".datepicker-days .day").click(function () {
$('.bootstrap-datetimepicker-widget').hide();
});
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants