Skip to content

Instantly share code, notes, and snippets.

@snigo
Last active February 12, 2021 13:14
Show Gist options
  • Save snigo/0291a69b66e7d70742fc475f9b30cc83 to your computer and use it in GitHub Desktop.
Save snigo/0291a69b66e7d70742fc475f9b30cc83 to your computer and use it in GitHub Desktop.
TS decorators. Live example: https://repl.it/@snigo/Decorators#index.ts
/**
* In order to use property decorator we need to import
* built-in Reflect.metadata decorator
*/
import 'reflect-metadata';
/**
* 1. CLASS DECORATOR
*/
/**
* Component configurator (metadata)
*/
interface ComponentConfig {
selector: string;
templateUrl: string;
stylesUrl: string;
}
/**
* Decorator factory - a function that returns a decorator
* Component() // => @ComponentDecorator
*/
function Component(metadata: ComponentConfig) {
/**
* Factory will immediatelly return decorator function
* that will expect target class as an argument
*
* To provide type definition for class (aka constructor function)
* we use `<T extends { new (...args: any[]): {} }>` signature
* which means we provide variable type T that extends constructor function
* (a function that can be invoked with `new` keyword)
*/
return function<T extends { new (...args: any[]): {} }>(TargetClass: T) {
/**
* We will return a new class that will extend provided target class
* where we'll declare additional properties from metadata
*/
class __Component extends TargetClass {
selector: string = metadata.selector;
templateUrl: string = metadata.templateUrl;
stylesUrl: string = metadata.stylesUrl;
}
return __Component;
}
}
/**
* Now let's use the decorator factory and declare component class
*/
@Component({
selector: 'topbar',
templateUrl: './topbar.component.html',
stylesUrl: './topbar.component.scss',
})
class Topbar {
title = 'Welcome to my homepage!'
}
const topbarComponent = new Topbar();
/**
* Whenever your decorator modifies constructor of the class
* and provides new properties you must maintain the original prototype.
*
* We didn't do that to simplify this example and thus we have to cast
* our component to `any`, simply because TS doesn't know that new
* decorated class indeed has `selector` property.
* It happens because decorators are being applied at the runtime
* and not the declaration time
*/
console.log((topbarComponent as any).selector);
console.log((topbarComponent as any).templateUrl);
/**
* 2. PROPERTY DECORATOR
*/
/**
* Next step we would need unique key to access metadata
* of the property values of the class. We will create it
* using JS Symbol: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol
* This will guarantee uniqueness of the key
*/
const inputKey = Symbol('importKey');
/**
* Just like with class decorator example we'll use decorator factory,
* a function that will return a property decorator
*/
function Input(selector?: string) {
/**
* Input factory takes `selector` argument as a string
* but instead of returning our custom decorator function
* we will return `@Reflect.metadata` decorator factory
* that expects two arguments:
* - unique key to access metadata
* - metadata metadata itself, which in our case would be `selector`
*/
return Reflect.metadata(inputKey, selector);
}
/**
* At this stage our decorator doesn't do much,
* it just writes metadata to global Reflect object
*
* We need couple helper functions to actually use the metadata
*/
function __setInputValue(target: any, selector: string, value: any): void {
/**
* Find property key by selector or property key:
* loop through all keys of the class (`target`) and find one
* that either equals to provided selector
* or whose metadata equals to selector
*/
const propertyKey = Object.keys(target).find((key) => selector === key || Reflect.getMetadata(inputKey, target, key) === selector);
/**
* Set new value for the property if propertyKey was found
*
* We would need to cast `target` class to any,
* because we don't know the type of input property beforehand
*/
if (propertyKey !== undefined) {
(target as any)[propertyKey] = value;
}
}
function __getInputValue(target: any, selector: string): any {
/**
* Find property key by selector
*/
const propertyKey = Object.keys(target).find((key) => selector === key || Reflect.getMetadata(inputKey, target, key) === selector);
if (propertyKey === undefined) return undefined;
return (target as any)[propertyKey];
}
/**
* Now let's create new component with `Input` decorator
*/
@Component({
selector: 'movie-card',
templateUrl: './movie-card.component.html',
stylesUrl: './movie-card.component.scss',
})
class MovieCard {
@Input()
movie = '';
@Input('image-url')
url = '';
}
const movieCardComponent = new MovieCard();
__setInputValue(movieCardComponent, 'movie', 'Avatar');
__setInputValue(movieCardComponent, 'image-url', 'https://avatar.com/image.png');
console.log(movieCardComponent.movie);
console.log(movieCardComponent.url);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment