Questions:
Is the behavior I'm observing the expected behavior for TypeScript?
Is the behavior I'm observing the expected behavior for ECMAScript 6?
Is there an easy way to walk back through the inheritance hierarchy to process the 'myStatic' array for each level? How do I know when to stop?
Description: When using TypeScript, there appears to be some interesting behavior with derived classes and static properties.
TypeScript Example
class MyBase {
static myStatic = [];
}
class MyDerived extends MyBase {}
MyBase.myStatic = ['one', 'two', 'three']
class MyDerived2 extends MyBase {}
document.body.innerHTML += "<b>MyDerived.myStatic:</b> " + JSON.stringify(MyDerived.myStatic) + "<br/>";
document.body.innerHTML += "<b>MyDerived2.myStatic:</b> " + JSON.stringify(MyDerived2.myStatic) + "<br/>";
This is the result:
MyDerived.myStatic: []
MyDerived2.myStatic: ["one","two","three"]
Edit: Adding example that illustrates different behavior between TypeScript and ECMA Script 6. Note: ECMA Script doesn't support static properties, so these examples use static methods.
TypeScript Code:
class MyBase {
static myStatic() { return []; }
}
class MyDerived extends MyBase {}
MyBase.myStatic = () => { return ['one', 'two', 'three']; }
class MyDerived2 extends MyBase {}
document.body.innerHTML += "<b>MyDerived.myStatic:</b> " + JSON.stringify(MyDerived.myStatic()) + "<br/>";
document.body.innerHTML += "<b>MyDerived2.myStatic:</b> " + JSON.stringify(MyDerived2.myStatic()) + "<br/>";
TypeScript Results:
MyDerived.myStatic: []
MyDerived2.myStatic: ["one","two","three"]
ECMA Script 6 Code: ES6 Fiddle
class MyBase {
static myStatic() { return []; }
}
class MyDerived extends MyBase {}
MyBase.myStatic = () => { return ['one', 'two', 'three']; };
class MyDerived2 extends MyBase {}
console.log("MyDerived.myStatic: " + JSON.stringify(MyDerived.myStatic()));
console.log("MyDerived2.myStatic: " + JSON.stringify(MyDerived2.myStatic()));
ECMA Script 6 Results
MyDerived.myStatic: ["one","two","three"]
MyDerived2.myStatic: ["one","two","three"]
Is the behavior I'm observing the expected behavior for TypeScript?
Is the behavior I'm observing the expected behavior for ECMA Script 6?
Yes and yes. Classes allow runtime modification and processed in order of definition. inherit only kicks in at the point of definition so depends upon the properties of the base at that point.
See related TypeScript bug report
Here are TypeScript and ES6 code samples that have consistent behavior and walk the inheritance hierarchy:
TypeScript Code:
class MyBase {
static myStatic() { return []; }
}
class MyDerived extends MyBase {
static myStatic() { return ['mickey', 'goofy', super.myStatic()]; }
}
MyBase.myStatic = () => { return ['one', 'two', 'three']; };
class MyDerived2 extends MyBase {
static myStatic() { return ['mickey', 'goofy', super.myStatic()]; }
}
document.body.innerHTML += ("MyDerived.myStatic: " + JSON.stringify(MyDerived.myStatic()) + "<br/>");
document.body.innerHTML += ("MyDerived2.myStatic: " + JSON.stringify(MyDerived2.myStatic()) + "<br/>");
TypeScript Result:
MyDerived.myStatic: ["mickey","goofy",["one","two","three"]]
MyDerived2.myStatic: ["mickey","goofy",["one","two","three"]]
ES6 Code: ES6 Fiddle
class MyBase {
static myStatic() { return []; }
}
class MyDerived extends MyBase {
static myStatic() { return ['mickey', 'goofy', super.myStatic()]; }
}
MyBase.myStatic = () => { return ['one', 'two', 'three']; };
class MyDerived2 extends MyBase {
static myStatic() { return ['mickey', 'goofy', super.myStatic()]; }
}
console.log("MyDerived.myStatic: " + JSON.stringify(MyDerived.myStatic()));
console.log("MyDerived2.myStatic: " + JSON.stringify(MyDerived2.myStatic()));
ES6 Result
MyDerived.myStatic: ["mickey","goofy",["one","two","three"]]
MyDerived2.myStatic: ["mickey","goofy",["one","two","three"]]
Related
I am trying to access value of a class variable in function setRating() but the console print is "undefined".
export class UserFeedbackComponent implements OnInit {
rating: number;
constructor() {
this.rating = 3;
}
ngOnInit() {
//initial setup
console.log("Rating " + this.rating);
document.addEventListener('DOMContentLoaded', function() {
let stars = document.querySelectorAll('.star');
stars.forEach(function(star) {
star.addEventListener('click', setRating);
});
let temps = parseInt(document.querySelector('.stars').getAttribute('data-rating'));
console.log("Rating 2: " + this.rating);
let target = stars[temps - 1];
target.dispatchEvent(new MouseEvent('click'));
});
}
function setRating(ev) {
//Printing 'undefined' in console.log
console.log('At top: ' + this.rating);
let span = ev.currentTarget;
let stars = document.querySelectorAll('.star');
let match = false;
let num = 0;
stars.forEach(function(star, index) {
if (match) {
star.classList.remove('rated');
} else {
star.classList.add('rated');
}
//are we currently looking at the span that was clicked
if (star === span) {
match = true;
num = index + 1;
}
});
this.rating = num;
console.log("value after update: " + this.rating)
document.querySelector('.stars').setAttribute('data-rating', num.toString());
}
}
the "value after update: " console log prints "undefined" unless this.rating is assigned to num. Can someone please help me with how to access the value of rating variable in setRating() function and how to update its value?
It's a context binding issue, you have to bind the setRating function to the class this otherwise it is going to use its own this which is different than the classes this no having access to this.rating. You can achieve this by using setRating.bind(this).
You can start by changing the DOMContentLoaded to an arrow function so that you inherit the context's this like so:
document.addEventListener('DOMContentLoaded', () => {
// this.rating is visible here now
...
})
Then you can do the same to the forEach handler:
stars.forEach((star) => {
// this.rating is visible here now too
...
});
Finally, you can bind the this of your external function to the classes this:
star.addEventListener('click', setRating.bind(this));
Your final code would be something like bellow:
export class UserFeedbackComponent implements OnInit {
rating: number;
constructor() {
this.rating = 3;
}
ngOnInit() {
//initial setup
console.log("Rating " + this.rating);
document.addEventListener('DOMContentLoaded', () => {
let stars = document.querySelectorAll('.star');
stars.forEach((star) => {
star.addEventListener('click', setRating.bind(this));
});
let temps = parseInt(document.querySelector('.stars').getAttribute('data-rating'));
console.log("Rating 2: " + this.rating);
let target = stars[temps - 1];
target.dispatchEvent(new MouseEvent('click'));
});
}
function setRating(ev) {
//Printing 'undefined' in console.log
console.log('At top: ' + this.rating);
let span = ev.currentTarget;
let stars = document.querySelectorAll('.star');
let match = false;
let num = 0;
stars.forEach(function(star, index) {
if (match) {
star.classList.remove('rated');
} else {
star.classList.add('rated');
}
//are we currently looking at the span that was clicked
if (star === span) {
match = true;
num = index + 1;
}
});
this.rating = num;
console.log("value after update: " + this.rating)
document.querySelector('.stars').setAttribute('data-rating', num.toString());
}
}
Further observation: You are declaring a function inside a class, that is totally unnecessary, you can declare it as a member
export class UserFeedbackComponent implements OnInit {
...
setRating(ev) {
...
}
}
Then you don't even ave to bind it to call it like so:
star.addEventListener('click', ev => this.setRating(ev));
You do not have to define the function using the function keyword in the class. Declare it as a class member and you should be able to access it normally. This is the best practice in Typescript class declaration.
EX:
export class UserFeedbackComponent implements OnInit {
rating: number;
constructor() {
this.rating = 3;
}
ngOnInit() {
//initial setup
console.log("Rating " + this.rating);
......................more code............
}
setRating(ev) {
//Printing 'undefined' in console.log
console.log(this.rating); //should work fine
// do other stuff with class members
}
}
I am having trouble injecting the Angular built-in orderByFilter. Any suggestion would be helpful. Thanks in advance! I am getting this error:
TypeError: undefined is not a constructor (evaluating 'this.orderBy(this.registrationList, this.registrationSort, this.sortReverse)
Here is my component:
import { RegistrationModel } from "../models/RegistrationModel";
import { IRegistration } from "../../../services/interfaces/IRegistration";
export class RegistrationsComponent implements ng.IComponentOptions {
public static componentName = "registrations";
public template: string = require("./Registrations");
public controller = RegistrationsController;
}
export class RegistrationsController {
static $inject = ["RegistrationModel", "orderByFilter"];
public registrationsList: IRegistration[] = [];
public registrationSort: string;
public sortReverse: boolean = false;
constructor(
public RegistrationModel: RegistrationModel,
public orderBy
) {}
public sortRegistrations() {
this.registrationsList = this.orderBy(this.registrationsList, this.registrationSort, this.sortReverse);
}
}
Here is my unit test:
import "angular-mocks";
import { RegistrationsComponent, RegistrationsController } from "./RegistrationsComponent";
import { IRegistration } from "../../../services/interfaces/IRegistration";
describe("Registrations", () => {
var registrationModelMock = {};
var orderBy;
beforeEach(angular.mock.module("onboardingTestApp", ($provide: any) => {
$provide.service("RegistrationModel", () => registrationModelMock);
$provide.service("orderByFilter", () => orderBy);
}));
var registrationList = [
{
Created: "2016-05-13",
Email: "test1#test.com",
},
{
Created: "2017-03-13",
Email: "test2#test.com",
},
]
var registrationController = new RegistrationsController(<any>registrationModelMock, orderBy);
it("should sort the registrations", () => {
registrationController.registrationsList = <any>registrationList;
registrationController.registrationSort = "Email";
registrationController.sortReverse = true;
registrationController.sortRegistrations();
expect(registrationList[0].Email).toBe("test2#test.com");
});
});
There is no 'orderBy' pipe in angular 2. They do have other pipes like currency, uppercase, but they removed orderBy.
They removed because of the performance issue from previous angular version.
Here is the the documentation that states that. https://angular.io/docs/ts/latest/guide/pipes.html
Read the appendix part that explains lot more about this.
As I'm learning Angular 2 I used an observable to fetch some data via an API. Like this:
getPosts() {
return this.http.get(this._postsUrl)
.map(res => <Post[]>res.json())
.catch(this.handleError);
}
My post model looks is this:
export class Post {
constructor(
public title: string,
public content: string,
public img: string = 'test') {
}
The problem I'm facing is that the map operator doesn't do anything with the Post model. For example, I tried setting a default value for the img value but in the view post.img displays nothing. I even changed Post[] with an other model (Message[]) and the behaviour doesn't change. Can anybody explain this behaviour?
I had a similar issue when I wanted to use a computed property in a template.
I found a good solution in this article:
http://chariotsolutions.com/blog/post/angular-2-beta-0-somnambulant-inauguration-lands-small-app-rxjs-typescript/
You create a static method on your model that takes an array of objects and then call that method from the mapping function. In the static method you can then either call the constructor you've already defined or use a copy constructor:
Mapping Method
getPosts() {
return this.http.get(this._postsUrl)
.map(res => Post.fromJSONArray(res.json()))
.catch(this.handleError);
}
Existing Constructor
export class Post {
// Existing constructor.
constructor(public title:string, public content:string, public img:string = 'test') {}
// New static method.
static fromJSONArray(array: Array<Object>): Post[] {
return array.map(obj => new Post(obj['title'], obj['content'], obj['img']));
}
}
Copy Constructor
export class Post {
title:string;
content:string;
img:string;
// Copy constructor.
constructor(obj: Object) {
this.title = obj['title'];
this.content = obj['content'];
this.img = obj['img'] || 'test';
}
// New static method.
static fromJSONArray(array: Array<Object>): Post[] {
return array.map(obj => new Post(obj);
}
}
If you're using an editor that supports code completion, you can change the type of the obj and array parameters to Post:
export class Post {
title:string;
content:string;
img:string;
// Copy constructor.
constructor(obj: Post) {
this.title = obj.title;
this.content = obj.content;
this.img = obj.img || 'test';
}
// New static method.
static fromJSONArray(array: Array<Post>): Post[] {
return array.map(obj => new Post(obj);
}
}
You can use the as keyword to de-serialize the JSON to your object.
The Angular2 docs have a tutorial that walks you through this. However in short...
Model:
export class Hero {
id: number;
name: string;
}
Service:
...
import { Hero } from './hero';
...
get(): Observable<Hero> {
return this.http
.get('/myhero.json')
.map((r: Response) => r.json() as Hero);
}
Component:
get(id: string) {
this.myService.get()
.subscribe(
hero => {
console.log(hero);
},
error => console.log(error)
);
}
I'm writing my angular app in typescript.
For sake of redundancy prevention I would like to accomplish some type of generic handling.
This is where I'm coming from:
class BaseProvider {
api_url = 'http://localhost:80/api/FILL_OUT_PATH/:id';
$get($resource){
var provider = $resource(this.api_url, {}, {
update: {
method: 'PUT'
}
});
return provider;
}
}
and
class User extends BaseProvider{
constructor() {
super();
this.api_url = 'http://localhost:80/api/users/:id';
}
}
then
module Controllers
{
export class BaseController {
message = "Base controller";
entity : any;
entities : any;
constructor($scope)
{
}
}
}
and
module Controllers
{
export class UserController extends BaseController {
name = "UserController";
constructor($scope, User)
{
super($scope);
this.entity = new User();
this.entities = User.query();
$scope.vm = this;
}
}
}
This is where I'd like to go with UserController (P-Code):
module Controllers
{
export class UserController<T extends BaseProvider> extends BaseController {
name = "UserController";
static $inject = ['$scope', T.typename]; // Inject the types name somehow?
constructor($scope, entity)
{
super($scope);
this.entity = new T();
this.entities = T.query();
$scope.user = this;
}
}
Is there a facility in typescript to handle this?
Is there a facility in typescript to handle this?
No. All type information is erased from the generated JS so you cannot use generic parameters as variables.
There is no way to do it with generics since there will be no typing related information at the runtime, but as a workaround you can pass the type via constructor and type safety with generics. The following code compiles without error and shows how it could be accomplished.
class C1 {
m() { }
}
class C2 extends C1 { }
class C<T extends C1> {
constructor(t: { new (): C1 }) {
var instance = new t();
}
}
new C(C2);
E.g. in angularJS I may use the following construction:
myApp.factory('MyFactory', function(injectable) {
return function(param) {
this.saySomething = function() {
alert("Param=" + param + " injectable=" +injectable);
}
};
});
This can later be used like this:
function(MyFactory) {
new MyFactory().saySomething();
}
When the function passed to the method factory gets invoked, the param injectable is caged and will further be available to new instances of MyFactory without any need to specify that parameter again.
Now I want to use TypeScript and obviously I want to specify that my MyFactory is newable, and has a function saySomething. How could I do this elegantly?
I could write something like this:
class MyFactory {
constructor(private injectable, private param) {}
saySomething() {
alert(...);
}
}
myApp.factory('myFactory', function(injectable) {
return function(param) {
return new MyFactory(injectable, param);
}
});
But this changes the API:
function(myFactory) {
myFactory().saySomething();
}
I wonder if it could be more elegant, because I like how the "new" expresses quite clearly that a new unique object is created and this object creation is the whole purpose of the factory.
** Edit: TypeScript >= 1.6 supports class expressions and you can now write things like:
myApp.factory(injectable: SomeService) {
class TodoItem {
...
}
}
** Original answer:
I have the same problem: with AngularJS and ES5, I enjoy dependency injection not polluting constructors and be able to use the new keyword.
With ES6 you can wrap a class inside a function, this is not yet supported by TypeScript (see https://github.com/Microsoft/TypeScript/issues/307).
Here what I do (MyFactory is now class TodoItem from a todo app to be more relevant):
class TodoItem {
title: string;
completed: boolean;
date: Date;
constructor(private injectable: SomeService) { }
doSomething() {
alert(this.injectable);
}
}
class TodoItemFactory() {
constructor(private injectable: SomeService) { }
create(): TodoItem {
return new TodoItem(this.injectable);
}
// JSON from the server
createFromJson(data: any): TodoItem {
var todoItem = new TodoItem(this.injectable);
todoItem.title = data.title;
todoItem.completed = data.completed;
todoItem.date = data.date;
return todoItem;
}
}
// In ES5: myApp.factory('TodoItem', function(injectable) { ... });
myApp.service('TodoItemFactory', TodoItemFactory);
class TodosCtrl {
// In ES5: myApp.controller('TodosCtrl', function(TodoItem) { ... });
constructor(private todoItemFactory: TodoItemFactory) { }
doSomething() {
// In ES5: var todoItem1 = new TodoItem();
var todoItem1 = this.todoItemFactory.create();
// In ES5: var todoItem2 = TodoItem.createFromJson(...)
var todoItem2 = this.todoItemFactory.createFromJson(
{title: "Meet with Alex", completed: false}
);
}
}
This is less elegant than with ES5 and functions (and not using classes with TypeScript is a no go) :-/
What I would like to write instead:
#Factory
#InjectServices(injectable: SomeService, ...)
class TodoItem {
title: string;
completed: boolean;
date: Date;
// No DI pollution
constructor() { }
saySomething() {
alert(this.injectable);
}
static createFromJson(data: string): TodoItem {
...
}
}
#Controller
#InjectFactories(TodoItem: TodoItem, ...)
class TodosCtrl {
constructor() { }
doSomething() {
var todoItem1 = new TodoItem();
var todoItem2 = TodoItem.createFromJson({title: "Meet with Alex"});
}
}
Or with functions:
myApp.factory(injectable: SomeService) {
class TodoItem {
title: string;
completed: boolean;
date: Date;
// No constructor pollution
constructor() { }
saySomething() {
alert(injectable);
}
static createFromJson(data: string): TodoItem {
...
}
}
}
myApp.controller(TodoItem: TodoItem) {
class TodosCtrl {
constructor() { }
doSomething() {
var todoItem1 = new TodoItem();
var todoItem2 = TodoItem.createFromJson({title: "Meet with Alex"});
}
}
}
I could write something like this
This is what I do
Can I create a TypeScript class within a function
No it needs to be at the top level of the file or in a module. Just FYI if were able to create it inside a function the information would be locked inside that function and at least the type info would be useless.
What's the reason for instantiating multiple instances of MyFactory? Would you not want a single instance of your factory to be injected into your dependent code?
I think using the class declaration you provided will actually look like this once injected:
function(myFactory) {
myFactory.saySomething();
}
If you are really needing to pass a constructor function into your dependent code, then I think you will have to ditch TypeScript classes, since they can't be defined inside of a function which means you would have no way to create a closure on a variable injected into such function.
You do always have the option of just using a function in TypeScript instead of a class. Still get the strong typing benefits and can call 'new' on it since it is still a .js function at the end of the day. Here's a slightly more TypeScriptiffied version:
myApp.factory('MyFactory', (injectable: ng.SomeService) => {
return (param: string) => {
return {
saySomething: () {
alert("Param=" + param + " injectable=" +injectable);
}
};
};
});