File

src/datepicker/datepicker.component.ts

Description

See demo

../../iframe.html?id=date-picker--single

Implements

OnDestroy OnChanges AfterViewChecked AfterViewInit

Metadata

encapsulation ViewEncapsulation.None
providers { provide: NG_VALUE_ACCESSOR, useExisting: DatePicker, multi: true }
selector ibm-date-picker
styles .dayContainer { justify-content: initial; }
template
<div class="bx--form-item">
	<div class="bx--form-item">
		<div
			class="bx--date-picker"
			[ngClass]="{
				'bx--date-picker--range' : range,
				'bx--date-picker--single' : !range,
				'bx--date-picker--light' : theme === 'light',
				'bx--skeleton' : skeleton
			}">
			<div class="bx--date-picker-container">
				<ibm-date-picker-input
					#input
					[label]="label"
					[placeholder]="placeholder"
					[pattern]="pattern"
					[id]="id"
					[type]="(range ? 'range' : 'single')"
					[hasIcon]="(range ? false : true)"
					[disabled]="disabled"
					[invalid]="invalid"
					[invalidText]="invalidText"
					[skeleton]="skeleton"
					(valueChange)="onValueChange($event)"
					(click)="openCalendar(input)">
				</ibm-date-picker-input>
			</div>

			<div *ngIf="range" class="bx--date-picker-container">
				<ibm-date-picker-input
					#rangeInput
					[label]="rangeLabel"
					[placeholder]="placeholder"
					[pattern]="pattern"
					[id]="id + '-rangeInput'"
					[type]="(range ? 'range' : 'single')"
					[hasIcon]="(range ? true : null)"
					[disabled]="disabled"
					[invalid]="invalid"
					[invalidText]="invalidText"
					[skeleton]="skeleton"
					(valueChange)="onRangeValueChange($event)"
					(click)="openCalendar(rangeInput)">
				</ibm-date-picker-input>
			</div>
		</div>
	</div>
</div>

Index

Properties
Methods
Inputs
Outputs
HostListeners
Accessors

Constructor

constructor(elementRef: ElementRef, elementService: ElementService)
Parameters :
Name Type Optional
elementRef ElementRef No
elementService ElementService No

Inputs

dateFormat

Format of date

For reference: https://flatpickr.js.org/formatting/

Default value : "m/d/Y"

disabled

Default value : false

flatpickrOptions
id

Default value : `datepicker-${DatePicker.datePickerCount++}`

invalid

Default value : false

invalidText

Type : string | TemplateRef<any>

label

Type : string | TemplateRef<any>

language

Language of the flatpickr calendar.

For reference of the possible locales: https://github.com/flatpickr/flatpickr/blob/master/src/l10n/index.ts

Default value : "en"

pattern

Default value : "^\\d{1,2}/\\d{1,2}/\\d{4}$"

placeholder

Default value : "mm/dd/yyyy"

plugins

Default value : []

range

Select calendar range mode

Default value : false

rangeLabel

Type : string

skeleton

Default value : false

theme

Type : "light" | "dark"

Default value : "dark"

value

Type : []

Outputs

valueChange $event Type: EventEmitter<any>

HostListeners

focusin
focusin()

Methods

Protected didDateValueChange
didDateValueChange(currentValue, previousValue)
Parameters :
Name Optional
currentValue No
previousValue No
Returns : boolean
Protected doSelect
doSelect(selectedValue: (Date | string)[])
Parameters :
Name Type Optional
selectedValue (Date | string)[] No
Returns : void
Protected isFlatpickrLoaded
isFlatpickrLoaded()

More advanced checking of the loaded state of flatpickr

Returns : boolean
ngAfterViewChecked
ngAfterViewChecked()
Returns : void
ngAfterViewInit
ngAfterViewInit()
Returns : void
ngOnChanges
ngOnChanges(changes: SimpleChanges)
Parameters :
Name Type Optional
changes SimpleChanges No
Returns : void
ngOnDestroy
ngOnDestroy()

Cleans up our flatpickr instance

Returns : void
onRangeValueChange
onRangeValueChange(event: string)

Handles the valueChange event from the range input

Parameters :
Name Type Optional
event string No
Returns : void
onValueChange
onValueChange(event: string)

Handles the valueChange event from the primary/single input

Parameters :
Name Type Optional
event string No
Returns : void
openCalendar
openCalendar(datepickerInput: DatePickerInput)

Handles opening the calendar "properly" when the calendar icon is clicked.

