Last active
November 25, 2022 17:41
-
-
Save dmorosinotto/43703bdd5fadf01b55529b172fc62e13 to your computer and use it in GitHub Desktop.
Base custom FormControl (NG_VALUE_ACCESSOR implementation) compatible with [(two-way)] + [(ngModel)] + ReactiveForms / ngx-formly ^_^
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { ChangeDetectionStrategy, Directive, EventEmitter, Input, OnDestroy, Output } from "@angular/core"; | |
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; | |
import { Subject } from "rxjs"; | |
@Directive(/*{ | |
selector: "base-formctrl", | |
standalone: true, | |
// templateUrl: "./n-date-picker.component.html", //IL TEMPLATE VA SPECIFICATO SULLA @Component OSSIA LA CLASSE EREDITATA | |
// styleUrls: ["./n-date-picker.component.css"], //GLI STYLE VANNO SPECIFICATI SULLA @Component OSSIA LA CLASSE EREDITATA | |
// providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: NBaseFormCtrlComponent, multi: true }], //PURTROPPO QUESTO NON FUNZIONA SULLA ABSTRACT DEVO FARE IL PROVIDER NELLA CLASSE EREDITARE | |
changeDetection: ChangeDetectionStrategy.OnPush, | |
}*/) | |
export abstract class BaseFormCtrlComponent<V = string, M = any> implements ControlValueAccessor { | |
constructor() { | |
console.log(this.constructor.name, "on ctor"); | |
} | |
writeValue(value: any): void { | |
console.info(this.constructor.name, "writeValue", value); | |
this.value = value; | |
} | |
private _OnChange?: (_: any) => void; | |
registerOnChange(fn: (_: any) => void): void { | |
console.info(this.constructor.name, "registerOnChange", fn); | |
this._OnChange = fn; | |
} | |
private _OnTouhed?: () => void; | |
registerOnTouched(fn: () => void): void { | |
console.info(this.constructor.name, "registerOnTouched", fn); | |
this._OnTouhed = fn; | |
} | |
protected _disabled = false; | |
setDisabledState(isDisabled: boolean): void { | |
console.info(this.constructor.name, "setDisabledState", isDisabled); | |
//CALL OVERIDABLE LOGIC _onDisabling TO GET BACK disabled FLAG | |
this._disabled = this._onDisabling(isDisabled); | |
} | |
protected _model?: M | null; //INTERNAL MODEL <==> value WITH FUNCS | |
protected abstract _parseValueToModel(value: V | null): M | null; | |
protected abstract _formatModelToValue(model: M | null): V | null; | |
protected _onDisabling(disabled: boolean) { | |
return disabled; //OVERIDES IF NEED CUSTOM LOGIC IN setDisabled | |
} | |
protected _onChanging(from: "value" | "model", changed: boolean) { | |
return changed; //OVERRIDES IF NEED CUSTOM LOGIN IN _update | |
} | |
@Input() set value(value: V | null) { | |
if (value === undefined) { | |
console.info(this.constructor.name, "IGNORE MODEL NOT SET === undefined"); | |
return; | |
} | |
if (!this._issame(this._value, value)) { | |
console.info(this.constructor.name, "SET VALUE", value); | |
this._update("value", value); | |
} else { | |
console.info(this.constructor.name, value, "VALUE IS SAME"); | |
} | |
} | |
get value(): V | null { | |
return this._value!; | |
} | |
@Output() valueChange = new EventEmitter<V | null>(); | |
private _value?: V | null; | |
@Input() immutable?: boolean; | |
protected _issame = (old: any, cur: any) => (this.immutable && old === cur) || JSON.stringify(old) == JSON.stringify(cur); | |
protected _update(from: "value", what: V | null): void; | |
protected _update(from: "model", what: M | null): void; | |
protected _update(from: "value" | "model", what: unknown | null) { | |
if (what === undefined) return; //DO NOTHING IF value IS NOT SPECIFIED | |
const old = this._value!; | |
//INSERNAL SET STATE KEEP IN SYNC _model <==> value | |
if (from === "value") { | |
var value = what as V; | |
this._value = value; | |
this._model = this._parseValueToModel(value); | |
} else { | |
var model = what as M; | |
this._model = model; | |
this._value = this._formatModelToValue(model); | |
} | |
//CALL OVERIDABLE LOGIC _onChangeing TO GET BACK changed FLAG | |
const changed = this._onChanging(from, !this._issame(old, this._value)); | |
if (from !== "value") { | |
console.info(this.constructor.name, "EMIT value CHANGE"); | |
this.valueChange.emit(this._value); | |
} | |
if (this._OnChange && changed) { | |
console.info(this.constructor.name, "EMIT _OnChange", this._value); | |
this._OnChange(this._value); | |
} | |
if (this._OnTouhed && changed) { | |
console.info(this.constructor.name, "EMIT _OnTouhed"); | |
this._OnTouhed(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
AfterViewInit, | |
ChangeDetectionStrategy, | |
Component, | |
ElementRef, | |
EventEmitter, | |
Input, | |
OnDestroy, | |
OnChanges, | |
OnInit, | |
Output, | |
SimpleChanges, | |
ViewChild, | |
} from "@angular/core"; | |
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; | |
import { CommonModule } from "@angular/common"; | |
import { NBaseFormCtrlComponent } from "./BaseFormCtrl.ts"; | |
declare var $: any; | |
const UTC_FORMAT = "yy-mm-ddZ"; | |
type stringUTC = string; | |
@Component({ | |
selector: "date-picker", | |
standalone: true, | |
// imports: [CommonModule], | |
template: ` | |
<div> | |
<input type="text" #inp (input)="parse()" /> | |
<button type="button" id="show" (click)="dialog($event)">#</button> | |
<span>Model: {{ model }} <-> Value: {{ value }}</span> | |
</div> | |
`, | |
styles: ["div { display: flex }"], | |
providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: NDatePickerComponent, multi: true }], | |
changeDetection: ChangeDetectionStrategy.OnPush, | |
}) | |
export class DatePickerComponent extends BaseFormCtrlComponent<stringUTC, Date> implements OnInit, OnChanges, OnDestroy, AfterViewInit { | |
constructor() { | |
super(); | |
console.log(this.constructor.name, "on ctor", this.inp?.nativeElement ?? "NOT READY!"); | |
} | |
override _onDisabling(disabled: boolean): boolean { | |
//CUSTOM LOGIC TO HANDLE disabled AND RETURN disabled FLAG | |
$(this.inp.nativeElement).datepicker("option", "disabled", disabled); | |
$(this.inp.nativeElement).prop("disabled", disabled); | |
return super._onDisabling(disabled); | |
} | |
protected _parseValueToModel(value: string | null): Date | null { | |
//CUSTOM LOGIC TO SET INTERNAL _model WHEN value IS CHANGE/SETED | |
return value ? $.datepicker.parseDate(UTC_FORMAT, value) : null; | |
} | |
protected _formatModelToValue(model: Date | null): string | null { | |
//CUSTOM LOGIC TO SET BACK value WHEN INTERNAL _model IS CHANGED | |
return model ? $.datepicker.formatDate(UTC_FORMAT, model) : null; | |
} | |
@Input() format = "dd/mm/yy"; //FORMAT STRING TO SHOW DATE | |
//ESPOSE THE INTERNAL MODEL FOR TWO-WAY DATABIND [(model)] | |
@Input() set model(model: Date | null) { | |
if (model === undefined) { | |
console.info(this.constructor.name, "IGNORE MODEL NOT SET === undefined"); | |
return; | |
} | |
if (!this._issame(this._model, model)) { | |
console.info(this.constructor.name, "SET MODEL", model); | |
this._nofixinp = false; | |
this._update("model", model); | |
} else { | |
console.info(this.constructor.name, model, "MODEL IS SAME"); | |
} | |
} | |
get model(): Date | null { | |
return this._model!; | |
} | |
@Output() modelChange = new EventEmitter<Date | null>(); | |
private _nofixinp = false; | |
@Output() change = new EventEmitter<{ text: string; value: stringUTC | null; model: Date | null; old: Date | null }>(); | |
override _onChanging(from: "value" | "model", changed: boolean): boolean { | |
const old = this.model; | |
const text = $.datepicker.formatDate(this.format, this.model); | |
if (from != "model") { | |
console.info(this.constructor.name, "EMIT model CHANGE"); | |
this.modelChange.emit(this.model); | |
} | |
if (changed) { | |
this.change.emit({ value: this.value, text, model: this.model, old }); | |
} | |
if (this._nofixinp) { | |
setTimeout(() => (this.inp.nativeElement.value = text)); | |
this._nofixinp = false; | |
} | |
return changed; | |
} | |
ngOnChanges(changes: SimpleChanges) { | |
console.log(this.constructor.name, "on ngOnChange", this.inp?.nativeElement ?? "NOT READY!"); | |
} | |
ngOnInit() { | |
console.log(this.constructor.name, "on ngOnInit", this.inp?.nativeElement ?? "NOT READY!"); | |
} | |
ngAfterViewInit(): void { | |
console.log(this.constructor.name, "on ngAfterViewInit", this.inp?.nativeElement ?? "NOT READY!"); | |
//$(function () { | |
// $("#sampleDTPicker").datepicker({ showButtonPanel: true, showOn: "button", dateFormat: this.format }); | |
// var str = $.datepicker.formatDate("yy-mm-dd", new Date(1975, 3 - 1, 20)); //LOCALTIME | |
// var d = $.datepicker.parseDate("yy-mm-dd", str); //LOCALTIME | |
// console.log("format", str, "parse", d, "UTC", d.toISOString()); | |
//}); | |
} | |
@ViewChild("inp", { read: ElementRef, static: true }) inp!: ElementRef; | |
parse() { | |
var inputText = this.inp.nativeElement.value; | |
console.log(this.constructor.name, this.inp, "INPUT.value", inputText); | |
var model: Date | undefined; | |
try { | |
model = $.datepicker.parseDate(this.format, inputText); | |
} catch (err) { | |
// console.info("PARSING ERROR", err); | |
model = undefined; | |
} | |
// this.valueUTC = $.datepicker.formatDate(UTC_FORMAT, D); | |
this._nofixinp = true; | |
this._update("model", model || null); | |
} | |
dialog(e: MouseEvent) { | |
console.log(this.constructor.name, "$event", e); | |
this.init = $(e.target).datepicker( | |
"dialog", | |
this.value, | |
(...args: any[]) => { | |
console.log(this.constructor.name, "OnSelect", args); | |
//var model = $.datepicker.parseDate(UTC_FORMAT, args[0]); //STRING FORMATTED | |
var model: Date | null = $(args[1].input).datepicker("getDate"); //DATE OBJECT LT | |
// this.valueUTC = args[0]; //STRING FORMATTED | |
this._nofixinp = false; | |
this._update("model", model); | |
console.log(this.constructor.name, "model", model); | |
}, | |
{ showButtonPanel: true, dateFormat: UTC_FORMAT, altField: this.inp.nativeElement, altFormat: this.format }, | |
[e.clientX, e.clientY] | |
); | |
} | |
private init: any; | |
ngOnDestroy(): void { | |
if (this.init) | |
try { | |
this.init.destroy(); | |
this.init.options.OnSelect = function () {}; | |
} catch (err) { | |
console.error(this.constructor.name, "ERROR ngOnDestroy", err); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Component, ChangeDetectionStrategy } from "@angular/core"; | |
import { CommonModule } from "@angular/common"; | |
import { DatePickerComponent } from "./date-picker.component"; | |
import { FormsModule } from "@angular/forms"; | |
@Component({ | |
selector: "sample-use-page", | |
standalone: true, | |
imports: [CommonModule, FormsModule, DatePickerComponent], | |
template: ` | |
<p>SAMPLE USE PAGE!</p> | |
<date-picker [(value)]="birtday" [format]="'ITA dd/mm/yy'" [(model)]="bDate"></date-picker> | |
<hr /> | |
<h2>birtday: {{ birtday }}</h2> | |
<h3>bDate: {{ bDate }}</h3> | |
<button (click)="compleanno()">INIT MODEL</button> | |
<button (click)="clearValue()">CLEAR</button> | |
`, | |
styles: [":host { background-color: lightcyan; }"], | |
host: { class: "box" }, | |
changeDetection: ChangeDetectionStrategy.OnPush, | |
}) | |
export class PluginPageComponent { | |
constructor() {} | |
birtday: string | null = "164502000000"; //"1975-03-20Z"; | |
bDate!: Date | null; | |
compleanno() { | |
this.bDate = new Date(1975, 3 - 1, 20); | |
} | |
clearValue() { | |
this.birtday = null; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment