Archiv für den Monat März 2016

AngularJs mit TypeScript und Resharper Templates


Seit gut 9 Monaten arbeite ich mit TypeScript und AngularJs zusammen und habe mir für die wichtigsten AngularJs Komponenten passende TypeScript Klassen gebaut. In Verbindung mit Resharper lässt sich damit sehr schnell eine funktionierende AngularJs Anwendung mit TypeScript aufsetzen, denn ich habe mir für die wichtigsten AngularJs Komponenten fertige Templates in Resharper hinterlegt. Denn auch hier gibt es wieder X Möglichkeiten eine Anwendung in TypeScript und AngularJs zu schreiben.

Es war mir aber erst mit der Benutzung von TypeScript Klassen möglich auch durchweg eine gute IntelliSense zu erhalten. Da die Implementierung von AngularJs mit TypeScript Klassen aber alles andere als intuitiv ist, war ich froh als ich für jede AngularJs Komponente die passende Klassen Implementierung erstellt hatte, dies war mir vor allem durch die gute Unterstützung der StackOverflow Community möglich.

Meine AngularJs Anwendungen bauen dabei auf folgende Komponenten auf:

  • Twitter Bootstrap 3.x (Styles und Modal)
  • AngularJs 1.4.x
  • Ui-Router
  • Angular-Ui (Modal)
  • AngularStrap für weitere Bootstrap Komponenten wie z.B. Date und Timepicker
  • aktuelle TypeScript Version die von Resharper unterstützt wird.

Ein komplettes sehr einfaches Beispiel, in dem ich alle folgenden Komponenten verwende findet Ihr auch auf meinem GitHub [AngularJsTypeScriptTemplates] Account, das Projekt ist als Visual Studio Solution hinterlegt. Außerdem findet Ihr dort auch meine TypeScript Live Templates für Resharper, diese lassen sich sehr einfach mit Resharper importieren und an eure Bedürfnisse anpassen. Im Folgenden findet Ihr einfach ein Beispiel für die jeweiligen AngularJs Komponenten umgesetzt in TypeScript.

Ich bin natürlich immer gern für Verbesserungsvorschläge offen und freue mich über Kommentare sollte ich etwas “falsch” machen oder wenn ich etwas besser umsetzen kann. Außerdem kann ich nur jedem empfehlen, einmal TypeScript auszuprobieren, ich könnte/möchte inzwischen nicht mehr ohne TypeScript arbeiten wollen, denn IntelliSense und Typsicherheit auch auf Clientseite sind einfach unbezahlbar.

1. Definieren der MainApp als Einstiegspunkt für die Anwendung

In der MainApp werden alle wichtigen Module registriert und z.b. die Routenkonfiguration aufgerufen.

module App {
    export class MainApp {
        static createApp(angular: ng.IAngularStatic) {
            //Alle Module definieren die wir verwenden.
            angular.module("app.main", [
                //Fremdanbieter Module
                "ui.router",
                "ui.bootstrap",
                //Module die mit TypeScript geschrieben wurden einbinden
                App.Controller.TodoModalCtrl.module.name,
                App.Controller.TodoCtrl.module.name,
                App.Services.TodoService.module.name,
                App.Services.ModalService.module.name,
                App.Directives.CtrlAsDirectiveName.module.name,
                App.Directives.DirectiveName.module.name,
                App.Filter.AddPrefixFilter.module.name,
            ])
            .config([
                "$stateProvider", "$urlRouterProvider", "$locationProvider", ($stateProvider: ng.ui.IStateProvider, $urlRouterProvider: ng.ui.IUrlRouterProvider, $locationProvider: ng.ILocationProvider) => {
                    return new Config.RouteConfig($stateProvider, $urlRouterProvider, $locationProvider);
                }
            ]);
        }
    }
}

//Unsere Anwendung intial aufrufen/starten
App.MainApp.createApp(angular);

/*Window Interface erweitern, damit wir auch hier auf z.b. eigene Properties zugreifen können die wir z.B: in alten JS dateien definiert haben */
interface Window {
    siteRoot: string;
}

2. Festlegen der Routen für Ui-Router

module App.Config {
    /**
     * Enthält die Routenconfigutation für unsere Anwendung.
    */
    export class RouteConfig {
        constructor(private $stateProvider: ng.ui.IStateProvider, private $urlRouterProvider: ng.ui.IUrlRouterProvider, private $locationProvider: ng.ILocationProvider) {
            this.init();
        }

        private init(): void {
            //$urlRouterProvider.when('/Home', '/home/index');
            this.$urlRouterProvider.otherwise("Todo");
            //$locationProvider.html5Mode(true);

            //Definieren der Routen die in unserer App zur Verfügung stehen.
            this.$stateProvider
                .state("Todo", {
                    url: "/Todo",
                    templateUrl: "Todo/TodoList"
                })
                .state("Todo.Overview", {
                    url: "/Overview",
                    templateUrl: "Todo/TodoOverview"
                });
        }
    }
}

3. Einrichten eines Controllers

Alle AngularJs Module die man registrieren kann, enthalten einen Bereich “Module Definition”, hier wird der Name des Moduls und der Typ festgelegt und z.B. in der MainApp als Abhängigkeit hinzugefügt. Achtung es wird die “ControllerAs ” Syntax verwendet, bei dem hier gezeigten Beispiel für einen Controller. Achtung beim Inject aufpassen, denn die Reihenfolge im Inject musst der Reihenfolge im Konstruktor der Klassen entsprechen.

module App.Controller {
    //import My = Sq.Internal.ProjectName.Gui.Web.Models;

    //Alle Variablen für unseren Controller im Interface definieren
    export interface ITodoCtrl {
        //Locals
        //viewModel: My.;
        //Functions
        init(): void;
    }

    //Unsere CtrlName Klasse erstellen
    export class TodoCtrl implements ITodoCtrl {
        private locals: CtrlNameLocalsModel = new CtrlNameLocalsModel();
        static $inject: string[] = [
            App.Services.TodoService.module.name,
            App.Services.ModalService.module.name,
        ];

        constructor(private todoService: Services.ITodoService,
                    private modalService: Services.IModalService) {
            this.init();
        }

        /**
        * Initialisieren der wichtigsten lokalen Variablen
        */
        init(): void {
            this.locals.persons = this.todoService.getPersonList();
        }

        public editPerson(personId: number): void {
            this.modalService.editPerson(personId).then(() => {
               //Do Something nachdem das Modal geschlossen wurde.
            });
        }


        //#region Angular Module Definition
        private static _module: ng.IModule;
        /**
         * Stellt das aktuelle Angular Modul für den "CtrlName" bereit.
         */
        public static get module(): ng.IModule {
            if (this._module) {
                return this._module;
            }

            //Hier die abhängigen Module für diesen controller definieren, damit brauchen wir von außen nur den Controller einbinden
            //und müssen seine Abhängkeiten nicht wissen.
            this._module = angular.module('todoCtrl', []);
            this._module.controller('todoCtrl', TodoCtrl);
            return this._module;
        }
        //#endregion
    }

    class CtrlNameLocalsModel {
        persons: App.Person[] = [];
        name : string = "SquadWuschel";
    }
} 

4. Einrichten eines Services

Auch der AngularJs Service enthält wieder einen Bereich für Module Definitionen, mit dem man den Service entsprechend in der MainApp registrieren kann.

module App.Services {
    //import My = Internal.;

    export interface ITodoService {
        getPersonList(): App.Person[];
        getPerson(personId: number): Person;
    }

    /**
     * Beschreibung
     */
    export class TodoService implements ITodoService {
        private persons : App.Person[] = [];

        static $inject: string[] = [];

        constructor() {
            this.persons.push(new App.Person("SquadWuschel", 34, 1337));
            this.persons.push(new App.Person("Squadinator", 33, 7331));
            this.persons.push(new App.Person("SquadLeader", 3232312123.4454545, 3173));
        }

        public getPersonList(): App.Person[] {
            return this.persons;
        }

        public getPerson(personId: number): Person {
            var person: Person;

            this.getPersonList().forEach(value => {
                if (value.id === personId) {
                    person = value;
                }
            });

            return person;
        }

        //#region Angular Module Definition
        private static _module: ng.IModule;
        /**
        * Stellt das aktuelle Angular Modul für "ServiceName" bereit.
        */
        public static get module(): ng.IModule {
            if (this._module) {
                return this._module;
            }

            //Hier die Abhängigen Module für diesen Service definieren.
            this._module = angular.module('TodoService', []);
            this._module.service('TodoService', TodoService);
            return this._module;
        }
        //#endregion
    }
} 

5. Einrichten einer ControllerAs Direktive

Auch in der Definition der Direktive gibt es wieder eine Modul Definition und alle bekannten Properties die zu einer Direktive gehören.

module App.Directives {

    /* 
	* Definition der "Scope" Variablen für die CtrlAsDirectiveName Directive
	*/
    export interface ICtrlAsDirectiveNameScope {
        sqTitle: string;
        isDeleted: boolean;
        model: string;
    }

    /*
	* Beschreibung
	*
	* Verwendung: 
	*  <div></div>
	*/
    export class CtrlAsDirectiveName implements ng.IDirective {
        public restrict: string = "A";
        public replcae: boolean = true;
        public require = "ngModel";
        //public templateUrl: string = 'ScriptsApp/directives/templates/CtrlAsDirectiveName.directives.html';
        public template: string = '<a class="btn btn-default" title="{{ctrl.sqTitle}}">Test Counter: {{ctrl.counter}}</a>';
        public scope = {}

        public controller = CtrlAsDirectiveNameCtrl;
        public controllerAs = "ctrl";
        public bindToController = {
            sqTitle: "=", 
            isDeleted: "=",
            model : "=ngModel"
        }

        //constructor(private $interval: angular.IIntervalService) { }
        constructor() { }

        public link = ($scope: any, element: JQuery, attrs: ng.IAttributes, model: ng.INgModelController) =&gt; {

        }


        //#region Angular Module Definition
        private static _module: ng.IModule;
        /**
        * Stellt die Angular Module für CtrlAsDirectiveName bereit.
        */
        public static get module(): ng.IModule {
            if (this._module) {
                return this._module;
            }

            //Hier die abhängigen Module für unsere Direktive definieren.
            this._module = angular.module('ctrlAsDirectiveName', []);
            //this._module.directive('CtrlAsDirectiveName', ["$interval", ($interval: angular.IIntervalService) =&gt; { return new CtrlAsDirectiveName($interval); }]);
            this._module.directive('ctrlAsDirectiveName', [() =&gt; { return new CtrlAsDirectiveName(); }]);
            return this._module;
        }
        //#endregion
    }

    /*
	* Implementierung unseres CtrlAsDirectiveName Controllers.
	*/
    export class CtrlAsDirectiveNameCtrl implements ICtrlAsDirectiveNameScope {
        public sqTitle: string;
        public isDeleted: boolean;
        public model: string;
        public counter : number;

        static $inject = [];

        constructor() {
            this.init();
        }

        init(): void {
            this.counter = 0;
        }

        /*
		* Eine Funktion die z.B: über ein btn Click des Templates aufgerufen werden kann
		*/
        public doSomethingBtnClick(): void {
            //Zugriff auf die "Scope" Variable mit "this"
            this.sqTitle = "Blubb";
            this.counter++;
        }
    }
}

6. Einrichten eines Filters

module App.Filter {

    /*
     * Beschreibung 
     * 
     * Verwendung: 
     *
     */
    export class AddPrefixFilter {
     
        static $inject: string[] = ["$sce"];
       
        static filter($sce: ng.ISCEService) {
            return (value, text) => {
                return text + ' ' + value;
            }
        }
      
