Typescript & Angular 2 Reflection - javascript

Though this topic has already been discussed in other posts like this:
Dynamically loading a typescript class (reflection for typescript)
I'm not able to find an answer to my specific issue. So, pardon me if this is duplicated.
I'm trying to create a very simple directive in Angular 2 (using Typescript), which allows dynamic addition or removal of a set of controls represented by a Type. For example, if the type is:
class Stone{
constructor(
public nameOfStone?: string,
public typeOfStone?: string
){}
}
the UI would have something like this:
I'm able to get this working with a specific Type (ex: Stone). But, given that the directive's objective is just to add this dynamic add/remove feature, I felt that it would make sense to parameterise the type to be created and use this for different type definitions. I tried something like this in the Component class:
import {Component} from 'angular2/core';
import {NgForm} from 'angular2/common';
import {ControlGroup, Control, FormBuilder, FORM_DIRECTIVES} from 'angular2/common'
#Component({
selector: 'stone-details',
templateUrl: '../stones/stone-details.component.html',
directives: [FORM_DIRECTIVES]
})
export class StoneComponent {
type = 'Stone';
Stones = new Array<Stone>();
addBtnClicked(){
let Stone = Object.create(window['Stone'].prototype);
//let Stone = new Stone('', '');
this.Stones.push(Stone);
}
removeBtnClicked(index: number){
if(index >= this.Stones.length){
alert('Not a valid index');
}else if(confirm('Remove this Stone?')){
this.Stones.splice(index, 1);
}
}
}
class Stone{
constructor(
public nameOfDeity?: string,
public typeOfDeity?: string
){}
}
When I use the commented line
let Stone = new Stone('', '');
the component works perfectly, but if I use
let Stone = Object.create(window['Stone'].prototype);
it doesn't seem to work and the error I see is
angular2.dev.js:23941 ORIGINAL EXCEPTION: TypeError: Cannot read property 'prototype' of undefined.
I initially thought exporting the Stone class would help, but none of the crazy variations (exporting the class, trying to refer to the class as window['StoneComponent'].export_1['Stone']) helped. I understand the component isn't directly visible under the window component, but I'm not sure what I'm missing. Is there an alternate way to doing this? Am I missing something? Please advise.
P.S: I'm using the latest version of Angular 2 and Typescript (I started this application a couple of days back).