Parameters :
Name Type Optional
datepickerInput DatePickerInput No
Returns : void
registerOnChange
registerOnChange(fn: any)
Parameters :
Name Type Optional
fn any No
Returns : void
registerOnTouched
registerOnTouched(fn: any)
Parameters :
Name Type Optional
fn any No
Returns : void
Protected setDateValues
setDateValues(dates: (Date | string)[])

Applies the given date value array to both the flatpickr instance and the input(s)

Parameters :
Name Type Optional Description
dates (Date | string)[] No

the date values to apply

Returns : void
Protected updateCalendarListeners
updateCalendarListeners()
Returns : void
Protected updateClassNames
updateClassNames()

Carbon uses a number of specific classnames for parts of the flatpickr - this idempotent method applies them if needed.

Returns : void
writeValue
writeValue(value: (Date | string)[])

Writes a value from the model to the component. Expects the value to be null or (Date | string)[]

Parameters :
Name Type Optional Description
value (Date | string)[] No

value received from the model

Returns : void

Properties

Protected _flatpickrOptions
_flatpickrOptions: object
Type : object
Default value : { allowInput: true }
Protected _value
_value: []
Type : []
Default value : []
Private Static datePickerCount
datePickerCount: number
Type : number
Default value : 0
Protected flatpickrBaseOptions
flatpickrBaseOptions: object
Type : object
Default value : { mode: "single", dateFormat: "m/d/Y", plugins: this.plugins, onOpen: () => { this.updateClassNames(); this.updateCalendarListeners(); }, value: this.value }
Protected flatpickrInstance
flatpickrInstance: null
Type : null
Default value : null
input
input: DatePickerInput
Type : DatePickerInput
Decorators :
@ViewChild('input')
onTouched
onTouched: function
Type : function
Default value : () => {}
Protected preventCalendarClose
preventCalendarClose:
Default value : event => event.stopPropagation()
propagateChange
propagateChange:
Default value : (_: any) => {}
rangeInput
rangeInput: DatePickerInput
Type : DatePickerInput
Decorators :
@ViewChild('rangeInput')
Protected visibilitySubscription
visibilitySubscription:
Default value : new Subscription()

Accessors

value
getvalue()
setvalue(v: [])
Parameters :
Name Type Optional
v [] No
Returns : void
flatpickrOptions
getflatpickrOptions()
setflatpickrOptions(options)
Parameters :
Name Optional
options No
Returns : void
flatpickrOptionsRange
getflatpickrOptionsRange()
setflatpickrOptionsRange(options)
Parameters :
Name Optional
options No
Returns : void
import {
	Component,
	Input,
	Output,
	EventEmitter,
	ViewEncapsulation,
	ElementRef,
	OnDestroy,
	HostListener,
	TemplateRef,
	OnChanges,
	SimpleChanges,
	AfterViewChecked,
	AfterViewInit,
	ViewChild
} from "@angular/core";
import rangePlugin from "flatpickr/dist/plugins/rangePlugin";
import flatpickr from "flatpickr";
import { NG_VALUE_ACCESSOR } from "@angular/forms";
import { carbonFlatpickrMonthSelectPlugin } from "./carbon-flatpickr-month-select";
import { Subscription } from "rxjs";
import * as languages from "flatpickr/dist/l10n/index";
import { DatePickerInput } from "../datepicker-input/datepicker-input.component";
import { ElementService } from "../utils/element.service";

/**
 * [See demo](../../?path=/story/date-picker--single)
 *
 * <example-url>../../iframe.html?id=date-picker--single</example-url>
 */
@Component({
	selector: "ibm-date-picker",
	template: `
	<div class="bx--form-item">
		<div class="bx--form-item">
			<div
				class="bx--date-picker"
				[ngClass]="{
					'bx--date-picker--range' : range,
					'bx--date-picker--single' : !range,
					'bx--date-picker--light' : theme === 'light',
					'bx--skeleton' : skeleton
				}">
				<div class="bx--date-picker-container">
					<ibm-date-picker-input
						#input
						[label]="label"
						[placeholder]="placeholder"
						[pattern]="pattern"
						[id]="id"
						[type]="(range ? 'range' : 'single')"
						[hasIcon]="(range ? false : true)"
						[disabled]="disabled"
						[invalid]="invalid"
						[invalidText]="invalidText"
						[skeleton]="skeleton"
						(valueChange)="onValueChange($event)"
						(click)="openCalendar(input)">
					</ibm-date-picker-input>
				</div>

				<div *ngIf="range" class="bx--date-picker-container">
					<ibm-date-picker-input
						#rangeInput
						[label]="rangeLabel"
						[placeholder]="placeholder"
						[pattern]="pattern"
						[id]="id + '-rangeInput'"
						[type]="(range ? 'range' : 'single')"
						[hasIcon]="(range ? true : null)"
						[disabled]="disabled"
						[invalid]="invalid"
						[invalidText]="invalidText"
						[skeleton]="skeleton"
						(valueChange)="onRangeValueChange($event)"
						(click)="openCalendar(rangeInput)">
					</ibm-date-picker-input>
				</div>
			</div>
		</div>
	</div>
	`,
	styles: [
		`.dayContainer {
			justify-content: initial;
		}`
	],
	providers: [
		{
			provide: NG_VALUE_ACCESSOR,
			useExisting: DatePicker,
			multi: true
		}
	],
	encapsulation: ViewEncapsulation.None
})
export class DatePicker implements OnDestroy, OnChanges, AfterViewChecked, AfterViewInit {
	private static datePickerCount = 0;

