My life with the Cargo Code Cult
När man stöter på något man inte förstår kan man antingen ge upp direkt eller så tar man smällen, visa sig stor som människan, rullar upp ärmarna och gör jobbet för att lära sig. Så när jag stötte på JavaScript så valde jag naturligtvis att ignorera existensen av JavaScript och ta mig an TypeScript istället. I ärlighetens namn har jag bråkat med JavaScript i flera år. Jag tog mig aldrig an något mer avancerat än Cargo Cult-programmering av vymodeller med Knockout.js men med TypeScript så har jag hittat min brygga mellan det dynamiska och prototypbaserade JavaScript och C# som jag framgångsrikt har kodat med i flera år nu. Så det va med en viss iver jag tog mig an att bygga om ett av mina Cargo Cult-uppdrag till TypeScript och Knockout.
It’s a trap!
Den första fällan jag gick i va att direkt slänga all min nya kod i en module. Jag hade läst någonstans att modules är som namespaces och all min C# kod ligger i ett namespace, och därmed ska givetvis min TypeScript-kod ligga i ett namespace:
module administration {
class ViewModel {
}
}
Riktigt så enkelt är det inte. Det är antagligen inget fel på modules, men förstår man inte alla konsekvenser av detta (vilket jag uppenbarligen inte gjorde) så är det stor risk att stöta på problem när man rör sig utanför de enklaste exemplen.
DevExpress
Vi använder DevExpress-komponenter i vårt projekt. DevExpress bakar in jQuery i sina komponenter och vanligtvis är detta inte ett problem. När jag hade min kod i en module däremot så blev alla mina anrop till $.map anrop till DevExpress’s inbakade version av jQuery.
Utökning av Knockout
Tillägg med push i en observableArray innebär att DOM:en uppdateras för varje anrop. Jag googlade mig fram till http://jsfiddle.net/johnpapa/nfnbD/ som visar ett enkelt sätt att utöka knockout med en pushAll. John Papas lösning påminde mig om Extension Method-begreppet i C# så jag började genast att försöka översätta det till TypeScript kod. Efter en del försök så kändes följande som det borde fungera:
module administration {
interface KnockoutObservableArray<T> extends KnockoutObservable<T[]>, KnockoutObservableArrayFunctions<T> {
pushAll(items: T[]): void;
}
ko.observableArray.fn['pushAll'] = function (valuesToPush: Array<any>) {
var underlyingArray = this();
this.valueWillMutate();
ko.utils.arrayPushAll(underlyingArray, valuesToPush);
this.valueHasMutated();
return this;
}
}
Nu vet jag att det hade fungerar rätt väl om jag bara hade slopat module. Jag antar att det beror på att en extension måste ligga i samma scope som det som utökas.
Det ska nämnas att interface:et som utökas måste återdefinieras med samma signatur. Följande fungerar inte:
interface KnockoutObservableArray<T> {
pushAll(items: T[]): void;
}
this != this && this.self === undefined
Det andra stora hindret är att ett vanligt mönster i vymodellerna för Knockout, och säkerligen andra JavaScripts-användningar, är att binda en lokal variabel till this:
var AccessViewModel = function() {
var self = this;
}
Den är bra att ha eftersom i Knockout går det utmärkt att binda till click och andra events. I event, och som jag förstått det andra callback-anrop som har sitt ursprung utanför JavaScript, så sätts this till objektet som genererade callback-anropet. Exempelvis vid musklickning blir this satt till html-objektet som klickades på.
Problemet med mönstret var self = this; är att i TypeScript så nås alla klassvariabler via this.
class User {
firstName: string;
lastName: string;
constructor(firstName: string, lastName: string) {
this.firstName = firstName;
this.lastName = lastName;
}
showName(): void {
alert(this.firstName + ' ' + this.lastName);
}
}
Om showName är triggat från ett click i Knockout-vyn:
<input type="submit" data-bind='click: $parent.showName' />
så kommer this i showName ovan inte vara objektet som funktionen ligger på, och att fixa en var self = this; funkar inte för this används för att nå klassvariabler:
class User {
...
self: User;
constructor(firstName: string, lastName: string) {
...
this.self = this;
}
showName(): void {
// self fungerar inte eftersom den också måste nås via this
alert(this.self.firstName + ' ' + this.self.lastName);
}
En lösning runt det problemet är att skapa din funktion som en tom funktionsvariabel, och sätta den i konstruktorn med en arrow function. För C#:are så är en arrow function liknande ett lambda-uttryck. Om en sådan används så ändrar det beteendet på this som istället innehåller referens till klassen i vilken funktionen är definierad:
class User {
firstName: string;
lastName: string;
showName: () => void;
constructor(firstName: string, lastName: string) {
this.firstName = firstName;
this.lastName = lastName;
showName = () => {
// this går bra att använda här för att nå klassens variabler
alert(this.firstName + ' ' + this.lastName);
}
}
}
Lämna gärna en kommentar, eller så hittar ni mig i skuggorna på twitter under @freljung.