Skip to content

Instantly share code, notes, and snippets.

@marcj
Created February 16, 2023 00:28
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 marcj/2715606d0586e5f547fd067b81514807 to your computer and use it in GitHub Desktop.
Save marcj/2715606d0586e5f547fd067b81514807 to your computer and use it in GitHub Desktop.
import { AbstractControl, FormArray, FormControl, FormControlOptions, FormControlState, FormGroup, ValidationErrors, ValidatorFn } from "@angular/forms";
import { ClassType, isFunction } from "@deepkit/core";
import {
deserialize,
getValidatorFunction,
hasDefaultValue,
isCustomTypeClass,
isOptional,
isType,
ReflectionClass,
ReflectionKind, ReflectionProperty,
serialize,
Type,
validationAnnotation,
ValidationErrorItem
} from "@deepkit/type";
type PropPath = string | (() => string);
function getLastProp(propPath?: PropPath): string {
propPath = isFunction(propPath) ? propPath() : propPath;
if (!propPath) return '';
return propPath.split('.').pop() || '';
}
function getPropPath(propPath?: PropPath, append?: string | number): string {
propPath = isFunction(propPath) ? propPath() : propPath;
if (propPath && append !== undefined) {
return propPath + '.' + append;
}
if (propPath) {
return propPath;
}
if (append !== undefined) {
return String(append);
}
return '';
}
function isRequired(type: Type) {
const val = validationAnnotation.getFirst(type);
if (val && val.name === 'minLength') {
return true;
}
if (type.parent && (type.parent.kind === ReflectionKind.property || type.parent.kind === ReflectionKind.propertySignature)) {
return !isOptional(type.parent) && !hasDefaultValue(type.parent);
}
return !isOptional(type);
}
function errorsToAngularErrors(errors: ValidationErrorItem[]): any {
if (errors.length) {
const res: ValidationErrors = {};
for (const e of errors) {
res[e.code] = e.message;
}
return res;
}
return null;
}
export class TypedFormControl2<T = any> extends FormControl {
constructor(propPath: PropPath, public type: Type, value: FormControlState<T> | T, validatorOrOpts?: ValidatorFn | ValidatorFn[] | FormControlOptions | null) {
super(value, validatorOrOpts);
Object.defineProperty(this, 'value', {
get: () => {
if (this.parent) {
return this.parent.value ? this.parent.value[getLastProp(propPath)] : undefined;
}
return value;
},
set: (v: any) => {
if (this.parent) {
if (this.parent.value) {
this.parent.value[getLastProp(propPath)] = v;
}
}
value = v;
}
});
}
isRequired() {
return isRequired(this.type);
}
}
function createControl<T>(
propPath: PropPath,
propName: string,
prop: Type,
parent?: FormGroup | FormArray,
): AbstractControl {
const type = prop.kind === ReflectionKind.property || prop.kind === ReflectionKind.propertySignature ? prop.type : prop;
const validator = (control: AbstractControl): ValidationErrors | null => {
const rootFormGroup = control.root as TypedFormGroup2<any>;
if (!rootFormGroup.value) {
// not yet initialized
return null;
}
let parent = control.parent;
while (parent) {
if (parent instanceof TypedFormGroup2) {
//null/undefined values are handled by the parent
if (!parent.value) {
return null;
}
}
parent = parent.parent;
}
const errors: ValidationErrorItem[] = [];
if (prop && (prop.kind === ReflectionKind.property || prop.kind === ReflectionKind.propertySignature)) {
if (!control.value) {
if (!isRequired(prop)) {
return null;
}
}
if (type.kind === ReflectionKind.class && isCustomTypeClass(type)) {
return null; //handled in sub controls
}
}
const fn = getValidatorFunction(undefined, prop);
(fn as any)(control.value, { errors }, getPropPath(propPath));
return errorsToAngularErrors(errors);
};
let control: AbstractControl;
if (type.kind === ReflectionKind.array) {
throw new Error('Array not supported yet');
} else {
if (type.kind === ReflectionKind.class && isCustomTypeClass(type)) {
control = TypedFormGroup2.fromEntityClass(type, undefined, propPath);
} else {
control = new TypedFormControl2(propPath, prop, undefined, validator);
}
}
if (parent) {
control.setParent(parent);
}
return control;
}
export class TypedFormGroup2<T extends object, TRawValue extends T = T> extends FormGroup {
public value!: T;
init(value?: T): this {
this.reset(value);
if (value) this.setValue(value);
return this;
}
_updateValue(): void {
//angular forms works normally in the way that controls updates the value,
//but we change that. The real values change controls.
}
setValue(value: T, options: { onlySelf?: boolean; emitEvent?: boolean } = {}) {
if (value) {
const o: any = {};
const set = (target: any, prop: ReflectionProperty, newValue: any, d?: PropertyDescriptor) => {
if (d && d.set) {
d.set(newValue);
} else {
o[prop.name] = newValue;
}
if (prop.type.kind === ReflectionKind.union) {
//figure out the type and see if it changed.
//if so, change the control if we need to (from object to array, or array to primitive, etc)
}
if (prop.isOptional() && newValue === undefined) {
//remove control
this.controls[prop.name].disable();
} else {
this.controls[prop.name].enable();
}
}
Object.assign(o, value);
for (const prop of this.reflection.getProperties()) {
const d = Object.getOwnPropertyDescriptor(value, prop.name);
Object.defineProperty(value, prop.name, {
// configurable: false,
set: (newValue: any) => set(value, prop, newValue, d),
get: () => d?.get ? d.get() : (o as any)[prop.name],
});
}
(this as any).value = value;
Object.keys(value).forEach(name => {
if (!this.controls[name]) return;
const property = this.reflection.getProperty(name);
if (property.isOptional() && (value as any)[name] === undefined) return;
this.controls[name].setValue((value as any)[name], { onlySelf: true, emitEvent: options.emitEvent });
});
} else {
(this as any).value = undefined;
}
this.updateValueAndValidity(options);
}
protected reflection: ReflectionClass<any>;
constructor(public type: Type, public path?: PropPath) {
super({}, null);
this.reflection = ReflectionClass.from(type);
for (const prop of this.reflection.getProperties()) {
this.registerControl(prop.name, createControl(() => getPropPath(path, prop.name), prop.name, prop.property, this));
}
}
storeLocalStorage(key: string): this {
this.valueChanges.subscribe(() => {
const v = this.value;
if (!v) return;
localStorage.setItem(key, JSON.stringify(serialize(v, undefined, undefined, undefined, this.type)));
});
const ch = localStorage.getItem(key);
if (ch) {
this.markAsDirty();
const loaded: any = deserialize(JSON.parse(ch), undefined, undefined, undefined, this.type);
this.setValue(loaded);
}
return this;
}
static fromEntityClass<T extends object>(
classType: ClassType<T> | Type,
value?: T,
path?: PropPath,
): TypedFormGroup2<T> {
const type = isType(classType) ? classType : ReflectionClass.from(classType).type;
return new TypedFormGroup2<T>(type, path).init(value);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment