1

I couldn't find any DateTime picker for Angular 7. So I decided to combine the Date Picker and Time Picker

https://ng-bootstrap.github.io/#/components/datepicker

https://ng-bootstrap.github.io/#/components/timepicker

<ng-template #dateTimePicker>
  <ngb-datepicker #createdStartDate name="datepicker"></ngb-datepicker>
  <ngb-timepicker #createdStartTime name="timepicker" [meridian]="true"></ngb-timepicker>
</ng-template>

<form [formGroup]="managePromotionsForm" 
    <div class="col-md-6">
      <div class="row form-group">
        <label class="col-md-4 control-label" for="createdStartDate" translate="">Created From </label>
        <div class="col-md-6">
          <div class="input-group">
            <input readOnly class="form-control" id="createdStartDate" placeholder="From Date"
              [formControl]="controls['createdStartDate']">
              
              
            <div class="input-group-append">
              <button class="btn btn-outline-secondary calendar" [ngbPopover]="dateTimePicker"  type="button"></button>
            </div>
          </div>
        </div>
      </div>
    </div>
</form>

This is what I have so far

DateTimePicker

Now how to display the selected date and time on the createdStartDate textbox?

  • Reading the title of this question, see link: https://danielykpan.github.io/date-time-picker/ – Luuk Aug 30 '20 at 08:13

3 Answers3

2

If you want you can use a combined to ngbDropDown, ngbDatePicker and ngbTimePicker