	/**
	 * Select calendar range mode
	 */
	@Input() range = false;

	/**
	 * Format of date
	 *
	 * For reference: https://flatpickr.js.org/formatting/
	 */
	@Input() dateFormat = "m/d/Y";

	/**
	 * Language of the flatpickr calendar.
	 *
	 * For reference of the possible locales:
	 * https://github.com/flatpickr/flatpickr/blob/master/src/l10n/index.ts
	 */
	@Input() language = "en";

	@Input() label: string  | TemplateRef<any>;

	@Input() rangeLabel: string;

	@Input() placeholder = "mm/dd/yyyy";

	@Input() pattern = "^\\d{1,2}/\\d{1,2}/\\d{4}$";

	@Input() id = `datepicker-${DatePicker.datePickerCount++}`;

	@Input() set value(v: (Date | string)[]) {
		if (!v) {
			v = [];
		}
		this._value = v;
	}

	get value() {
		return this._value;
	}

	@Input() theme: "light" | "dark" = "dark";

	@Input() disabled = false;

	@Input() invalid = false;

	@Input() invalidText: string | TemplateRef<any>;

	@Input() skeleton = false;

	@Input() plugins = [];

	@Input()
	set flatpickrOptions(options) {
		this._flatpickrOptions = Object.assign({}, this._flatpickrOptions, options);
	}
	get flatpickrOptions() {
		const plugins = [...this.plugins, carbonFlatpickrMonthSelectPlugin];
		if (this.range) {
			plugins.push(rangePlugin({ input: `#${this.id}-rangeInput`, position: "left"}));
		}
		return Object.assign({}, this._flatpickrOptions, this.flatpickrBaseOptions, {
			mode: this.range ? "range" : "single",
			plugins,
			dateFormat: this.dateFormat,
			locale: languages.default[this.language]
		});
	}

	@ViewChild("input") input: DatePickerInput;

	@ViewChild("rangeInput") rangeInput: DatePickerInput;

	set flatpickrOptionsRange (options) {
		console.warn("flatpickrOptionsRange is deprecated, use flatpickrOptions and set the range to true instead");
		this.range = true;
		this.flatpickrOptions = options;
	}
	get flatpickrOptionsRange () {
		console.warn("flatpickrOptionsRange is deprecated, use flatpickrOptions and set the range to true instead");
		return this.flatpickrOptions;
	}

	@Output() valueChange: EventEmitter<any> = new EventEmitter();

	protected _value = [];

	protected _flatpickrOptions = {
		allowInput: true
	};

	protected flatpickrBaseOptions = {
		mode: "single",
		dateFormat: "m/d/Y",
		plugins: this.plugins,
		onOpen: () => {
			this.updateClassNames();
			this.updateCalendarListeners();
		},
		value: this.value
	};

	protected flatpickrInstance = null;

	protected visibilitySubscription = new Subscription();

	constructor(protected elementRef: ElementRef, protected elementService: ElementService) { }

	ngOnChanges(changes: SimpleChanges) {
		if (this.isFlatpickrLoaded()) {
			let dates = this.flatpickrInstance.selectedDates;
			if (changes.value && this.didDateValueChange(changes.value.currentValue, changes.value.previousValue)) {
				dates = changes.value.currentValue;
			}
			// only reset the flatpickr instance on Input changes
			this.flatpickrInstance = flatpickr(`#${this.id}`, this.flatpickrOptions);
			this.setDateValues(dates);
		}
	}

	ngAfterViewInit() {
		this.visibilitySubscription = this.elementService
			.visibility(this.elementRef.nativeElement, this.elementRef.nativeElement)
			.subscribe(value => {
				if (this.isFlatpickrLoaded() && this.flatpickrInstance.isOpen) {
					this.flatpickrInstance._positionCalendar(this.elementRef.nativeElement.querySelector(`#${this.id}`));
					if (!value.visible) {
						this.flatpickrInstance.close();
					}
				}
			});
	}

	// because the actual view may be delayed in loading (think projection into a tab pane)
	// and because we rely on a library that operates outside the Angular view of the world
	// we need to keep trying to load the library, until the relevant DOM is actually live
	ngAfterViewChecked() {
		if (!this.isFlatpickrLoaded()) {
			this.flatpickrInstance = flatpickr(`#${this.id}`, this.flatpickrOptions);
			// if (and only if) the initialization succeeded, we can set the date values
			if (this.isFlatpickrLoaded()) {
				if (this.value.length > 0) {
					this.setDateValues(this.value);
				}
			}
		}
	}

	@HostListener("focusin")
	onFocus() {
		// Updates the month manually when calendar mode is range because month
		// will not update properly without manually updating them on focus.
		if (this.range) {
			if (this.rangeInput.input.nativeElement === document.activeElement && this.flatpickrInstance.selectedDates[1]) {
				const currentMonth = this.flatpickrInstance.selectedDates[1].getMonth();
				this.flatpickrInstance.changeMonth(currentMonth, false);
			} else if (this.input.input.nativeElement === document.activeElement && this.flatpickrInstance.selectedDates[0]) {
				const currentMonth = this.flatpickrInstance.selectedDates[0].getMonth();
				this.flatpickrInstance.changeMonth(currentMonth, false);
			}
		}

		this.onTouched();
	}

	/**
	 * Writes a value from the model to the component. Expects the value to be `null` or `(Date | string)[]`
	 * @param value value received from the model
	 */
	writeValue(value: (Date | string)[]) {
		this.value = value;
		if (this.isFlatpickrLoaded() && this.flatpickrInstance.config) {
			this.setDateValues(this.value);
		}
	}

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

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

	onTouched: () => any = () => {};

	propagateChange = (_: any) => {};

	/**
	 * Cleans up our flatpickr instance
	 */
	ngOnDestroy() {
		if (!this.isFlatpickrLoaded()) { return; }
		this.flatpickrInstance.destroy();
		this.visibilitySubscription.unsubscribe();
	}

	/**
	 * Handles the `valueChange` event from the primary/single input
	 */
	onValueChange(event: string) {
		if (this.isFlatpickrLoaded()) {
			const date = this.flatpickrInstance.parseDate(event, this.dateFormat);
			if (this.range) {
				this.setDateValues([date, this.flatpickrInstance.selectedDates[1]]);
			} else {
				this.setDateValues([date]);
			}
			this.doSelect(this.flatpickrInstance.selectedDates);
		}
	}

	/**
	 * Handles the `valueChange` event from the range input
	 */
	onRangeValueChange(event: string) {
		if (this.isFlatpickrLoaded()) {
			const date = this.flatpickrInstance.parseDate(event, this.dateFormat);
			this.setDateValues([this.flatpickrInstance.selectedDates[0], date]);
			this.doSelect(this.flatpickrInstance.selectedDates);
		}
	}

	/**
	 * Handles opening the calendar "properly" when the calendar icon is clicked.
	 */
	openCalendar(datepickerInput: DatePickerInput) {
		if (this.range) {
			datepickerInput.input.nativeElement.click();

			// If the first input's calendar icon is clicked when calendar is in range mode, then
			// the month and year needs to be manually changed to the current selected month and
			// year otherwise the calendar view will not be updated upon opening.
			if (datepickerInput === this.input && this.flatpickrInstance.selectedDates[0]) {
				const currentMonth = this.flatpickrInstance.selectedDates[0].getMonth();

				this.flatpickrInstance.currentYear = this.flatpickrInstance.selectedDates[0].getFullYear();
				this.flatpickrInstance.changeMonth(currentMonth, false);
			}
		} else {
			// Single-mode flatpickr handles mousedown but not click, so nativeElement.click() won't
			// work when the calendar icon is clicked. In this case we simply use flatpickr.open().
			this.flatpickrInstance.open();
		}
	}

	protected updateCalendarListeners() {
		const calendarContainer = document.querySelectorAll(".flatpickr-calendar");
		Array.from(calendarContainer).forEach(calendar => {
			calendar.removeEventListener("click", this.preventCalendarClose);
			calendar.addEventListener("click", this.preventCalendarClose);
		});
	}

	/**
	 * Carbon uses a number of specific classnames for parts of the flatpickr - this idempotent method applies them if needed.
	 */
	protected updateClassNames() {
		if (!this.elementRef) { return; }
		// get all the possible flatpickrs in the document - we need to add classes to (potentially) all of them
		const calendarContainer = document.querySelectorAll(".flatpickr-calendar");
		const monthContainer = document.querySelectorAll(".flatpickr-month");
		const weekdaysContainer = document.querySelectorAll(".flatpickr-weekdays");
		const weekdayContainer = document.querySelectorAll(".flatpickr-weekday");
		const daysContainer = document.querySelectorAll(".flatpickr-days");
		const dayContainer = document.querySelectorAll(".flatpickr-day");

		// add classes to lists of elements
		const addClassIfNotExists = (classname: string, elementList: NodeListOf<Element>) => {
			Array.from(elementList).forEach(element => {
				if (!element.classList.contains(classname)) {
					element.classList.add(classname);
				}
			});
		};

		// add classes (but only if they don't exist, small perf win)
		addClassIfNotExists("bx--date-picker__calendar", calendarContainer);
		addClassIfNotExists("bx--date-picker__month", monthContainer);
		addClassIfNotExists("bx--date-picker__weekdays", weekdaysContainer);
		addClassIfNotExists("bx--date-picker__days", daysContainer);

		// add weekday classes and format the text
		Array.from(weekdayContainer).forEach(element => {
			element.innerHTML = element.innerHTML.replace(/\s+/g, "");
			element.classList.add("bx--date-picker__weekday");
		});

		// add day classes and special case the "today" element based on `this.value`
		Array.from(dayContainer).forEach(element => {
			element.classList.add("bx--date-picker__day");
			if (!this.value) {
				return;
			}
			if (element.classList.contains("today") && this.value.length > 0) {
				element.classList.add("no-border");
			} else if (element.classList.contains("today") && this.value.length === 0) {
				element.classList.remove("no-border");
			}
		});
	}

	/**
	 * Applies the given date value array to both the flatpickr instance and the `input`(s)
	 * @param dates the date values to apply
	 */
	protected setDateValues(dates: (Date | string)[]) {
		if (this.isFlatpickrLoaded()) {
			const singleInput = this.elementRef.nativeElement.querySelector(`#${this.id}`);
			const rangeInput = this.elementRef.nativeElement.querySelector(`#${this.id}-rangeInput`);

			// set the date on the instance
			this.flatpickrInstance.setDate(dates);

			// we can either set a date value or an empty string, so we start with an empty string
			let singleDate = "";
			// if date is a string, parse and format
			if (typeof this.flatpickrInstance.selectedDates[0] === "string") {
				singleDate = this.flatpickrInstance.parseDate(this.flatpickrInstance.selectedDates[0], this.dateFormat);
				singleDate = this.flatpickrInstance.formatDate(singleDate, this.dateFormat);
			// if date is not a string we can assume it's a Date and we should format
			} else if (!!this.flatpickrInstance.selectedDates[0]) {
				singleDate = this.flatpickrInstance.formatDate(this.flatpickrInstance.selectedDates[0], this.dateFormat);
			}

			if (rangeInput) {
				// we can either set a date value or an empty string, so we start with an empty string
				let rangeDate = "";
				// if date is a string, parse and format
				if (typeof this.flatpickrInstance.selectedDates[1] === "string") {
					rangeDate = this.flatpickrInstance.parseDate(this.flatpickrInstance.selectedDates[1].toString(), this.dateFormat);
					rangeDate = this.flatpickrInstance.formatDate(rangeDate, this.dateFormat);
				// if date is not a string we can assume it's a Date and we should format
				} else if (!!this.flatpickrInstance.selectedDates[1]) {
					rangeDate = this.flatpickrInstance.formatDate(this.flatpickrInstance.selectedDates[1], this.dateFormat);
				}
				setTimeout(() => {
					// apply the values
					rangeInput.value = rangeDate;
					singleInput.value = singleDate;
				});
			}
		}
	}

	protected preventCalendarClose = event => event.stopPropagation();

	protected doSelect(selectedValue: (Date | string)[]) {
		this.valueChange.emit(selectedValue);
		this.propagateChange(selectedValue);
	}

	protected didDateValueChange(currentValue, previousValue) {
		return currentValue[0] !== previousValue[0] || currentValue[1] !== previousValue[1];
	}

	/**
	 * More advanced checking of the loaded state of flatpickr
	 */
	protected isFlatpickrLoaded() {
		// cast the instance to a boolean, and some method that has to exist for the library to be loaded in this case `setDate`
		return !!this.flatpickrInstance && !!this.flatpickrInstance.setDate;
	}
}
.dayContainer {
			justify-content: initial;
		}
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""