I'm using a property decorator Field
which pushes its' key to a fields
Reflect metadata property:
export function Field(): PropertyDecorator {
return (target, key) => {
const fields = Reflect.getMetadata('fields', target) || [];
if (!fields.includes(key)) {
fields.push(key)
}
Reflect.defineMetadata('fields', fields, target)
}
}
I then have an abstract base-class Form
that accesses the metadata in a getter accessory:
abstract class Form {
get fields() {
return Reflect.getMetadata('fields', this) || [];
}
}
I have so far been able to use it successfully to distinguish form fields from other class properties. Consider these classes:
abstract class UserForm extends Form {
@Field()
public firstName: string
@Field()
public lastName: string
get fullName() {
return this.firstName + ' ' + this.lastName;
}
}
class AdminForm extends UserForm {
@Field()
roles: string[]
}
const form = new AdminForm()
console.log(form.fields)
// ['roles', 'firstName', 'lastName']
The problem occurs when I define a sister class to AdminForm
- MemberForm
. When multiple subclasses exists to Form
it seems the fields
getter returns all fields:
class MemberForm extends UserForm {
@Field()
memberSince: Date;
}
const form = new AdminForm()
console.log(form.fields)
// ['roles', 'firstName', 'lastName', 'memberSince'] <--!!!
This makes no sense to me. Why does the memberSince
field appear on an instance of AdminForm
? How can I define different fields on different subclasses?
The problem is getMetadata
goes down the prototype chain and will always return what is defined on the base type (since that gets assigned first). You need to use getOwnMetadata
to get the array field of the current class only when you add a new field and when getting the fields you need to walk up the property chain to get all base class fields.
This should work:
import 'reflect-metadata'
export function Field(): PropertyDecorator {
return (target, key) => {
const fields = Reflect.getOwnMetadata('fields', target) || [];
if (!fields.includes(key)) {
fields.push(key)
}
Reflect.defineMetadata('fields', fields, target)
}
}
abstract class Form {
get fields() {
let fields = []
let target = Object.getPrototypeOf(this);
while(target != Object.prototype) {
let childFields = Reflect.getOwnMetadata('fields', target) || [];
fields.push(...childFields);
target = Object.getPrototypeOf(target);
}
return fields;
}
}
abstract class UserForm extends Form {
@Field()
public firstName!: string
@Field()
public lastName!: string
get fullName() {
return this.firstName + ' ' + this.lastName;
}
}
class AdminForm extends UserForm {
@Field()
roles!: string[]
}
const form1 = new AdminForm()
console.log(form1.fields) // ['roles', 'firstName', 'lastName']
class MemberForm extends UserForm {
@Field()
memberSince!: Date;
}
const form2 = new MemberForm()
console.log(form2.fields) // ["memberSince", "firstName", "lastName"]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With