Skip to content

Commit

Permalink
feat: allow extendable class chains for the form builder (#326)
Browse files Browse the repository at this point in the history
  • Loading branch information
mvdicarlo authored Jan 8, 2025
1 parent b15dc4b commit e62eca4
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 4 deletions.
66 changes: 66 additions & 0 deletions libs/form-builder/src/lib/form-builder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,72 @@ describe('formBuilder', () => {
});
});

it('should extend classes', () => {
class BooleanType {
@BooleanField({ label: 'description', defaultValue: false })
public field: boolean;
}

class ExtendedType extends BooleanType {
@TextField({ label: 'description', defaultValue: 'Hello' })
public field2: string;
}

class ExtendedAndOverrideType extends ExtendedType {
@TextField({ label: 'title', defaultValue: 'Goodbye' })
public field2: string;
}

expect(formBuilder(new BooleanType(), {})).toEqual({
field: {
label: 'description',
defaultValue: false,
type: 'boolean',
formField: 'checkbox',
row: Number.MAX_SAFE_INTEGER,
col: 0,
},
});

expect(formBuilder(new ExtendedType(), {})).toEqual({
field: {
label: 'description',
defaultValue: false,
type: 'boolean',
formField: 'checkbox',
row: Number.MAX_SAFE_INTEGER,
col: 0,
},
field2: {
label: 'description',
defaultValue: 'Hello',
type: 'text',
formField: 'input',
row: Number.MAX_SAFE_INTEGER,
col: 0,
},
});

expect(formBuilder(new ExtendedAndOverrideType(), {})).toEqual({
field: {
label: 'description',
defaultValue: false,
type: 'boolean',
formField: 'checkbox',
row: Number.MAX_SAFE_INTEGER,
col: 0,
},
field2: {
label: 'title',
defaultValue: 'Goodbye',
type: 'text',
formField: 'input',
row: Number.MAX_SAFE_INTEGER,
col: 0,
},
});
});

it('should build text types', () => {
class TextType {
@TextField({ label: 'description', defaultValue: 'Hello' })
Expand Down
8 changes: 6 additions & 2 deletions libs/form-builder/src/lib/form-builder.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import 'reflect-metadata';
import { METADATA_KEY } from '../constants';
import { FormBuilderMetadata } from './types/form-builder-metadata';
import { PrimitiveRecord } from './types/primitive-record';
import { getMetadataKey } from './utils/assign-metadata';

export function formBuilder(
target: object,
data: PrimitiveRecord,
): FormBuilderMetadata {
const key = getMetadataKey(target.constructor.name);
const sym = target[key];
if (!sym) throw new Error('No metadata symbol found');
const metadata = JSON.parse(
JSON.stringify(Reflect.getMetadata(METADATA_KEY, target.constructor)),
JSON.stringify(Reflect.getMetadata(sym, target.constructor)),
) as FormBuilderMetadata;

for (const value of Object.values(metadata)) {
Expand All @@ -20,3 +23,4 @@ export function formBuilder(

export * from './decorators';
export * from './types';

36 changes: 34 additions & 2 deletions libs/form-builder/src/lib/utils/assign-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { FieldAggregateType, FieldType } from '../types';
import { FormBuilderMetadata } from '../types/form-builder-metadata';
import { PrimitiveRecord } from '../types/primitive-record';

export function getMetadataKey(name: string) {
return `__${METADATA_KEY}__${name}__`;
}

/**
* Make keys V in type T partial
*/
Expand Down Expand Up @@ -92,18 +96,46 @@ export function createFieldDecorator<
if (typeof propertyKey === 'symbol') return;

const proto = target.constructor;
const chain = [];
let currentProto = proto;
while (currentProto && currentProto.name) {
chain.push(currentProto.name);
currentProto = Object.getPrototypeOf(currentProto);
}
const key = getMetadataKey(proto.name);
if (!target[key]) {
target[key] = Symbol(key);
}
const sym = target[key];
const fields: FormBuilderMetadata =
Reflect.getMetadata(METADATA_KEY, proto) || {};
Reflect.getMetadata(sym, proto) || {};

field.onCreate?.(
fieldOptions as unknown as FieldType<FieldValue, TypeKey>,
target,
propertyKey,
);

const chainedFields = chain
.reverse()
.filter((c) => c !== target.constructor.name)
.map((c) => Reflect.getMetadata(target[getMetadataKey(c)], proto));

for (const c of chainedFields) {
if (c) {
Object.entries(c).forEach(([fieldKey, value]) => {
if (value !== undefined) {
fields[fieldKey] = JSON.parse(
JSON.stringify(value),
) as unknown as FieldAggregateType;
}
});
}
}

fields[propertyKey] = fieldOptions as unknown as FieldAggregateType;

Reflect.defineMetadata(METADATA_KEY, fields, proto);
Reflect.defineMetadata(sym, fields, proto);
};
}

Expand Down

0 comments on commit e62eca4

Please sign in to comment.