For this you need two variables and one getter

  date: any;
  time:any= {hour:0,minute:0};

  _value;
  label;
  ngOnInit()
  {
    this.getDatetime()
  }
  getDatetime() {
    let value = null;
    if (!this.date) {
      if (!this.time) value = "yyyy/MM/dd hh:mm";
      else
        value =
          "yyyy/MM/dd " +
          ("0" + this.time.hour).slice(-2) +
          ":" +
          ("0" + this.time.minute).slice(-2);
    }
    if (!value) {
      value = new Date(Date.UTC(
        this.date.year,
        this.date.month - 1,
        this.date.day,
        this.time ? this.time.hour : 0,
        this.time ? this.time.minute : 0
      );
      this._value=value;
   } else 
      this._value=null

   this.form.get("control").setValue(this._value);
   this.label=value;
  }

<form [formGroup]="form">
  <div ngbDropdown>
  <button class="datepicker btn btn-link"  ngbDropdownToggle>{{_value?(_value|date:'medium'):label}}</button>
      <div ngbDropdownMenu >
        <ngb-datepicker #dp [(ngModel)]="date" (dateSelect)="getDatetime()"[ngModelOptions]="{standalone:true}" ></ngb-datepicker>
        <ngb-timepicker [ngModel]="time" (ngModelChange)="time=$event;getDatetime()"[ngModelOptions]="{standalone:true}"></ngb-timepicker>
      </div>
      </div>
  <button class="btn btn-primary">submit</button>

See in stackblitz

NOTE: This is a case that we would create create a custom form control to not make so dependency

Update for curiosity, in stackblitz I make the custom form control

Eliseo
  • 50,109
  • 4
  • 29
  • 67
  • Thank you so much for your answer. Also, how can I use `(change)` instead of `(ngModelChange)`? And what is `[ngModelOptions]="{standalone:true}"` for? – Nehal Jaisalmeria Aug 30 '20 at 18:09
  • 1
    the `[ngModelOptions]="{standalone:true}"` is necesary if you use [(ngModel)] inside a `
    ` and indicate Angular that the variables are not relationated with the FormGroup -else Angular give you an error - I don't remember if in Angular 7, sure in Angular 8 and next-. A ngbDatePicker or a ngbTimePicker has not event `(change)`, so you must use or `(dateSelect)` or `(ngModelChange)` to know when change happens
    – Eliseo Aug 30 '20 at 20:47
  • I'm getting this error: `ERROR Error: No value accessor for form control with name: 'control'` I am using FormBuilder. Please assist. @Eliseo – Nehal Jaisalmeria Sep 01 '20 at 07:34
  • see the line: `this.form.get("control").setValue(this._value);` change the `control` for the name of your propery. Anyway, you can also use the custom form control indicate in the last stackblitz – Eliseo Sep 01 '20 at 07:45
  • I have made the following changes: `constructor(private fb: FormBuilder) { } this.managePromotionsForm = this.fb.group({ createdStartDate: [''], }) this.managePromotionsForm.get("createdStartDate").setValue(this._value);` – Nehal Jaisalmeria Sep 01 '20 at 07:51
  • Where should I add `formControlName="createdStartDate"` on the template/HTML? @Eliseo – Nehal Jaisalmeria Sep 01 '20 at 08:12
  • If you're using the custom form control the answer is yes, else the answer is no:a FormGroup exist itselft, it's not necesary that the controls has an input. to avoid initials error you can add an if `if (this.managePromotionsForm && this.managePromotionsForm.get('createdStartDate'){this.managePromotionsForm.get('createdStartDate').setValue(this._value);}` – Eliseo Sep 01 '20 at 08:27
  • How to patch default values for `[hourStep]` and `[minuteStep]` for ngb-timepicker? – Nehal Jaisalmeria Sep 02 '20 at 08:37
  • I just add a new `@Input` to the custom form Control – Eliseo Sep 02 '20 at 11:05
2

There is an open issue as a feature request for this: https://github.com/ng-bootstrap/ng-bootstrap/issues/2086

The datepicker and timepicker can be combined with some custom code. enter image description here

Here is an example that uses a text input with a button to open the datepicker and toggle between the timepicker.

HTML:

<div class="input-group mr-2">
    <input
    [ngClass]="ngControl?.valid ? 'ng-valid' : 'ng-invalid'"
    class="form-control"
    (blur)="inputBlur($event)"
    [ngModel]="dateString | date:inputDatetimeFormat"
    (change)="onInputChange($event)"
    [disabled]="disabled"
  />

    <div class="input-group-append">
        <button
      class="btn btn-outline-secondary"
      [ngbPopover]="calendarContent"
      [disabled]="disabled"
      type="button"
    >
      <fa-icon [icon]="['far', 'calendar']"></fa-icon>
    </button>
    </div>
</div>

<ng-template #calendarContent>
    <div>
        <div *ngIf="!showTimePickerToggle">
            <ngb-datepicker id="dp" #dp name="datepicker" [ngModel]="datetime"
                (ngModelChange)="onDateChange($event, dp)"></ngb-datepicker>
            <button
        class="btn btn-block btn-outline-secondary"
        [disabled]="!datetime?.day"
        [ngbPopover]="timePickerContent"
        type="button"
        (click)="toggleDateTimeState($event)"
      >
        <fa-icon [icon]="['far', 'clock']"></fa-icon>
      </button>
        </div>
        <div *ngIf="showTimePickerToggle">
            <button
        class="btn btn-block btn-outline-secondary"
        [ngbPopover]="calendarContent"
        type="button"
        (click)="toggleDateTimeState($event)"
      >
        <fa-icon [icon]="['far', 'calendar']"></fa-icon>
      </button>
            <div class="mt-auto">
                <ngb-timepicker #tp name="timepicker" [ngModel]="datetime" (ngModelChange)="onTimeChange($event)"
                    [seconds]="seconds" [hourStep]="hourStep" [minuteStep]="minuteStep" [secondStep]="secondStep">
                </ngb-timepicker>
            </div>
        </div>
    </div>
</ng-template>

TS:

import {
  Component,
  OnInit,
  Input,
  forwardRef,
  ViewChild,
  AfterViewInit,
  Injector
} from "@angular/core";
import {
  NgbTimeStruct,
  NgbDateStruct,
  NgbPopoverConfig,
  NgbPopover,
  NgbDatepicker
} from "@ng-bootstrap/ng-bootstrap";
import {
  NG_VALUE_ACCESSOR,
  ControlValueAccessor,
  NgControl
} from "@angular/forms";
import { DatePipe } from "@angular/common";
import { DateTimeModel } from "./date-time.model";
import { noop } from "rxjs";

@Component({
  selector: "app-date-time-picker",
  templateUrl: "./date-time-picker.component.html",
  styleUrls: ["./date-time-picker.component.scss"],
  providers: [
    DatePipe,
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => DateTimePickerComponent),
      multi: true
    }
  ]
})
export class DateTimePickerComponent
  implements ControlValueAccessor, OnInit, AfterViewInit {
  @Input()
  dateString: string;

  @Input()
  inputDatetimeFormat = "M/d/yyyy H:mm:ss";
  @Input()
  hourStep = 1;
  @Input()
  minuteStep = 15;
  @Input()
  secondStep = 30;
  @Input()
  seconds = true;

  @Input()
  disabled = false;

  private showTimePickerToggle = false;

  private datetime: DateTimeModel = new DateTimeModel();
  private firstTimeAssign = true;

  // @ViewChild(NgbDatepicker, { static: true })
  // private dp: NgbDatepicker;

  @ViewChild(NgbPopover, { static: true })
  private popover: NgbPopover;

  private onTouched: () => void = noop;
  private onChange: (_: any) => void = noop;

  private ngControl: NgControl;

  constructor(private config: NgbPopoverConfig, private inj: Injector) {
    config.autoClose = "outside";
    config.placement = "auto";
  }

  ngOnInit(): void {
    this.ngControl = this.inj.get(NgControl);
  }

  ngAfterViewInit(): void {
    this.popover.hidden.subscribe($event => {
      this.showTimePickerToggle = false;
    });
  }

  writeValue(newModel: string) {
    if (newModel) {
      this.datetime = Object.assign(
        this.datetime,
        DateTimeModel.fromLocalString(newModel)
      );
      this.dateString = newModel;
      this.setDateStringModel();
    } else {
      this.datetime = new DateTimeModel();
    }
  }

  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  toggleDateTimeState($event) {
    this.showTimePickerToggle = !this.showTimePickerToggle;
    $event.stopPropagation();
  }

  setDisabledState?(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  onInputChange($event: any) {
    const value = $event.target.value;
    const dt = DateTimeModel.fromLocalString(value);

    if (dt) {
      this.datetime = dt;
      this.setDateStringModel();
    } else if (value.trim() === "") {
      this.datetime = new DateTimeModel();
      this.dateString = "";
      this.onChange(this.dateString);
    } else {
      this.onChange(value);
    }
  }

  onDateChange($event: string | NgbDateStruct, dp: NgbDatepicker) {
    const date = new DateTimeModel($event);

    if (!date) {
      this.dateString = this.dateString;
      return;
    }

    if (!this.datetime) {
      this.datetime = date;
    }

    this.datetime.year = date.year;
    this.datetime.month = date.month;
    this.datetime.day = date.day;

    const adjustedDate = new Date(this.datetime.toString());
    if (this.datetime.timeZoneOffset !== adjustedDate.getTimezoneOffset()) {
      this.datetime.timeZoneOffset = adjustedDate.getTimezoneOffset();
    }

    this.setDateStringModel();
  }

  onTimeChange(event: NgbTimeStruct) {
    this.datetime.hour = event.hour;
    this.datetime.minute = event.minute;
    this.datetime.second = event.second;

    this.setDateStringModel();
  }

  setDateStringModel() {
    this.dateString = this.datetime.toString();

    if (!this.firstTimeAssign) {
      this.onChange(this.dateString);
    } else {
      // Skip very first assignment to null done by Angular
      if (this.dateString !== null) {
        this.firstTimeAssign = false;
      }
    }
  }

  inputBlur($event) {
    this.onTouched();
  }
}
import { NgbTimeStruct, NgbDateStruct } from "@ng-bootstrap/ng-bootstrap";
import { DatePipe } from "@angular/common";

export interface NgbDateTimeStruct extends NgbDateStruct, NgbTimeStruct {}

export class DateTimeModel implements NgbDateTimeStruct {
  year: number;
  month: number;
  day: number;
  hour: number;
  minute: number;
  second: number;

  timeZoneOffset: number;

  public constructor(init?: Partial<DateTimeModel>) {
    Object.assign(this, init);
  }

  public static fromLocalString(dateString: string): DateTimeModel {
    const date = new Date(dateString);

    const isValidDate = !isNaN(date.valueOf());

    if (!dateString || !isValidDate) {
      return null;
    }

    return new DateTimeModel({
      year: date.getFullYear(),
      month: date.getMonth() + 1,
      day: date.getDate(),
      hour: date.getHours(),
      minute: date.getMinutes(),
      second: date.getSeconds(),
      timeZoneOffset: date.getTimezoneOffset()
    });
  }

  private isInteger(value: any): value is number {
    return (
      typeof value === "number" &&
      isFinite(value) &&
      Math.floor(value) === value
    );
  }

  public toString(): string {
    if (
      this.isInteger(this.year) &&
      this.isInteger(this.month) &&
      this.isInteger(this.day)
    ) {
      const year = this.year.toString().padStart(2, "0");
      const month = this.month.toString().padStart(2, "0");
      const day = this.day.toString().padStart(2, "0");

      if (!this.hour) {
        this.hour = 0;
      }
      if (!this.minute) {
        this.minute = 0;
      }
      if (!this.second) {
        this.second = 0;
      }
      if (!this.timeZoneOffset) {
        this.timeZoneOffset = new Date().getTimezoneOffset();
      }

      const hour = this.hour.toString().padStart(2, "0");
      const minute = this.minute.toString().padStart(2, "0");
      const second = this.second.toString().padStart(2, "0");

      const tzo = -this.timeZoneOffset;
      const dif = tzo >= 0 ? "+" : "-",
        pad = function(num) {
          const norm = Math.floor(Math.abs(num));
          return (norm < 10 ? "0" : "") + norm;
        };

      const isoString = `${pad(year)}-${pad(month)}-${pad(day)}T${pad(
        hour
      )}:${pad(minute)}:${pad(second)}${dif}${pad(tzo / 60)}:${pad(tzo % 60)}`;
      return isoString;
    }

    return null;
  }
}

Here is a link to a working example https://stackblitz.com/edit/angular-datetimepicker

Cirem
  • 840
  • 1
  • 11
  • 15
  • A link to a solution is welcome, but please ensure your answer is useful without it: [add context around the link](//meta.stackexchange.com/a/8259) so your fellow users will have some idea what it is and why it’s there, then quote the most relevant part of the page you're linking to in case the target page is unavailable. [Answers that are little more than a link may be deleted.](/help/deleted-answers) – USERNAME GOES HERE Dec 22 '20 at 20:39
1

If you want to create a form element that you can bind to. A better way is to create a separate component and implement control value accessor.

Then you can use the component as a form element and bind ngModal or form control to it.

Muhammad Kamran
  • 978
  • 6
  • 10