The problem with your code is definition order.
Specifically, class definitions are not hoisted like function definitions are. The tricky part is that the type Stone is hoisted, which is perfectly valid, but the value Stone, the constructor function, is not.
To get around this just move the definition of Stone above the component or extract it into a separate module and import it.
Do not try to shove it into a global variable, say window. That is a very poor practice and will lead to bugs and name collisions faster than one might think. It also defeats the benefits of modules.
In short, what you need is
class Stone {
constructor(
readonly nameOfDeity?: string,
readonly typeOfDeity?: string
) {}
}
export class StoneComponent {
kind = 'Stone';
stones: Stone[] = [];
addBtnClicked() {
const stone = new Stone();
this.stones.push(stone);
}
removeBtnClicked(index: number) {
if (index >= this.stones.length) {
alert('Not a valid index');
} else if (confirm('Remove this Stone?')){
this.stones.splice(index, 1);
}
}
}
UPDATE
Since in the original question you state that this will be a generic component and you will have multiple classes where the actual class is selected by a kind property of the component class. You may want to consider the following pattern rather.
component-field-kinds.ts
export class Stone { ... }
export class Widget { ... }
export class Gizmo { ... }
generic-component.ts
import * as kinds from './component-field-kinds';
type Kind = keyof typeof kinds;
export class GenericComponent {
#Input() kind: Kind;
values: typeof kinds[Kind][] = [];
addBtnClicked() {
const value = new kinds[this.kind]();
this.values.push(value);
}
Note, for what it is worth, JavaScript, and therefore TypeScript has no such thing as a dynamic class loader. This is just how the language works all the time and the whole structure is first class.
This is not Java.

Since class is simple function you could create instance using new func(), but you still have to pass func from outer code.
I guess the most efficient solution is the following:
export class StoneComponent<T> {
addBtnClicked(){
let item:T = <T>{}
this.items.push(item);
}
}
types will match as long as objects have the same set of properties.

Related

How can I make TypeScript aware that `classInstance.constructor` in JS will give access to the class' static methods and members?

The following code works in plain JavaScript:
class Post {
static table = "Posts"
title = ""
}
const post = new Post()
console.log(post.constructor.table) // will log "Posts"
But for some reason, TypeScript does not seem to be aware that the constructor member of a class instance gives access to that class' static methods and members. TypeScript will insist that post.constructor is nothing more than the same function as Object.constructor.
This is problematic in a project that I'm working on. So I wonder, is there a way to make TypeScript aware that post.constructor.table does actually exist?
I have found a way that works, but it is super ugly and I really hope there is another way.
import type { Class, Constructor, EmptyObject } from "type-fest"
// Here I'm overriding the constructor function on a class instance type,
// because in plain javascript it's also possible to access
// the constructor function from a class instance and then
// get access to all the class' static methods and members.
type OverrideConstructorToIncludeStatic<
T,
Static = EmptyObject,
Arguments extends unknown[] = any[],
> = T & { constructor: Static & Constructor<T, Arguments> }
// This augmented Class type allows one to define
// expected static methods and members on classes.
type ClassWithStatic<
T,
Static = EmptyObject,
ConstructorArguments extends unknown[] = any[],
> = Class<
OverrideConstructorToIncludeStatic<T, Static, ConstructorArguments>,
ConstructorArguments
> & Static
type HasTable = { table: string }
type ClassHasTable<T> = ClassWithStatic<T, HasTable>
function extend<T>(classType: Class<T> & HasTable): ClassHasTable<T> {
return classType as ClassHasTable<T>
}
const Post = extend(class Post {
static table = "Posts"
title = ""
})
const post = new Post()
post.constructor.table // now TypeScript won't complain
So that works, but const Post = extend(class Post { ... }) is so ugly. And moreover, I'm not the one who would need to write that. The consumers of my library would need to write that and I don't want to do that to anyone.
Is there any other way of doing this?
Edit
Of course I'm using over-simplified examples here. In case you'd like to know the real use-case I'm asking this for, you can check out this playground: https://tsplay.dev/wXqeDN.
In short, I'm trying to see if I can figure out a way to (un)serialize class objects that adhere to a certain interface requiring static methods for the (un)serializing process. The end goal would be that I could serialize class objects in a backend and unserialize them in frontend code.
But I just realized that that might not be possible after all, because I would probably also need to serialize the classRegister Map you see in that playground. And I'm afraid that may prove to be impossible. Unless I'd use something like eval, but we all know how dangerous that is.
Corrected thanks to caTS's observation
Since constructor properties are not available you have to do something more or less ugly.
Your code can be made more general by getting the static properties of any class.
A variant using type casting might be:
type ClassLike = { new(...args: readonly unknown[]): unknown }
type InstanceTypeWithConstructor<C extends ClassLike> =
{
constructor: C
}&
InstanceType<C>;
class Post {
static table = "Posts"
title = ""
}
const post = new Post() as InstanceTypeWithConstructor<typeof Post>;
const consTable = post.constructor.table;
playground link
The first version of my answer was erroneously removing prototype from the properties of constructor using key remapping
-- playground link

Scope of an imported variable in ionic v3 [duplicate]

I have a constants file constants.ts:
export const C0NST = "constant";
I access it in a service some.service.ts like so:
import { C0NST } from './constants';
console.log(C0NST); // "constant"
However, when I access it in a component template:
some.component.ts:
import { C0NST } from './constants';
some.component.html:
{{ C0NST }} <!-- Outputs nothing -->
However defining a member in the component class works:
some.component.ts
public const constant = C0NST;
some.component.html
{{ constant }} <!-- constant -->
I don't understand why I was able to access the imported constant directly in the service class but not in the component template even though I imported it in the component class.
In Angular2, the template can only access fields and methods of the component class. Everything else is off-limits. This includes things which are visible to the component class.
The way to go around this is to have a field inside the component, which just references the constant, and use that instead.
It's one limitation of the design, but perhaps you should think a bit more about why you need a constant in the template in the first place. Usually these things are used by components themselves, or services, but not the template.
Since in the Component's template you can only use attributes of the Component's class, you can't directly use any external constants (or external variables).
The most elegant way that I've found so far is the following:
import { MY_CONSTANT } from '../constants';
#Component({
// ...
})
export class MyTestComponent implements OnInit {
readonly MY_CONSTANT = MY_CONSTANT;
// ...
}
which basically just creates a new attribute MY_CONSTANT inside the component class. Using readonly we make sure that the new attribute cannot be modified.
Doing so, in your template you can now use:
{{ MY_CONSTANT }}
The scope of Angular2 template bindings is the component instance. Only what's accessible there can be used in bindings.
You can make it available like
class MyComponent {
myConst = CONST;
}
{{myConst}}
There are two best directions in my opinion:
Wrapping constants as internal component property
enum.ts
export enum stateEnum {
'DOING' = 0,
'DONE',
'FAILED'
}
component.ts
import { stateEnum } from './enum'
export class EnumUserClass {
readonly stateEnum : typeof stateEnum = stateEnum ;
}
Example uses enum, but this can be any type of defined constant. typeof operator gives you all of benefits of TypeScript typing features. You can use then this variable directly in templates:
component.html
<p>{{stateEnum.DOING}}<p>
This solution is less efficient in memory usage context, because you are basically duplicating data (or references to constants) in each component you wish to use it. Beside that, syntax
readonly constData: typeof constData = constData
in my opinion introduce a lot of syntax noise and may be confusing to newcommers
Wrapping external constant in component function
Second option is to wrap your external variable/constant with component function and use that function on template:
enum.ts
export enum stateEnum {
'DOING' = 0,
'DONE',
'FAILED'
}
component.ts
import { stateEnum } from './enum'
export class EnumUserClass {
getEnumString(idx) {
return stateEnum[stateEnum[idx]];
}
}
component.html
<p>{{getEnumString(1)}}</p>
Good thing is that data is not duplicated in controller but other major downside occur. According to Angular team, usage of functions in templates is not recommended due to change detection mechanism, which works way less efficient in case of functions returning values to templates: change detection have no idea does value return by a function has changed, so it will be called way often than needed (and assuming you returning const from it, it's actually needed only once, when populating template view. It may be just a bit efficiency killing to your application (if you are lucky) or it may totally break it down if function resolves with Observable for instance, and you use async pipe to subscribe to results. You can refer to my short article on that HERE
You can create a BaseComponent , it is a place where you should create your constant instances and then you can create your FooComponent extends BaseComponent and you can use your constants.

Angular 2: How to detect changes in an array? (#input property)

I have a parent component that retrieves an array of objects using an ajax request.
This component has two children components: One of them shows the objects in a tree structure and the other one renders its content in a table format. The parent passes the array to their children through an #input property and they display the content properly. Everything as expected.
The problem occurs when you change some field within the objects: the child components are not notified of those changes. Changes are only triggered if you manually reassign the array to its variable.
I'm used to working with Knockout JS and I need to get an effect similar to that of observableArrays.
I've read something about DoCheck but I'm not sure how it works.
OnChanges Lifecycle Hook will trigger only when input property's instance changes.
If you want to check whether an element inside the input array has been added, moved or removed, you can use IterableDiffers inside the DoCheck Lifecycle Hook as follows:
constructor(private iterableDiffers: IterableDiffers) {
this.iterableDiffer = iterableDiffers.find([]).create(null);
}
ngDoCheck() {
let changes = this.iterableDiffer.diff(this.inputArray);
if (changes) {
console.log('Changes detected!');
}
}
If you need to detect changes in objects inside an array, you will need to iterate through all elements, and apply KeyValueDiffers for each element. (You can do this in parallel with previous check).
Visit this post for more information: Detect changes in objects inside array in Angular2
You can always create a new reference to the array by merging it with an empty array:
this.yourArray = [{...}, {...}, {...}];
this.yourArray[0].yourModifiedField = "whatever";
this.yourArray = [].concat(this.yourArray);
The code above will change the array reference and it will trigger the OnChanges mechanism in children components.
Read following article, don't miss mutable vs immutable objects.
Key issue is that you mutate array elements, while array reference stays the same. And Angular2 change detection checks only array reference to detect changes. After you understand concept of immutable objects you would understand why you have an issue and how to solve it.
I use redux store in one of my projects to avoid this kind of issues.
https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html
You can use IterableDiffers
It's used by *ngFor
constructor(private _differs: IterableDiffers) {}
ngOnChanges(changes: SimpleChanges): void {
if (!this._differ && value) {
this._differ = this._differs.find(value).create(this.ngForTrackBy);
}
}
ngDoCheck(): void {
if (this._differ) {
const changes = this._differ.diff(this.ngForOf);
if (changes) this._applyChanges(changes);
}
}
It's work for me:
#Component({
selector: 'my-component',
templateUrl: './my-component.component.html',
styleUrls: ['./my-component.component.scss']
})
export class MyComponent implements DoCheck {
#Input() changeArray: MyClassArray[]= [];
private differ: IterableDiffers;
constructor(private differs: IterableDiffers) {
this.differ = differs;
}
ngDoCheck() {
const changes = this.differ.find(this.insertedTasks);
if (changes) {
this.myMethodAfterChange();
}
}
This already appears answered. However for future problem seekers, I wanted to add something missed when I was researching and debugging a change detection problem I had. Now, my issue was a little isolated, and admittedly a stupid mistake on my end, but nonetheless relevant.
When you are updating the values in the Array or Object in reference, ensure that you are in the correct scope. I set myself into a trap by using setInterval(myService.function, 1000), where myService.function() would update the values of a public array, I used outside the service. This never actually updated the array, as the binding was off, and the correct usage should have been setInterval(myService.function.bind(this), 1000). I wasted my time trying change detection hacks, when it was a silly/simple blunder. Eliminate scope as a culprit before trying change detection solutions; it might save you some time.
Instead of triggering change detection via concat method, it might be more elegant to use ES6 destructuring operator:
this.yourArray[0].yourModifiedField = "whatever";
this.yourArray = [...this.yourArray];
You can use an impure pipe if you are directly using the array in your components template. (This example is for simple arrays that don't need deep checking)
#Pipe({
name: 'arrayChangeDetector',
pure: false
})
export class ArrayChangeDetectorPipe implements PipeTransform {
private differ: IterableDiffer<any>;
constructor(iDiff: IterableDiffers) {
this.differ = iDiff.find([]).create();
}
transform(value: any[]): any[] {
if (this.differ.diff(value)) {
return [...value];
}
return value;
}
}
<cmp [items]="arrayInput | arrayChangeDetector"></cmp>
For those time travelers among us still hitting array problems here is a reproduction of the issue along with several possible solutions.
https://stackblitz.com/edit/array-value-changes-not-detected-ang-8
Solutions include:
NgDoCheck
Using a Pipe
Using Immutable JS NPM github

Creating TypeScript objects dynamically

Following a post regarding creating objects dynamically in TypeScript, I have the following code used as a factory to create an object from its name:
public createComponent(context: Object, componentName: string): ComponentRef<ComponentBase> {
this.viewContainer.clear();
var instance =
Object.create(context[componentName].prototype); // <-- fails
let componentFactory =
this.componentFactoryResolver.resolveComponentFactory(instance);
return <ComponentRef<ComponentBase>> this.viewContainer.createComponent(componentFactory);
}
I'm not entirely convinced I understand this window[suchAndSuch] syntax: what does it mean? Can't find any documentation for it.
In any event it seems that window[myClassName] is undefined, even though the class in question is defined in the same file. I have looked at the Javascript transpiled from the TypeScript, and the class in question is in scope.
I think.
Help!
-- UPDATE --
I have this code, which is part of an Angular 2 application, that is calling the above method to try to get an instance of a component, injectables and all:
export class CustomUserComponent implements OnChanges {
#Input() componentType: string;
#ViewChild(ComponentDirective) componentAnchor: ComponentDirective;
ref: ComponentRef<GalleriaComponentBase>;
ngAfterViewInit() {
this.ref = this.componentAnchor
.createComponent(window, this.componentType);
}
}

How to namespace es6 classes (for React components)

