I have an Angular project that uses services that very often repeat themselves with the typical create/read/update/delete/read-list-of-all-entries. I also very often want to manipulate received data in similar ways after receiving it. So I wrote decorators that use apply
which "transform" the data in a way determined by the parameter passed to them (the parameter is always an object class).
In normal use this looks like this:
//rule.service.ts
@Injectable({
providedIn: 'root'
})
export class RuleService {
rulesUrl: string = `${Constants.wikiApiUrl}/rule/`;
constructor(private http: HttpClient) {}
@TransformObservable(RuleObject)
getRule(pk: number): Observable<Rule>{
return this.http.get<Rule>(`${this.rulesUrl}/${pk}`);
}
}
This happens a lot, so I want to write a parent-service that I can extend:
//generic-object.service.ts
@Injectable({
providedIn: 'root'
})
export abstract class GenericObjectService{
baseUrl: string;
constructor(
private http: HttpClient,
public objectClass: any,
) {}
@TransformObservable(this.objectClass) //This line won't work
read(pk: number): Observable<any>{
return this.http.get(`${this.baseUrl}/${pk}`);
}
}
The issue is that I can't use "this" for a decorator parameter, it doesn't exist at the time of parsing IIRC. But is there a way I could pass the decorator-parameter to my injected service somehow so that it may be used by the decorator?
Find below also the "TransformObservable" decorator:
export function TransformObservable(modelClass: any){
/**Decorator to apply transformObservableContent */
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor){
const originalMethod = descriptor.value;
descriptor.value = function(){
const observable = originalMethod.apply(this, arguments);
return transformObservableContent(observable, modelClass);
}
return descriptor;
}
}
The this
keyword will not be available outside of function scopes. However, you may define the needed object as a field/property in same class and in the decorator annotation, you may just pass a string that decorator function will lookup, or pass in an actual object reference.
In the decorator, check the argument that was passed to the annotation:
if(typeof modelClass === 'string'){
// lookup a property in decorated class definition
modelClass = target[modelClass]; // or, this[modelClass]
}else {
// assume modelClass is the needed object
}
This will allow you to use:
@TransformObservable('propertyName');
// or
@TransformObservable(someObjectReference);
Distinction between target
and this
:
The target
passed to decorator function is the object that defines the class where the decorator is used. It will have all the properties and methods defined in the class and can be used to retrieve them as shown above. However target
is not the runtime this
you would generally use inside the class methods. Its only the replacement method (descriptor.value
) that will have this
available inside it, like original method.
Hence, if you really need this
and not just the decorated class definition, you must move all the code inside the replacement function descriptor.value
. There you have access to this
as well as all the parameters that are passed to the decorator and the decorator factory.