Last active
February 12, 2021 13:14
-
-
Save snigo/0291a69b66e7d70742fc475f9b30cc83 to your computer and use it in GitHub Desktop.
TS decorators. Live example: https://repl.it/@snigo/Decorators#index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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