Eine AngularJs 1.x SPA mit TypeScript 1.4 erstellen


In meinem letzten Eintrag habe ich bereits einen kurzen Einblick in die Verwendung von TypeScript und Visual Studio gegeben. Da ich in einem Großteil meiner Projekte AngularJs verwende hat mich ebenfalls interessiert, wie man mit TypeScript 1.4 und AngularJs 1.x eine Anwendung schreibt und in der Anwendung z.B. “alte” JavaScript Direktiven verwendet.

Ich verwende in meiner Anwendung die folgenden Komponenten:

  • ASP.NET MVC zum Bereitstellen der AJAX Funktionen (Daten) und Views
  • AngularJs 1.3
  • Ui-Router
  • TypeLite zum erstellen von TypeScript Interfaces für ausgewählte .NET Klassen die mit einem passenden Attribut gekennzeichnet werden
  • TypeScript 1.4

Die Anwendung selbst ist in unterschiedliche “Komponenten” (Dateien) aufgeteilt, welche sich im “ScriptsApp” Ordner im Root der Webseite befinden. Um meine Anwendung besser zu strukturieren verwende ich in meinem Beispiel ebenfalls TypeScript “module” was einem Namespace in .NET ähnelt bzw. entsprechen soll und der Namespace entspricht hier prinzipiell der Ordnerstruktur.

image

1. Main App

Der zentrale Ausgangspunkt ist die “app.main.ts” Datei, in der die zentrale Moduldefinition abgelegt wird und die Anwendung Initial aufgerufen wird. Wichtig ohne die Zeile “App.MainApp.createApp(angular)” – würde hier “nichts” passieren denn die App würde nie gestartet/erstellt.

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",
                "mgcrea.ngStrap.datepicker",
                //Eigene Module einbinden
                "deafaultModal.directives",
                //Module die mit TypeScript geschrieben wurden einbinden
                Views.MainAppCtrl.module.name,
                Views.Todo.TodoOverviewCtrl.module.name,
                Views.Todo.TodoEditModalCtrl.module.name,
                Views.Shared.TodoModalService.module.name,
                Views.Shared.TodoListenService.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);

Wie man bereits bei der Moduldefinition erkennen kann stellt es keine Probleme dar TypeScript Komponenten mit “alten” JavaScript Komponenten zu verwenden. Ich habe z.B. das Modal von ui-bootstrap problemlos verwenden können. Wichtig ist nur das die aus dem TypeScript erstellten JavaScript Dateien in der richtigen Reihenfolge eingebunden werden z.B. in der “BundleConfig”.

bundles.Add(new ScriptBundle("~/bundles/mainApp")
    .IncludeDirectory("~/ScriptsApp/directives", "*.js", true)
    .Include(
            "~/ScriptsApp/Views/routeConfig.js",
            "~/ScriptsApp/Views/mainCtrl.js",
            "~/ScriptsApp/Services/todoPSrv.js",
            "~/ScriptsApp/Views/Shared/todoListenService.js",
            "~/ScriptsApp/Views/Shared/todoModalService.js",
            "~/ScriptsApp/Views/Todo/todooverviewctrl.js",
            "~/ScriptsApp/Views/Todo/todoEditModalCtrl.js",
            "~/ScriptsApp/Views/app.main.js"
            ));

  2. Controller

Damit man auch das gesamte Potential von TypeScript ausnutzen kann, definiere ich für jeden Controller/Service noch ein passendes Interface über die bereitgestellten Funktionen und Member für diesen Controller.

//Alle Variablen für unseren Controller im Interface definieren
export interface ITodoOverviewCtrl {
    //Locals
    searchModel: My.ITodoOverviewSearchModel;
    resultModel: My.ITodoOverviewResultModel;
    initModel: My.ITodoOverviewInitModel;
    locals: LocalsModel;
    //Functions
    init(): void;
    search(): void;
    deleteEntry(id: number): void;
    toggleNewEntry(): void;
    getPrioString(prio: MvcTypeScript.Helper.Prioritaet): string;
    listenService: App.Views.Shared.ITodoListenService;
}

Der Controller selbst bindet dieses Interface ein und implementiert die entsprechenden Methoden und Member. Damit man die Dependency Injection von AngularJs verwenden kann, benötigt man nur “$inject” und die entsprechenden Modulnamen die Injected werden sollen, dabei ist wie immer die Reihenfolge ausschlaggebend, denn die muss der im constructor entsprechen, wie im Standard AngularJs ohne TypeScript.

 

//Unsere TodoController Klasse erstellen
export class TodoOverviewCtrl implements ITodoOverviewCtrl {
      searchModel: My.ITodoOverviewSearchModel;
      resultModel: My.ITodoOverviewResultModel;
      initModel: My.ITodoOverviewInitModel;
      listenService: App.Views.Shared.ITodoListenService;
      locals : LocalsModel;
      static $inject = [App.Services.TodoPService.module.name, App.Views.Shared.TodoModalService.module.name, App.Views.Shared.TodoListenService.module.name];
      constructor(private todoSrv: App.Services.ITodoPService, private modalFactory: App.Views.Shared.ITodoModalService, listenService: App.Views.Shared.ITodoListenService) {
          this.listenService = listenService;
          this.init();
      }
 ...
}

In jedem AngularJs Modul (Controller, Service, Directive, …) gibt es eine Moduldefinition die das Modul selbst definiert und im “$inject” greift man nur noch auf den “module.name” zu um den Namen des jeweiligen Moduls abzurufen, d.h. der Name muss nur noch einmal “ausgeschrieben” werden.

private static _module: ng.IModule;
/**
 * Stellt das aktuelle Angular Modul für den "todoOverviewCtrl" 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('todoOverviewCtrl', [Services.TodoPService.module.name]);

    this._module.controller('todoOverviewCtrl', TodoOverviewCtrl);
    return this._module;
}

den kompletten Controller findet man in GitHub.

3. Service

Im Service gebe ich die passenden Datentypen aus .NET zurück, für die ich vorher mit TypeLite das passende TypeScript Interface erstellt habe. Auch im Service der einen Teil eines .NET Controllers abbildet stelle ich ein passendes Interface über die Methoden und Rückgabewerte des Services zur Verfügung. Der Service enthält ebenfalls wieder eine passende Modul Definition.

module App.Services {
   export interface ITodoPService {
        initTodoOverviewInitModel(): ng.IPromise;
        initTodoOverviewSearchModel(): ng.IPromise;
        todoOverviewResultModel(searchModel: MvcTypeScript.Models.Todo.ITodoOverviewSearchModel): ng.IPromise;
        initTodoCreateViewModel(): ng.IPromise;
        addOrUpdateTodoItem(createItem: MvcTypeScript.Models.Todo.ITodoCreateViewModel): ng.IPromise;
        deleteTodoEntry(todoItemId: number): void;
        loadTodoItem(todoItemId: number): ng.IPromise;
        initTodoListenViewModel(): ng.IPromise;
   }

    export class TodoPService implements ITodoPService {
        static $inject = ['$http'];
        constructor(private $http: ng.IHttpService) { }

        initTodoOverviewInitModel(): ng.IPromise {
            return this.$http.get('Todo/InitTodoOverviewInitModel').then((response: ng.IHttpPromiseCallbackArg): MvcTypeScript.Models.Todo.ITodoOverviewInitModel
                => { return response.data; });
        }

       ...

        initTodoListenViewModel(): ng.IPromise {
            return this.$http.get('Todo/InitTodoListenViewModel').then((response: ng.IHttpPromiseCallbackArg): MvcTypeScript.Models.Todo.ITodoListenViewModel
                => { return response.data; });
        }

        //#region Angular Module Definition
        private static _module: ng.IModule;
        public static get module(): ng.IModule {
            if (this._module) {
                return this._module;
            }
            this._module = angular.module('todoPSrv', []);
            this._module.service('todoPSrv', TodoPService);
            return this._module;
        }
        //#endregion
    }
}

Das gesamte Beispiel kann man sich in GitHub anschauen. Dabei handelt es sich um einen ersten Entwurf. Über Kommentare was die Umsetzung angeht würde ich mir freuen, wenn schon jemand Erfahrungen damit sammeln konnte.

Die folgenden Links haben mir ebenfalls weitergeholfen um meine erste AngularJS Anwendung in TypeScript zu erstellen:

https://www.youtube.com/watch?v=32VyeinxbbI

http://kwilson.me.uk/blog/writing-cleaner-angularjs-with-typescript-and-controlleras/

http://dotnetbyexample.blogspot.de/2014/07/angularjs-typescript-setting-up-basic.html

http://sirarsalih.com/2014/01/28/when-two-forces-meet-angularjs-typescript/

http://www.dotnetcurry.com/showarticle.aspx?ID=1016

Advertisements

3 Gedanken zu „Eine AngularJs 1.x SPA mit TypeScript 1.4 erstellen

  1. Pingback: Zwei Monate mit TypeScript und AngularJs – meine Erfahrungen | SquadWuschel's Blog

  2. iSOcH

    Danke für diesen Post! :)

    Bzgl. Typescript und Angular bin ich noch ein rechter Neuling.
    Ich habe eine Frage zu der Moduldefinition in den Controller/Service-Klassen. So wie Du das machst, definierst du ja für jeden Controller, jeden Service etc. ein separates Modul.
    Bei JS Angular-Anleitungen habe ich das eigentlich noch nie gesehen, da wird immer dasselbe Modul verwendet und da einfach z.B. mit .controller() ein Controller drangehängt.

    Also bei deinem Beispiel:
    angular.module(„meinController“, […]).controller(„meinController“, meinControllerConstructor)

    „Üblich“ hingegen (soweit ich das sehe):
    angular.module(„meineAppAnDieIchSchonAnderesDrangehängtHabe“).controller(„meinController“, meinControllerConstructor)

    Kannst du erläutern, warum Du das so gemacht hast? Was sind die Unterschiede und ggf. Vor- und Nachteile?

    Gefällt mir

    Antwort
    1. SquadWuschel Autor

      Hi,

      so lange es sich bei deiner Anwendung um eine kleine überschaubare OnePageApplication handelt, ist das direkte Aneinanderhängen von Modulen sicherlich auch ok.

      Aber dann hast du deinen ganzen Code auch in einer Datei, was irgendwann unübersichtlich wird. Ich setze in meinen Projekten immer auf viele einzelne TypeScript Dateien die immer z.B. einen Service, Controller oder direktive beinhalten. Diese Dateien werden dann auch noch in einer passenden Ordnerstruktur abgelegt, damit man alles so leicht wie möglich wiederfindet, wie die Struktur genau aussieht, solltest du dir selbst ausdenken, denn du musst damit arbeiten und klarkommen.

      Angular eignet sich außerdem sehr gut um seine Projekte modular aufzubauen, bei den meisten Beispielen im Netz handelt es sich aber meist um kurze Snippets und hier macht es natürlich keinen Sinn diese noch einmal in eigene Module aufzuteilen, ich habe auch ein passenden GitHub Projekt von mir verlinkt und wie du dort sehen kannst, lege ich meine AngularProjekte auch in einem eigenen Root Ordner „ScriptApps“ ab.

      Wirkliche Nachteile gibt es an dieser Variante nicht, würde ich von meinem aktuellen wissensstand aus behaupten wollen. Denn wenn du dir Bestpractices für größere Angular Projekte anschaust, wird das dort auch änlich gehandhabt und nicht mehr alles aneinander gereiht in einer Datei.

      p.s. Wenn du neu in Angular bist, würde ich dir erst einmal empfehlen ein oder zwei Projekte im purem AngularJS anzufertigen und dann auf jedenfall auf TypeScript umzusteigen, wenn die Controller und Services entsprechend in TypeScript Klassen abbilden willst. Dann versteht man das ganze etwas besser. Wenn du TypeScript erst einmal nur für die Typsicherheit verwenden willst, dann auf jedenfall von Anfang an TypeScript verwenden.

      MFG
      SquadWuschel

      Gefällt mir

      Antwort

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s