X
dnn.blog.
Back

DNN module development with AngularJS (Part 4)

The Angular part

Now that we have finished the infrastructure for our module it is time to implement the Angular part. As told before - this is not an Angular course, it should only be shown how the Angular bits play together with DNN. So if you have never seen Angular before stop here and take a look at Angular first.

Controller, Services and Templates

It is a good idea to separate the parts of your app in a folder. So when your app grows you do not end up with get a mess of files in one folder. This is no must but helps a lot to keep the overview. See the following image how I structured the different parts of the app:

Module structure

To load all the new js files we have to include some more lines at the top of our View.ascx beneath the loading of the other libraries:

<dnn:DnnJsInclude runat="server" FilePath="~/DesktopModules/Angularmodule/Script/app.js" Priority="40"/>
<dnn:DnnJsInclude runat="server" FilePath="~/DesktopModules/Angularmodule/Script/itemService.js" Priority="100" />
<dnn:DnnJsInclude runat="server" FilePath="~/DesktopModules/Angularmodule/Script/itemController.js" Priority="100" />

app.js

The app.js in the main script folder is where our Angular app is instantiated:

(function () {
    "use strict";

   var jsFileLocation = $('script[src*="Angularmodule/Script/app"]').attr('src');  // the js file path
    jsFileLocation = jsFileLocation.replace('app.js', '');   // the js folder path
    if (jsFileLocation.indexOf('?') > -1) {
        jsFileLocation = jsFileLocation.substr(0, jsFileLocation.indexOf('?'));
    }
    angular
        .module("itemApp", ["ngRoute","ngDialog","ngProgress","ui.sortable"])
        .config(function ($routeProvider) {
            $routeProvider.
                otherwise({
                    templateUrl: jsFileLocation + "Templates/index.html",
                    controller: "itemController",
                    controllerAs: "vm"
                });
        });

})();

At first we retrieve the full url of our project by using jquery to jsFileLocation. Next we instantiate our module itemApp and inject some other angular libraries with dependency injection:

  • ngRoute: Not really needed in this module but when your module should have different pages you need this for configuration
  • ngDialog: Module for displaying dialog windows
  • ngProgress: Showing a loading bar at the top of the browser window
  • ui.sortable: Module for sorting <li> with drag and drop

Our module routing will be configured to load the template index.htm from templates folder and use the itemController as corresponding controller and name it vm. If you have different pages in your module you can add more cases like:

    .when("/list", {templateUrl: jsFileLocation + "ClientList.html", controller: "clientController", controllerAs: "vm"})
    .when("/edit/:ClientId", {templateUrl: jsFileLocation + "ClientEdit.html",controller: "clientController",controllerAs: "vm"})
    .otherwise({redirectTo: '/list'});

which means: when the url ends with %urltopage%/#/list load template ClientList.html with clientController. When it ends with %urltopage%/#/edit/32 load template ClientEdit.html and bring ClientId=32 as injected parameter into the clientController. In any other case show first case.

index.html

This is our html part of the tasklist:

<div class="angularmodule">
    <ul>
        <li class="panel panel-success" ng-show="vm.EditMode">
            <div class="panel-heading">
                <h3>{{vm.localize.NewItem_Text}}</h3>
                <div class="btn-group btn-group-xs pull-right" role="group">
                    <a class="btn btn-add" ng-click="vm.showAdd()" href="javascript:void(0);">{{vm.localize.AddItem_Text}}</a>
                </div>
            </div>
            
        </li>
    </ul>
    <div>
        <ul ui-sortable="vm.sortableOptions" ng-model="vm.Items">
            <li class="panel panel-success" ng-repeat="item in vm.Items" ng-style="{cursor: vm.EditMode ? 'move' : 'auto'}">
                <div class="panel-heading">
                    <h3><span class="angularmodule-id">#{{item.ItemId}}</span> {{item.Title}}</h3>
                    <div class="btn-group btn-group-xs pull-right" role="group">
                        <a class="btn btn-xs btn-edit" ng-click="vm.showEdit(item, $index)" ng-show="vm.EditMode" href="javascript:void(0);">{{vm.localize.EditItem_Text}}</a>
                        <a class="btn btn-xs btn-delete" ng-click="vm.deleteItem(item, $index)" ng-show="vm.EditMode" href="javascript:void(0);">{{vm.localize.DeleteItem_Text}}</a>
                    </div>
                </div>
                <div class="panel-body">
                    {{item.Description}}
                </div>
                <div class="panel-footer">
                    <span><b><span class="glyphicon glyphicon-user"/></b> {{item.AssignedUserName}}</span>
                    <span class="pull-right"><b><span class="glyphicon glyphicon-calendar"/></b> {{item.CreatedOnDate| date:'short'}}</span>
                </div>
            </li>
        </ul>
    </div>
</div>

The first <ul> defines the Add item button display and is only shown if DNN is in Editmode and the user has edit rights (ng-show="vm.EditMode"). Caption and button text are coming from our injected resources object (e.g. {{vm.localize.NewItem_Text}}, see itemController)

The first <ul> defines our list of task objects and has an extra attribute ui-sortable. This is defined in one of the Javascript libraries we injected in our module [ui.sortable] and it makes our list sortable with drag and drop.

The ng-repeat="item in vm.items" attribute on the <li> element leads to repeating the <li> with all its content for every item in the items collection of our viewModel in the controller (vm).

Clicking a button invokes the corresponding method in the controller. ng-click="vm.deleteItem(item,$index)" for example calls deleteItem in the controller and uses the actual item of the iteration and the $index of this item in the viewModel item array vm.Items[].

itemController.js

The itemController holds all the logic of our module. At first we dependency-inject (like in app.js) the needed bits into our controller. There are some that are defined from Angular:

  • $scope: The actual model scope. We should try to avoid the usage of $scope but it is needed by ngDialog to set the scope of the dialog boxes to our vm
  • $window: Angular equivalent to dom window object
  • $log: Logging to the console of browser debugging tools

The other ones are needed modules and our injected data from codebehind:

  • ngDialog, ngProgress: make usage of dialog and progress bar functionality
  • itemService: the Angular service to communicate with the WebApi (see itemService.js)
  • userList, resources, settings, editable, moduleId: the Json strings we injected in View.ascx as constants
(function () {
    "use strict";

    angular
        .module("itemApp")
        .controller("itemController", itemController);

    itemController.$inject = ["$scope", "$window", "$log", "ngDialog", "ngProgress", "itemService", "userlist", "resources", "settings", "editable","moduleId"];
    
    function itemController($scope, $window, $log, ngDialog, ngProgress, itemService, userlist, resources, settings, editable, moduleId) {

        var vm = this;
        vm.Items = [];
        vm.AddEditTitle = "";
        vm.EditIndex = -1;
        vm.UserList = JSON.parse(userlist);
        vm.localize = JSON.parse(resources);
        vm.settings = JSON.parse(settings);
        vm.EditMode = (editable.toLowerCase() === "true");
        vm.ModuleId = parseInt(moduleId);
        vm.Item = {};

        vm.getAllItems = getAllItems;
        vm.createUpdateItem = createUpdateItem;
        vm.deleteItem = deleteItem;
        vm.showAdd = showAdd;
        vm.showEdit = showEdit;
        vm.reset = resetItem;
        vm.userSelected = userSelected;
        vm.sortableOptions = { stop: sortStop, disabled: !vm.EditMode };
        
        var jsFileLocation = $('script[src*="Angularmodule/Script/app"]').attr('src');  // the js file path
        jsFileLocation = jsFileLocation.replace('app.js', '');   // the js folder path
        if (jsFileLocation.indexOf('?') > -1) {
            jsFileLocation = jsFileLocation.substr(0, jsFileLocation.indexOf('?'));
        }

        resetItem();
        getAllItems();
 
        function getAllItems() {
            ngProgress.color('red');
            ngProgress.start();
            itemService.getAllItems()
                .success(function(response) {
                    vm.Items = response;
                    ngProgress.complete();
                })
                .error(function(errData) {
                    $log.error('failure loading items', errData);
                    ngProgress.complete();
                });
        };

        function createUpdateItem(form) {
            vm.invalidSubmitAttempt = false;
            if (form.$invalid) {
                vm.invalidSubmitAttempt = true;
                return;
            }

            if (vm.Item.ItemId > 0) {
                itemService.updateItem(vm.Item)
                    .success(function(response) {
                        if (vm.EditIndex >= 0) {
                            vm.Items[vm.EditIndex] = vm.Item;
                        }
                    })
                    .catch(function(errData) {
                        $log.error('failure saving item', errData);
                    });
            } else {
                itemService.newItem(vm.Item)
                    .success(function (response) {
                        if (response.ItemId > 0) {
                            vm.Items.push(response);
                        }
                    })
                    .error(function (errData) {
                        $log.error('failure saving new item', errData);
                    });
            }
            ngDialog.close();
        };

        function deleteItem(item, idx) {
            if (confirm('Are you sure to delete "' + item.Title + '" ?')) {
                itemService.deleteItem(item)
                    .success(function (response) {
                        vm.Items.splice(idx, 1);
                    })
                    .error(function (errData) {
                        $log.error('failure deleting item', errData);
                    });
            }
        };

        function showAdd() {
            vm.reset();
            vm.AddEditTitle = "Add Item";
            ngDialog.open({
                template: jsFileLocation + 'Templates/itemForm.html',
                className: 'ngdialog-theme-default',
                scope: $scope
            });
        };

        function showEdit(item, idx) {
            vm.Item = angular.copy(item);
            vm.EditIndex = idx;
            vm.AddEditTitle = "Edit Item: #" + item.ItemId;
            ngDialog.open({
                template: jsFileLocation + 'Templates/itemForm.html',
                className: 'ngdialog-theme-default',
                scope: $scope
            });
        };

        function resetItem() {
            vm.Item = {
                ItemId: 0,
                ModuleId: vm.ModuleId,
                Title: '',
                Description: '',
                AssignedUserId: ''
            };
        };

        function userSelected() {
            for (var i = 0; i < vm.UserList.length; i++) {
                if (vm.UserList[i].id == vm.Item.AssignedUserId) {
                    vm.Item.AssignedUserName = vm.UserList[i].text;
                }
            }
        }
        function sortStop(e, ui) {

            var sortItems = [];
            for (var index in vm.Items) {
                if (vm.Items[index].ItemId) {
                    var sortItem = { ItemId: vm.Items[index].ItemId, Sort: index };
                    vm.Items[index].Sort = index;
                    sortItems.push(sortItem);
                }
            }       
            itemService.reorderItems(angular.toJson(sortItems))
                .catch(function(errData) {
                    $log.error('failure reordering items', errData.data);
                });
        };
    };
})();

We parse the given Json strings and store them as Javascript objects / arrays in our viewModel and initialize some variables.

Next we define all our methods which are declared later in the code and then call the getAllItems method to retrieve our items via itemService / WebApi from the database:

       function getAllItems() {
            ngProgress.color('red');
            ngProgress.start();
            itemService.getAllItems()
                .success(function(response) {
                    vm.Items = response;
                    ngProgress.complete();
                })
                .error(function(errData) {
                    $log.error('failure loading items', errData);
                    ngProgress.complete();
                });
        };

The progress bar is started in color red and and then the itemService.getAllItems method is called. This returns a promise. If our answer from WebApi is OK (status code = 200), the .success method is called, otherwise the .error one. WebApi and Angular are taking care of serielization and deserialization so that our response is a JSON object or array (depending on what we are returning). In this case we get back an array of item objects and we store it in vm.Items[].

In the failure case we log this error to the console. This is not the best solution because you only see the error when you open the developer tools. Normally you should display this error to the user! A great way to do this is the usage of the ngToast module (see http://tamerayd.in/ngToast/)

As last step in either case we clear the progress bar.

itemService.js

This is where we call the webApi methods of our module:

(function () {
    "use strict";

    angular
        .module("itemApp")
        .factory("itemService", itemService);

    itemService.$inject = ["$http", "serviceRoot"];
    
    function itemService($http, serviceRoot) {

        var urlBase = serviceRoot + "item/";
        var service = {};
        service.getAllItems = getAllItems;
        service.updateItem = updateItem;
        service.newItem = newItem;
        service.deleteItem = deleteItem;
        service.reorderItems = reorderItems;

        function getAllItems() {
            return $http.get(urlBase + "list");
        };
        
        function updateItem(item) {
            return $http.post(urlBase + "edit",item);
        }

        function newItem(item) {
            return $http.post(urlBase + "new", item );
        }
        
        function deleteItem(item) {
            return $http.post(urlBase + "delete", item );
        }
        function reorderItems(sortItems) {
            return $http.post(urlBase + "reorder", sortItems );
        }

        return service;
   }
})();

We inject the $http service from Angular which we had prepared in View.ascx with the special DNN RequestVerificationToken, ModuleId and TabId and call the get or post methods with the corresponding urls. This one is easy to understand!

itemForm.html

The itemForm template defines the html of the edit form and is invoked by the showAdd and showEdit functions of our controller:

function showAdd() {
    vm.reset();
    vm.AddEditTitle = "Add Item";
    ngDialog.open({
        template: '/DesktopModules/Angularmodule/Script/Templates/itemForm.html',
        className: 'ngdialog-theme-default',
        scope: $scope
    });
};

In this case I made usage of the DNN DNN UX Guide to style the form but you can do this however you want:

<div ng-form="dnnItemForm" class="dnnForm ngdialog-message">
    <h3>{{vm.AddEditTitle}}</h3>
    <fieldset>
        <legend></legend>
        <div class="dnnFormItem">
            <label class="dnnLabel" for="txtTitle">{{vm.localize.lblName_Text}}</label>
            <input id="txtTitle" name="txtTitle"
                   ng-model="vm.Item.Title"
                   ng-minlength="5"
                   placeholder="Enter name"
                   class="input"
                   type="text" required />
            <span style="display: inline;" class="dnnFormMessage dnnFormError"
                  ng-show="(dnnItemForm.txtTitle.$dirty || vm.invalidSubmitAttempt) && dnnItemForm.txtTitle.$error.required">
                {{vm.localize.reqTitle_Text}}
            </span>
            <span style="display: inline;" class="dnnFormMessage dnnFormError"
                  ng-show="(dnnItemForm.txtTitle.$dirty || vm.invalidSubmitAttempt) && dnnItemForm.txtTitle.$error.minlength">
                {{vm.localize.minTitle_Text}}
            </span>
        </div>
        <div class="dnnFormItem">
            <label class="dnnLabel" for="description">{{vm.localize.lblDescription_Text}}</label>
            <textarea name="txtDescription" ng-model="vm.Item.Description" type="text" rows="4" title="Description" required />
            <span style="display: inline;" class="dnnFormMessage dnnFormError"
                  ng-show="(dnnItemForm.txtDescription.$dirty || vm.invalidSubmitAttempt) && dnnItemForm.txtDescription.$error.required">
                {{vm.localize.reqDescription_Text}}
            </span>
        </div>
        <div class="dnnFormItem">
            <label class="dnnLabel" for="ddlAssignedUser">{{vm.localize.lblAssignedUser_Text}}</label>
            <select name="ddlAssignedUser"
                    ng-model="vm.Item.AssignedUserId" ,
                    ng-options="u.id as u.text for u in vm.UserList"
                    ng-change="vm.userSelected()" required></select>
            <span style="display: inline;" class="dnnFormMessage dnnFormError"
                  ng-show="(dnnItemForm.ddlAssignedUser.$dirty || vm.invalidSubmitAttempt) && dnnItemForm.ddlAssignedUser.$error.required">
                {{vm.localize.reqAssignedUser_Text}}
            </span>
        </div>
    </fieldset>
</div>
<ul class="dnnActions">
    <button type="button" class="dnnSecondaryAction" ng-click="closeThisDialog(1)">{{vm.localize.CancelEdit_Text}}</button>
    <button type="submit" class="dnnPrimaryAction" ng-click="vm.createUpdateItem(dnnItemForm)">{{vm.localize.SaveEdit_Text}}</button>
</ul>

Get the complete code of the project here: Angularmodule_01.00.00_Source.zip

Now our module is complete and you can run it! Play around and try to understand how everything works together! The completed tasklist Angular module app

In the next part of this blog series I'll show you how we add some bits to our module so that it creates an installable package on every release build!

Back

about.me.

Torsten WeggenMy name is Torsten Weggen and I am CEO of indisoftware GmbH in Hanover, Germany. I'm into DNN since 2008. Before this, I did a lot of desktop stuff mainly coded with Visual Foxpro (see http://www.auktionsbuddy.de). 

I'm programmer, husband, father + born in 1965.

Please feel free to contact me if you have questions.

Latest Posts

DNN module development with AngularJS (Part 6)
12/16/2016 7:00 AM | Torsten Weggen
DNN module development with AngularJS (Part 5)
12/16/2016 6:00 AM | Torsten Weggen
DNN module development with AngularJS (Part 4)
12/16/2016 5:00 AM | Torsten Weggen
DNN module development with AngularJS (Part 3)
12/16/2016 4:00 AM | Torsten Weggen
DNN module development with AngularJS (Part 2)
12/16/2016 3:00 AM | Torsten Weggen
DNN module development with AngularJS (Part 1)
12/15/2016 7:19 AM | Torsten Weggen
Blogging in DNN with Markdown Monster by Rick Strahl
11/27/2016 1:14 PM | Torsten Weggen
Creating a global token engine
11/18/2016 10:25 AM | Torsten Weggen
DnnImagehandler - Hot or not ?
2/21/2015 11:52 PM | Torsten Weggen
Rapid Module Development Part 2 - The multilanguage thing…
4/7/2014 7:32 PM | Torsten Weggen

My Twitter

Torsten Weggen 7/22/2017

RT @CBPSC: Ventrian modules now free on Github! https://t.co/aKLeZawVHN #DNNCMS https://t.co/x6mhie382I

Torsten Weggen 5/24/2017

RT @WaldkauzFolk: ++ Waldkauz goes W:O:A 2017 ++ Der Wahnsinn - Wir sind in diesem Jahr beim großartigen Wacken Open Air mit... https://t.…

Torsten Weggen 4/25/2017

RT @WaldkauzFolk: "A magical pagan folk release getting a lot of inspiration of myth, legends and tales from countries all over... https://…