        //#region Angular Module Definition
        private static _module: ng.IModule;

        /**
        * Stellt die Angular Module für AddPrefixFilter bereit.
        */
        public static get module(): ng.IModule {
            if (this._module) {
                return this._module;
            }

            //Hier die abhängigen Module für unsere Direktive definieren.
            this._module = angular.module('addPrefix', []);
            this._module.filter('addPrefix', App.Filter.AddPrefixFilter.filter);
            return this._module;
        }
        //#endregion
    }
}
Advertisements

Visual Studio Code für absolute Beginner – TypeScript debuggen


Normalerweise entwickle ich Webanwendungen mit Visual Studio und habe viel Spaß dabei. Aber man muss auch mal über den Tellerrand hinausschauen, habe ich gedacht und einmal etwas anderes ausprobieren.

Die Aufgabenstellung bestand daraus, einfach nur eine TypeScript “Hallo Welt” Anwendung zu erstellen und zu debuggen. Hier muss ich bereits sagen, dies ist leider leichter gesagt als getan, denn Visual Studio Code reicht hier allein nicht aus und bisher habe ich von nodeJs oder npm nur hier und da etwas gelesen aber bisher noch nicht selbst damit gearbeitet.

Daher mussten zuerst die Entwicklungsumgebung und die passenden Tools installiert werden. Dazu gehört zum einen Visual Studio Code und nodeJs welches auch den npm (node package manager) enthält.

Bei nodeJs handelt es sich um eine Laufzeitumgebung in der JavaScript Programme serverseitig (über die Kommandozeile) ausgeführt werden können, wie z.B. einfache Webserver die in JavaScript geschrieben sind. Und npm ist quasi so etwas ähnliches wie ein NuGet auf Steroiden. Zu npm und nodeJs findet man aber auch diverse Einführungen auf YouTube. Auch zum UI von Visual Studio Code gibt es auf YouTube einiges an Videos.

Nachdem wir alle Tools installiert haben, installieren wir noch die neueste TypeScript Version mit npm. Dies ist zwar nicht zwingend notwendig da VS Code bereits eine aktuelle Version von TypeScript installiert, aber um auch einmal zu sehen wie npm “funktioniert” kann es nicht schaden. Dafür müssen wir die “Node.js Konsole” öffnen.

image

und hier einfach den folgenden Befehl eingeben:

npm install -g typescript

Damit wird TypeScript für nodeJs installiert und kann von VS Code bzw. über die npm Konsole verwendet werden.

Normalerweise gibt es eine gute Anleitung wie man TypeScript in VS Code verwendet in der Dokumentation. Leider hat diese bei mir nur teilweise funktioniert. Es wurde zwar die JavaScript Datei erstellt, aber die Mapping Dateien haben gefehlt, damit man auch Haltepunkte in der TypeScript Datei setzen kann. Daher im Folgenden die Anleitung wie ich es geschafft habe das ich TypeScript in VS Code debuggen konnte.

Dafür einfach ein lokales leeres Verzeichnis auswählen und dieses mit VS Code öffnen. Dann einfach eine neue Datei “app.ts” hinzufügen mit z.B. dem folgenden Inhalt:

module App {
        export class Person {
            constructor(public name: string) {  }

            public log(showLog: boolean): void {
                if (showLog) { 
                    console.log("Der Name des Nutzers ist: " + this.name)
                }
            }
        }
    }

    var person = new App.Person("Hallo Welt");
    person.log(true);

dann eine “tsconfig.json” Datei im gleichen Verzeichnis (root) anlegen mit dem folgenden Inhalt:

{
    "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "sourceMap": true 
    },
    "files": [
        "app.ts"
    ]
}

Das Files Property kann auch entfernt werden, dann werden alle TypeScript Dateien im Verzeichnis kompiliert, wichtig ist das die “sourceMap” erstellt wird, damit wir später auch debuggen können.

