Drag and drop bootstrap tabs in ASP.NET Core 1.0 with AngularJS

This article shows how to create a bootstrap tab menu with data from an MVC 6 service in an ASP.NET Core 1.0 application using Angular. The tabs can be dragged and dropped so that the position or tab order can be changed by the user. The drag and drop list is implemented using angular-drag-and-drop-lists from Marcel Juenemann.

Code: https://github.com/damienbod/AspNet5DragAndDrop

2016.07.01: Updated to ASP.NET Core 1.0 RTM
2016.05.19: Updated to ASP.NET Core 1.0 RC2 dotnet
2015.11.18: Updated to ASP.NET Core 1.0 RC1
2015.10.20: Updated to ASP.NET Core 1.0 beta 8
2015.09.20: Updated to ASP.NET Core 1.0 beta 7

Project setup

The project is a standard MVC 6 solution with no security. The front end dependencies are added to the project using bower. In the dependencies property of the bower.json file, angular-drag-and-drop-lists version 1.2.0, angular, ui-router and bootstrap, are added. The bootstrap export is overridden to match the standard bootstrap deployment and change other bower configurations.

{
    "name": "bower",
    "license": "Apache-2.0",
    "private": true,
    "dependencies": {
        "angular": "1.3.15",
        "ui-router": "0.2.14",
        "bootstrap": "3.3.4",
        "angular-animate": "1.3.15",
        "jquery": "2.1.4",
        "angular-drag-and-drop-lists": "1.2.0"
    },
    "exportsOverride": {
        "bootstrap": {
            "js":  "dist/js/*.*",
            "css":  "dist/css/*.*",
            "fonts":  "dist/fonts/*.*"
        },
        "angular": {
            "": "angular{.js,.min.js,.min.js.map, .min.js.gzip}"
        },
        "angular-animate": {
            "": "angular-animate{.js,.min.js,.min.js.map}"
        },
        "ui-router": {
            "": "release/angular-ui-router.{js,min.js,min.js.map}"
        },
        "jquery": {
            "": "dist/*.{js,min.js,min.map}"
        }
    }
}

The bower configuration adds the third party libraries to the lib folder inside the wwwroot folder when the grunt bower task is executed. If required, this can be changed inside the grunt file in the bower task. The scripts and the css files are added to the index.html file in the wwwroot folder:

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="utf-8">
    <title>AngularJS DropAndDrop</title>
    <link href="lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
    <link href="myapp.css" rel="stylesheet" />
</head>
<body>

    <div ng-app="myapp">
        <div ui-view></div>
    </div>

    <script src="lib/jquery/jquery.min.js"></script>
    <script src="lib/bootstrap/js/bootstrap.min.js"></script>
    <script src="lib/angular/angular.js"></script>
    <script src="lib/ui-router/angular-ui-router.min.js"></script>
    <script src="lib/angular-drag-and-drop-lists/angular-drag-and-drop-lists.js"></script>
    <script src="app.js"></script>

</body>
</html>

The application uses Angular ui-router. The index.html is a layout with a home template. The Html uses the ui-view directive to define this. The ui states are defined in the app.js file which are used for the ui routing:

(function () {
    var mainApp = angular.module("myapp", ["ui.router", "dndLists"]);

    mainApp.config(["$stateProvider", "$urlRouterProvider",
		function ($stateProvider, $urlRouterProvider) {
		    $urlRouterProvider.otherwise("/home");

		    $stateProvider
                .state("home", {
                    url: "/home",
                    templateUrl: "/templates/home.html",
                    controller: "homeController",
                    resolve: {

                        WorkflowService: "WorkflowService",

                        workflows: ["WorkflowService", function (WorkflowService) {
                            return WorkflowService.getWorkflows();
                        }]
                    }
                })
                 
		}
    ]
    );
})();

The home state resolves the server data from the MVC 6 controller in the resolve method. This returns a promise and the template loads when the Http request is completed. The Angular service is implemented using $http. The service calls the 2 MVC controller methods and returns the result as a promise.

(function () {
	'use strict';

	function WorkflowService($http, $log) {

		$log.info("WorkflowService called");

		var getWorkflows = function () {
		    $log.info("WorkflowService getWorkflows called");
		    return $http.get("/api/Workflow")
			.then(function (response) {
				return response.data;
			});
		}

		var getSection = function (sectionId) {
		    $log.info("WorkflowService getAnimal called: " + sectionId);
		    $log.info(sectionId);
			return $http.get("/api/Workflow/" + sectionId.sectionId)
			.then(function (response) {
				return response.data;
			});
		}

		return {
		    getWorkflows: getWorkflows,
		    getSection: getSection
		}
	}

	var module = angular.module('myapp');

	module.factory("WorkflowService",
		[
			"$http",
			"$log",
			WorkflowService
		]
	);

})();

The MVC 6 controller returns a list of Section entities which are used for the tabs display.

using System;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using AspNet5DropAndDrop.Model;

namespace Controllers
{
	[Route("api/[controller]")]
	public class WorkflowController : Controller
	{
        private List<Section> _workflow = new List<Section>
        {
                new Section
                {
                    Id = 1,
                    Description = "This is a custom task.",
                    Name = "Custom",
                },
                new Section
                {
                    Id = 2,
                    Description = "This task is used to execute a grunt cmd line.",
                    Name = "Cmd Grunt Task",
                },
                new Section
                {
                    Id = 3,
                    Description = "A bower task can be executed on the build server using this task.",
                    Name = "Cmd Bower Task",
                },
                new Section
                {
                    Id = 4,
                    Description = "A npm task can be executed used this task.",
                    Name = "Cmd Npm Task",
                },
                new Section
                {
                    Id = 5,
                    Description = "A custom workflow task can be excuted using this task",
                    Name = "Workflow Spec",
                },
                new Section
                {
                    Id = 6,
                    Description = "Visual Studio build 2015",
                    Name = "taskVSBuild2015",
                },

        };

        [HttpGet]
		[Route("")]
		public IEnumerable<Section> Get()
		{
            return _workflow;
        }

		[HttpGet]
		[Route("{id}")]
		public Section Get(long id)
		{
            return _workflow.Find(t => t.Id == id);
		}
	}
}

Once the back end data is loaded, it is used in the Angular controller defined for the ui-router state:

(function () {
    'use strict';

    var module = angular.module('myapp');
   
    function homeController($scope, $log, workflows) {
        $log.info("homeController called");
        $scope.message = "home Controller";

        $scope.models = {
            lists: { "flow": workflows },
            selected: workflows[0]
        };
    }

    module.controller('homeController', ['$scope', '$log', 'workflows', homeController]);

})();

The scope of the controller, adds the workflows data to its models which is then used by the home template view. The home template displays the items using the dnd-list directive from the dndLists Angular module defined in the angular-drag-and-drop-lists script. This then uses the bootstrap nav-tabs and active class to display the items.


<div class="dragdroptabs row">
    <div class="container">
        <div class="header row">
            <div ng-repeat="(listName, list) in models.lists" class="col-md-12">

                <ul dnd-list="list" class="nav nav-tabs">
                    <li ng-repeat="item in list"
                        dnd-draggable="item"
                        dnd-moved="list.splice($index, 1)"
                        dnd-effect-allowed="move"
                        dnd-selected="models.selected = item"
                        ng-class="{'active':  models.selected === item}">
                        <a href="#">{{item.Name}}</a>

                    </li>
                </ul>
            </div>
        </div>

        <div class="contentpage">{{models.selected.Description}}</div>
    </div>

</div>

The tabs can now be dragged and dropped and also display the correct content depending on the selected tab.

AngularJsDropAndDrop_01

AngularJsDropAndDrop_02

The list can then be saved back to the server to persist the order of the tabs. The content could also be loaded using child states of the wizard, or further drag and drop menus could be loaded.

Conclusion

The angular-drag-and-drop-lists library works great and good documentation is provided on the gitHub page. More complex features are demonstrated as well as the simple hello world examples. The library works well together with bootstrap. When using this together with Angular ui-router, interesting and complex UI wizards with drag and drop functionality can be created relatively easily.

Links:

https://github.com/marceljuenemann/angular-drag-and-drop-lists

2 comments

  1. […] Drag and drop bootstrap tabs in ASP.NET 5 with Angular – damienbod […]

  2. Reblogged this on SutoCom Solutions.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: