ngx-persian-datepicker-element
Version:
Angular wrapper for Persian DatePicker Web Component with signal-based inputs (English/Persian docs)
491 lines (484 loc) • 20.2 kB
JavaScript
import 'persian-datepicker-element/dist/persian-datepicker-element.min.js';
import { CommonModule } from '@angular/common';
import * as i0 from '@angular/core';
import { inject, ElementRef, NgZone, Injector, signal, input, EventEmitter, effect, forwardRef, ViewChild, Output, CUSTOM_ELEMENTS_SCHEMA, Component, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
/**
* Angular wrapper for the Persian DatePicker Web Component
*
* This component provides Angular bindings for the native Persian DatePicker web component,
* allowing it to be used with Angular forms (both reactive and template-driven) and
* custom event handling.
*
* Note: The component automatically imports and registers the persian-datepicker-element
* web component, so you don't need to add any scripts to your angular.json file.
*
* @example
* Basic usage:
* ```html
* <ngx-persian-datepicker-element
* placeholderInput="انتخاب تاریخ"
* formatInput="YYYY/MM/DD"
* [showEventsInput]="true"
* (dateChange)="onDateChange($event)">
* </ngx-persian-datepicker-element>
* ```
*
* With Angular forms:
* ```html
* <form [formGroup]="myForm">
* <ngx-persian-datepicker-element formControlName="date"></ngx-persian-datepicker-element>
* </form>
* ```
*/
class NgxPersianDatepickerComponent {
// Dependency injection using inject() function
elRef = inject(ElementRef);
zone = inject(NgZone);
injector = inject(Injector);
// Web component reference - expose it to be accessed from parent components
elementSignal = signal(null);
// Form control implementation as signals
onChangeSignal = signal(() => { });
onTouchSignal = signal(() => { });
valueSignal = signal(null);
disabledSignal = signal(false);
// #region Input Signals
placeholder = input(undefined);
format = input(undefined);
showEvents = input(undefined);
eventTypes = input(undefined);
rtl = input(undefined);
minDate = input(undefined);
maxDate = input(undefined);
disabledDates = input(undefined);
rangeMode = input(undefined);
rangeStart = input(undefined);
rangeEnd = input(undefined);
defaultDate = input(undefined);
// #endregion
// #region Outputs
dateChange = new EventEmitter();
// #endregion
/** Reference to the container where the web component will be attached */
containerRef;
constructor() {
// Create an effect to respond to changes in the element signal
effect(() => {
const element = this.elementSignal();
if (element) {
// Apply initial attributes and styles when the element is created
this.setInitialAttributes();
}
});
// Create effects for each input to update attributes when they change
this.setupInputEffects();
}
/**
* Apply theme variables to the datepicker element
* @param variables Object containing CSS variable names and values
*/
applyThemeVariables(variables) {
const element = this.elementSignal();
if (!element)
return;
for (const [prop, value] of Object.entries(variables)) {
element.style.setProperty(prop, value);
}
}
/**
* Set up effects for all inputs to update attributes/styles when they change
*/
setupInputEffects() {
// Placeholder
effect(() => {
const value = this.placeholder();
if (value !== undefined) {
this.updateAttribute('placeholder', value);
}
});
// Format
effect(() => {
const value = this.format();
if (value !== undefined) {
this.updateAttribute('format', value);
}
});
// Show Holidays
effect(() => {
const value = this.showEvents();
if (value !== undefined) {
this.updateAttribute('show-holidays', String(value));
}
});
// Holiday Types
effect(() => {
const value = this.eventTypes();
if (value !== undefined) {
this.updateeventTypesAttribute(value);
}
});
// RTL
effect(() => {
const value = this.rtl();
if (value !== undefined) {
this.updateAttribute('rtl', String(value));
}
});
// Range picker effects
effect(() => {
const value = this.rangeMode();
if (value !== undefined) {
this.updateAttribute('range-mode', String(value));
}
});
effect(() => {
const value = this.rangeStart();
if (value !== undefined) {
this.updateAttribute('range-start', JSON.stringify(value));
}
});
effect(() => {
const value = this.rangeEnd();
if (value !== undefined) {
this.updateAttribute('range-end', JSON.stringify(value));
}
});
// Add effects for new inputs
effect(() => {
const value = this.minDate();
if (value !== undefined) {
this.updateAttribute('min-date', JSON.stringify(value));
}
});
effect(() => {
const value = this.maxDate();
if (value !== undefined) {
this.updateAttribute('max-date', JSON.stringify(value));
}
});
effect(() => {
const value = this.disabledDates();
if (value !== undefined) {
if (typeof value === 'function') {
const element = this.elementSignal();
if (element && 'setDisabledDatesFn' in element) {
element.setDisabledDatesFn(value);
}
}
else {
this.updateAttribute('disabled-dates', value);
}
}
});
effect(() => {
const value = this.defaultDate();
if (value !== undefined) {
this.updateAttribute('default-date', JSON.stringify(value));
}
});
}
ngOnInit() {
// Create the web component instance
this.createWebComponent();
}
/**
* Create the web component instance
*/
createWebComponent() {
// Create the element
this.zone.runOutsideAngular(() => {
try {
// Make sure the web component is registered
// This will typically happen from the import, but we check just in case
if (typeof window !== 'undefined' && !customElements.get('persian-datepicker-element')) {
console.warn('persian-datepicker-element not found in custom elements registry. The import should have registered it.');
}
// Create the web component
const element = document.createElement('persian-datepicker-element');
// Add the change event listener
element.addEventListener('change', this.handleChangeEvent);
// Store the element in the signal
this.elementSignal.set(element);
// Append to the container
if (this.containerRef && this.containerRef.nativeElement) {
this.containerRef.nativeElement.appendChild(element);
}
else {
this.elRef.nativeElement.appendChild(element);
}
}
catch (error) {
console.error('Error creating persian-datepicker-element:', error);
}
});
}
// Helper method to update attributes
updateAttribute(name, value) {
const element = this.elementSignal();
if (element && value !== undefined) {
element.setAttribute(name, value);
}
}
// Helper method specifically for holiday types
updateeventTypesAttribute(value) {
const element = this.elementSignal();
if (element && value !== undefined) {
const formattedValue = Array.isArray(value) ? value.join(',') : value;
element.setAttribute('holiday-types', formattedValue);
}
}
ngOnDestroy() {
// Clean up event listeners
const element = this.elementSignal();
if (element) {
element.removeEventListener('change', this.handleChangeEvent);
}
}
/**
* Handle change events from the web component
*/
handleChangeEvent = (event) => {
// Use NgZone to ensure the change is detected by Angular
this.zone.run(() => {
// Get the selected date from the event detail
const { jalali, gregorian, isHoliday, events, isRange, range } = event.detail;
// Emit the dateChange event
this.dateChange.emit({
jalali,
gregorian,
isHoliday,
events,
isRange,
range
});
// Update form control value
this.valueSignal.set(jalali);
this.onChangeSignal()(jalali);
this.onTouchSignal()();
});
};
/**
* Set initial attributes on the web component based on the signals
*/
setInitialAttributes() {
const element = this.elementSignal();
if (!element)
return;
// Set attributes based on input signals
const placeholder = this.placeholder();
if (placeholder !== undefined) {
element.setAttribute('placeholder', placeholder);
}
const format = this.format();
if (format !== undefined) {
element.setAttribute('format', format);
}
const showEvents = this.showEvents();
if (showEvents !== undefined) {
element.setAttribute('show-holidays', String(showEvents));
}
const eventTypes = this.eventTypes();
if (eventTypes !== undefined) {
this.updateeventTypesAttribute(eventTypes);
}
const rtl = this.rtl();
if (rtl !== undefined) {
element.setAttribute('rtl', String(rtl));
}
const minDate = this.minDate();
if (minDate !== undefined) {
element.setAttribute('min-date', JSON.stringify(minDate));
}
const maxDate = this.maxDate();
if (maxDate !== undefined) {
element.setAttribute('max-date', JSON.stringify(maxDate));
}
const disabledDates = this.disabledDates();
if (disabledDates !== undefined) {
if (typeof disabledDates === 'function') {
element.setDisabledDatesFn(disabledDates);
}
else {
element.setAttribute('disabled-dates', disabledDates);
}
}
const rangeMode = this.rangeMode();
if (rangeMode !== undefined) {
element.setAttribute('range-mode', String(rangeMode));
}
const rangeStart = this.rangeStart();
if (rangeStart !== undefined) {
element.setAttribute('range-start', JSON.stringify(rangeStart));
}
const rangeEnd = this.rangeEnd();
if (rangeEnd !== undefined) {
element.setAttribute('range-end', JSON.stringify(rangeEnd));
}
const defaultDate = this.defaultDate();
if (defaultDate !== undefined) {
element.setAttribute('default-date', JSON.stringify(defaultDate));
}
// Set disabled state
if (this.disabledSignal()) {
element.setAttribute('disabled', '');
}
// Set initial value if needed
if (this.valueSignal()) {
this.writeValue(this.valueSignal());
}
}
// #region ControlValueAccessor Implementation
/**
* Write value to the component (used by Angular forms)
*/
writeValue(value) {
this.valueSignal.set(value);
const element = this.elementSignal();
if (element && value) {
// Set the value on the web component
const [year, month, day] = value;
element.setValue(year, month, day);
}
else if (element) {
// Clear the value
element.clear();
}
}
/**
* Register change callback (used by Angular forms)
*/
registerOnChange(fn) {
this.onChangeSignal.set(fn);
}
/**
* Register touch callback (used by Angular forms)
*/
registerOnTouched(fn) {
this.onTouchSignal.set(fn);
}
/**
* Set disabled state (used by Angular forms)
*/
setDisabledState(isDisabled) {
this.disabledSignal.set(isDisabled);
const element = this.elementSignal();
if (element) {
if (isDisabled) {
element.setAttribute('disabled', '');
}
else {
element.removeAttribute('disabled');
}
}
}
// #endregion
// Add range picker methods
setRange(start, end) {
const element = this.elementSignal();
if (element) {
element.setRange(start, end);
}
}
getRange() {
const element = this.elementSignal();
if (element) {
return element.getRange() || { start: null, end: null };
}
return { start: null, end: null };
}
clear() {
const element = this.elementSignal();
if (element) {
element.clear();
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: NgxPersianDatepickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.2.15", type: NgxPersianDatepickerComponent, isStandalone: true, selector: "ngx-persian-datepicker-element", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, showEvents: { classPropertyName: "showEvents", publicName: "showEvents", isSignal: true, isRequired: false, transformFunction: null }, eventTypes: { classPropertyName: "eventTypes", publicName: "eventTypes", isSignal: true, isRequired: false, transformFunction: null }, rtl: { classPropertyName: "rtl", publicName: "rtl", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, disabledDates: { classPropertyName: "disabledDates", publicName: "disabledDates", isSignal: true, isRequired: false, transformFunction: null }, rangeMode: { classPropertyName: "rangeMode", publicName: "rangeMode", isSignal: true, isRequired: false, transformFunction: null }, rangeStart: { classPropertyName: "rangeStart", publicName: "rangeStart", isSignal: true, isRequired: false, transformFunction: null }, rangeEnd: { classPropertyName: "rangeEnd", publicName: "rangeEnd", isSignal: true, isRequired: false, transformFunction: null }, defaultDate: { classPropertyName: "defaultDate", publicName: "defaultDate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dateChange: "dateChange" }, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NgxPersianDatepickerComponent),
multi: true
}
], viewQueries: [{ propertyName: "containerRef", first: true, predicate: ["container"], descendants: true, static: true }], ngImport: i0, template: '<div #container></div>', isInline: true, styles: [":host{display:block;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: ReactiveFormsModule }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: NgxPersianDatepickerComponent, decorators: [{
type: Component,
args: [{ selector: 'ngx-persian-datepicker-element', standalone: true, imports: [CommonModule, FormsModule, ReactiveFormsModule], schemas: [CUSTOM_ELEMENTS_SCHEMA], template: '<div #container></div>', providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NgxPersianDatepickerComponent),
multi: true
}
], styles: [":host{display:block;width:100%}\n"] }]
}], ctorParameters: () => [], propDecorators: { dateChange: [{
type: Output
}], containerRef: [{
type: ViewChild,
args: ['container', { static: true }]
}] } });
// Import is handled by index.ts to ensure it's only registered once
// The comment is kept for documentation purposes
/**
* Module for the Persian DatePicker Angular wrapper component.
*
* This module can be imported in traditional Angular applications that use modules.
* For standalone components, you can import the NgxPersianDatepickerComponent directly.
*
* The persian-datepicker web component is bundled directly with this package, so you
* don't need to add any extra scripts to your angular.json file.
*
* @example
* ```typescript
* // In your app.module.ts or feature module
* import { NgxPersianDatepickerModule } from 'ngx-persian-datepicker-element';
*
* @NgModule({
* imports: [
* // ... other imports
* NgxPersianDatepickerModule
* ],
* // ... declarations, providers, etc.
* })
* export class AppModule { }
* ```
*/
class NgxPersianDatepickerModule {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: NgxPersianDatepickerModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.15", ngImport: i0, type: NgxPersianDatepickerModule, imports: [CommonModule,
FormsModule,
ReactiveFormsModule,
NgxPersianDatepickerComponent // Import as a standalone component
], exports: [NgxPersianDatepickerComponent] });
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: NgxPersianDatepickerModule, imports: [CommonModule,
FormsModule,
ReactiveFormsModule,
NgxPersianDatepickerComponent // Import as a standalone component
] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: NgxPersianDatepickerModule, decorators: [{
type: NgModule,
args: [{
declarations: [],
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
NgxPersianDatepickerComponent // Import as a standalone component
],
exports: [
NgxPersianDatepickerComponent
],
schemas: [CUSTOM_ELEMENTS_SCHEMA] // Required for using custom elements in Angular templates
}]
}] });
// Register the web component first
// Use a more explicit import path to help bundlers resolve the package
/*
* Public API Surface of ngx-persian-datepicker-element
*/
/**
* Generated bundle index. Do not edit.
*/
export { NgxPersianDatepickerComponent, NgxPersianDatepickerModule };
//# sourceMappingURL=ngx-persian-datepicker-element.mjs.map