Wenn wir jetzt versuchen das ganze zu bauen mit der Tastenkombination “Strg+Shift+B” bekommen wir eine Meldung “No taks runner configured”, jetzt einfach auf “Configure Task Runner” klicken.

image

Jetzt legt VS Code einen neuen Ordner “.vscode” mit der Datei “tasks.json” im Rootverzeichnis unserer Anwendung an.

image

In der “tasks.json” den aktuellen Eintrag auskommentieren und den nächsten Eintrag in der Datei verwenden:

{
	"version": "0.1.0",

	// The command is tsc. Assumes that tsc has been installed using npm install -g typescript
	"command": "tsc",

	// The command is a shell script
	"isShellCommand": true,

	// Show the output window only if unrecognized errors occur.
	"showOutput": "silent",

	// Tell the tsc compiler to use the tsconfig.json from the open folder.
	"args": ["-p", "."],

	// use the standard tsc problem matcher to find compile problems
	// in the output.
	"problemMatcher": "$tsc"
}

Wenn Ihr “Glück” habt, dann könnt Ihr jetzt bereits wieder mit “Strg+Shift+B” eure “app.ts” Datei übersetzen lassen, diese verwendet jetzt die Einstellungen in eurer “tsconfig.json”.

Euer Verzeichnis sollte jetzt folgendermaßen aussehen und noch die “app.js” und “app.js.map” Datei enthalten.

image

Bei mir kam es hier aber zu folgender Fehlermeldung:

error TS5007: Cannot resolve referenced file: '.'.
error TS5023: Unknown option 'p'
Use the '--help' flag to see options.

Hier hat sich herausgestellt, das in der PATH Variable von Windows noch ein Verweis auf TypeScript 1.0 enthalten war, diesen habe ich einfach entfernt VS Code neu gestartet und schon verwendet VS Code die TypeScript Version die ich mit npm installiert habe.

image

Wenn wir jetzt “F5” drücken kommt ein kleines Popup, welches uns nach der Umgebung fragt in der die Anwendung ausgeführt werden soll. Hier wählen wir “Node.js” aus und VS Code legt für uns wieder eine Datei “launch.json” im Verzeichnis “.vscode” an.

image

Hier müssen wir jetzt nur noch “app.js” in “app.ts” ändern und einstellen das sourceMaps verwendet werden sollen, dann können wir einen Haltepunkt in unserer “app.ts” Datei mit “F9” setzen und mit “F5” führen wir unsere Anwendung aus.

{
	"version": "0.2.0",
	"configurations": [
		{
			"name": "Launch",
			"type": "node",
			"request": "launch",
			"program": "${workspaceRoot}/app.ts",
			"stopOnEntry": false,
			"args": [],
			"cwd": "${workspaceRoot}",
			"preLaunchTask": null,
			"runtimeExecutable": null,
			"runtimeArgs": [
				"--nolazy"
			],
			"env": {
				"NODE_ENV": "development"
			},
			"externalConsole": false,
			"sourceMaps": true,
			"outDir": null
		},
		{
			"name": "Attach",
			"type": "node",
			"request": "attach",
			"port": 5858,
			"address": "localhost",
			"restart": false,
			"sourceMaps": false,
			"outDir": null,
			"localRoot": "${workspaceRoot}",
			"remoteRoot": null
		}
	]
}

Und siehe da wenn alles klappt, dann hält der Debugger am Haltepunkt

image

An die vielen Json Dateien muss man sich als .NET Entwickler erst einmal gewöhnen, wenn man in .NET auch noch auf ältere MVC Versionen setzt. Diese sind im Prinzip ganz gut beschrieben und VS Code bietet hier eine gute IntelliSense der einzelnen Einstellungen an.

Aber einen Vorteil muss ich gestehen sehe ich im Moment noch nicht zu meinem komfortablen Visual Studio 2015. Ich werde mich aber noch etwas damit beschäftigen und evtl. noch den einen oder anderen Blogpost über meine Probleme bzw. Fortschritte mit VS Code schreiben.