This is part ES6 question part React question. I'm trying to use namespaced components in React with ES6 classes and Babel. So I guess the real question is how to name space es6 classes so I can do what is explained here: https://facebook.github.io/react/docs/jsx-in-depth.html#namespaced-components
Since I get an unexpected token error:
class Headline extends Component { ... }
class Headline.Primary extends Component { ...
^
The ECMAScript-6 class declaration syntax expects a standard BindingIdentifer as the class name. A dot is not a valid character inside an identifier name.
In the context used in the link in OP, the "namespace" is an object, and properties are added to that object one by one using the dot notation for property access.
You could replicate that by using a class expression instead:
'use strict'
var ns = {}
ns.MyClass = class {
constructor() {
console.log('in constructor')
}
}
new ns.MyClass()
This doesn't really change with ES6, you still will have to do an assignment:
Headline.Primary = class Primary extends Component { … };
However, using classes like Headline as namespaces is getting pretty deprecated with ES6 (and has previously been a questionable practice anyway), you should instead leverage the new module system. Export Primary as a named export, and instead of importing the Headline class rather do import * as headlines from ….
This link also relates to this question.
In the Module objects section, it is described that you can do something like this:
// headline.js file
export {Headline, Primary}
class Headline {}
class Primary {}
// In another module...
import * as Headline from "headline";
let h = new Headline.Headline();
let hp = new Headline.Primary();
It's not exactly what you are trying to do, but is an alternative.
Another way of doing it is almost like #Bergi has already pointed out, but I'm just clarifying it further:
let Headline = class Headline extends Component { }
Headline.Primary = class Primary extends Component { }
export {Headline as default}
// in another module:
import Headline from 'headline';
let headline = new Headline();
let primary = new Headline.Primary();

Categories

Resources