Skip to content

Instantly share code, notes, and snippets.

@Eonasdan
Created August 22, 2022 13:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Eonasdan/81779e050c262841c429effa3b3fc00a to your computer and use it in GitHub Desktop.
Save Eonasdan/81779e050c262841c429effa3b3fc00a to your computer and use it in GitHub Desktop.
Tempus Dominus Angular directives

This some outdated code that I have to make my Tempus Dominus picker work with angular. I'll incorporate this into the docs at some point and update it. The code is not complete but the directives should help

Used as

<app-date-picker
          formControlName="startDate"
          [dateOnly]="true"
        ></app-date-picker>
<!--suppress XmlDuplicatedId, HtmlFormInputWithoutLabel -->
<app-field-wrapper
[label]="label"
[control]="control"
[controlName]="controlName"
>
<ng-container *ngIf="!readOnly">
<div
class="input-group date"
data-td-target-input="nearest"
data-td-target-toggle="nearest"
NgTempusDominus
>
<input
[class]="klass"
id="{{ controlName }}"
[formControl]="control"
[attr.readonly]="readOnly ? 'readonly' : null"
[ngClass]="{ 'is-invalid': isControlValid }"
[options]="options"
NgTempusDominusInput
type="text"
/>
<app-field-error-display [control]="control"></app-field-error-display>
<div
class="input-group-text"
data-td-target="#datetimepicker1"
data-td-toggle="datetimepicker"
>
<i class="fa-solid fa-calendar" aria-hidden="true"></i>
</div>
</div>
</ng-container>
<ng-container *ngIf="readOnly">
<input
type="text"
class="form-control-plaintext"
id="{{ controlName }}"
readonly
value="{{ formattedValue }}"
/>
</ng-container>
</app-field-wrapper>
import {
Component,
OnInit,
Input,
SimpleChanges,
OnChanges,
Self,
Optional,
} from '@angular/core';
import {
ControlContainer,
FormGroupDirective,
NgControl,
} from '@angular/forms';
import { BaseWrapperComponent } from '../base-wrapper.component';
import { LoggerService } from '../../../services/logger/logger.service';
import { Options, DateTime } from '@eonasdan/tempus-dominus';
import { ensureObjectPath } from '../../helpers';
@Component({
selector: 'app-date-picker',
templateUrl: './date-picker.component.html',
styleUrls: ['./date-picker.component.scss'],
viewProviders: [
{ provide: ControlContainer, useExisting: FormGroupDirective },
],
})
export class DatePickerComponent
extends BaseWrapperComponent
implements OnInit, OnChanges
{
@Input() options: Options = {} as Options;
@Input() dateOnly: boolean;
@Input() timeOnly: boolean;
formattedValue: string;
constructor(
logger: LoggerService,
@Self()
@Optional()
ngControl: NgControl
) {
super(logger, ngControl);
ensureObjectPath('display.components', this.options);
ensureObjectPath('display.icons', this.options);
}
ngOnInit() {
super.ngOnInit();
if (!this.klass) {
this.klass = `form-control`;
}
if (this.extraClasses) {
this.klass = `${this.klass} ${this.extraClasses}`;
}
if (!this.options?.display?.icons) {
this.options.display.icons = {
time: 'fa-solid fa-clock',
date: 'fa-solid fa-calendar',
up: 'fa-solid fa-arrow-up',
down: 'fa-solid fa-arrow-down',
previous: 'fa-solid fa-chevron-left',
next: 'fa-solid fa-chevron-right',
today: 'fa-solid fa-calendar-check-o',
clear: 'fa-solid fa-trash',
close: 'fa-solid fa-times',
type: 'icons',
};
}
this.formatValue();
this.control.valueChanges.subscribe(() => {
this.formatValue();
});
}
ngOnChanges(changes: SimpleChanges) {
super.ngOnChanges(changes);
ensureObjectPath('display.components', this.options || {});
ensureObjectPath('display.icons', this.options || {});
if (changes.dateOnly) {
this.options.display.components.calendar = true;
this.options.display.components.clock = false;
} else if (changes.timeOnly) {
this.options.display.components.calendar = false;
this.options.display.components.clock = true;
}
}
formatValue() {
const controlValue = this.control.value;
if (!controlValue) {
return;
}
this.formattedValue = new DateTime(controlValue).format({});
}
}
import {
ChangeDetectorRef,
Directive,
DoCheck,
ElementRef,
EventEmitter,
forwardRef,
HostListener,
Input,
KeyValueDiffer,
KeyValueDiffers,
OnDestroy,
OnInit,
Output,
} from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import {
Options,
DateTime,
TempusDominus,
Namespace,
} from '@eonasdan/tempus-dominus';
@Directive({
selector: '[NgTempusDominusInput]',
exportAs: 'NgTempusDominusBootstrapInput',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NgTempusDominusInputDirective),
multi: true,
},
],
})
export class NgTempusDominusInputDirective
implements OnInit, OnDestroy, DoCheck
{
_value: DateTime;
private _options: Options = {} as Options;
private tdSubscription: { unsubscribe: () => void };
@Input() set options(value: Options) {
if (value !== null) {
this._options = value;
}
}
get options(): any {
return this._options;
}
@Output() click: EventEmitter<any> = new EventEmitter<any>();
private dpInitialized: boolean;
private inputOnly: boolean;
datepicker: TempusDominus;
private optionsDiffer: KeyValueDiffer<string, any>;
private _onTouched: any = () => {};
private _onChange: any = () => {};
constructor(
private changeDetector: ChangeDetectorRef,
protected el: ElementRef,
private differs: KeyValueDiffers
) {
this.dpInitialized = false;
}
ngOnInit(): void {
const parent = this.el.nativeElement.parentNode;
this.inputOnly = !parent.classList.contains('input-group');
this.datepicker = new TempusDominus(
!this.inputOnly ? parent : this.el.nativeElement,
this.options
);
this.tdSubscription = this.datepicker.subscribe(
Namespace.events.change,
(e) => {
if (e.date && e.date !== this.value) {
this.value = e.date || null;
} /* else {
const date = moment(e.target.value, this.options.format);
if (date.isValid()) {
this.value = date;
}
}*/
}
) as any;
this.optionsDiffer = this.differs.find(this.options).create();
this.dpInitialized = true;
}
@HostListener('blur') onBlur() {
this._onTouched();
}
@HostListener('focus') onFocus() {
if (this.inputOnly) {
this.datepicker.show();
}
}
/**
* For click outside of input, for input only
* @param event event object
*/
@HostListener('document:click', ['$event'])
outsideClick(event): void {
let targetElement: HTMLElement = event?.target;
if (!targetElement || !this.inputOnly || this.options.inline) {
return;
}
const clickedInside = this.el.nativeElement.contains(targetElement);
if (!clickedInside) {
this.datepicker.hide();
}
}
get value() {
return this._value || null;
}
set value(val) {
this._value = val;
this._onChange(val);
if (val) {
this._onTouched();
}
this.changeDetector.markForCheck();
}
writeValue(value) {
// if we have a previous value and current value is falsy
// clear the picker
if (this._value && !value) {
this.value = null;
this.datepicker.clear();
}
this.value = value;
this.setDpValue(value);
}
registerOnChange(fn) {
this._onChange = fn;
}
registerOnTouched(fn: () => any): void {
this._onTouched = fn;
}
private setDpValue(val) {
if (!this.dpInitialized) {
return;
}
if (val) {
this.datepicker.dates.setValue(this.value, 0);
}
}
setDisabledState(isDisabled: boolean): void {
if (isDisabled) {
this.datepicker.disable();
return;
}
this.datepicker.enable();
}
ngDoCheck() {
if (this.dpInitialized) {
const changes = this.optionsDiffer.diff(this.options);
if (changes) {
this.datepicker.updateOptions(this.options);
}
}
}
ngOnDestroy(): void {
this.tdSubscription.unsubscribe();
this.datepicker?.dispose();
}
toggle(): void {
this.datepicker.toggle();
}
}
import {
ContentChild,
Directive,
ElementRef,
HostListener,
} from '@angular/core';
import { NgTempusDominusInputDirective } from './ng-tempus-dominus-input.directive';
/**
* Container
* */
@Directive({
selector: '[NgTempusDominus]',
})
export class NgTempusDominusDirective {
@ContentChild(NgTempusDominusInputDirective)
private _input: NgTempusDominusInputDirective;
/* /!**
* For click outside of container,
* @param event event object
* @param targetElement target element object
*!/
@HostListener('document:click', ['$event', '$event.target'])
outsideClick(event, targetElement: HTMLElement): void {
if (!targetElement) {
return;
}
const clickedInside = this.el.nativeElement.contains(targetElement);
if (!clickedInside) {
this._input.datepicker.hide();
}
}*/
constructor(private el: ElementRef) {}
toggle() {
this._input.toggle();
}
}
import {
Directive,
ElementRef,
forwardRef,
HostBinding,
HostListener,
Inject,
} from '@angular/core';
import { NgTempusDominusDirective } from './ng-tempus-dominus.directive';
/**
* Allows the datepicker to be toggled via click.
* */
@Directive({
selector: '[NgTempusDominusBootstrapToggle]',
})
export class NgTempusDominusBootstrapToggleDirective {
@HostBinding('style.cursor') cursor: String = 'pointer';
@HostListener('click') click() {
this.toggleOpen();
}
constructor(
@Inject(forwardRef(() => NgTempusDominusDirective))
public tempusDominus,
elementRef: ElementRef
) {}
toggleOpen() {
this.tempusDominus.toggle();
}
}
@apircher
Copy link

apircher commented May 6, 2023

It would be good to know what happens in BaseWrapperComponent and app-field-wrapper.

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