Skip to content

Instantly share code, notes, and snippets.

@dmorosinotto
Last active November 25, 2022 17:41
Show Gist options
  • Save dmorosinotto/43703bdd5fadf01b55529b172fc62e13 to your computer and use it in GitHub Desktop.
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 ^_^
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();
}
}
}
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);
}
}
}
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