iLemming
iLemming

Reputation: 36214

fairytale about mvc, require.js and angular. is there happily ever after?

So. Once upon a time there were four magical creatures: asp.net mvc, require.js and angular. And one wise wizard decided to put them in the same house, and let for every single view of asp.net to have its own "code-behind" javascript file;

first he added to the _Layout.cshtml

 <script  data-main="/main" src="~/Scripts/require.js"></script>

and then he created main.js in the root:

require.config({
    baseUrl: "/Scripts/",
    paths: {
        'jquery': 'jquery-1.9.1.min',
        'jquery-ui': 'jquery-ui-1.10.2.custom.min',
        'angular': 'angular.min',
        'ng-grid': 'ng-grid-2.0.2.debug'
    },
    shim: {
        'jquery': { exports: "$" },
        'underscore': { exports: "_" },
        'jquery-ui': ['jquery'],
    },
});
 // Standard Libs
require(['jquery','jquery-ui','underscore','angular']);

nothing fancy and magical yet. But then he created an html helper as such:

public static MvcHtmlString RequireJs(this HtmlHelper helper)
{
    var controllerName = helper.ViewContext.RouteData.Values["Controller"].ToString(); // get the controllername 
    var viewName = Regex.Match((helper.ViewContext.View as RazorView).ViewPath, @"(?<=" + controllerName + @"\/)(.*)(?=\.cshtml)").Value; //get the ViewName - extract it from ViewPath by running regex - everything between controllerName +slash+.cshtml should be it;

// chek if file exists
    var filename = helper.ViewContext.RequestContext.HttpContext.Request.MapPath("/Scripts/views/" + controllerName.ToLower() + "-" +
                                                                  viewName.ToLower()+".js");
    if (File.Exists(filename))
    {
        return helper.RequireJs(@"views/" + controllerName.ToLower() + "-" + viewName.ToLower());   
    }
    return new MvcHtmlString("");
}

public static MvcHtmlString RequireJs(this HtmlHelper helper, string module)
{
    var require = new StringBuilder();
    require.AppendLine(" <script type=\"text/javascript\">");
    require.AppendLine("    require(['Scripts/ngcommon'], function() {");
    require.AppendLine("        require( [ \"" + module + "\"] );");
    require.AppendLine("    });");
    require.AppendLine(" </script>");

    return new MvcHtmlString(require.ToString());
}

and then he could use it in _Layout.cshtml just like that:

   @Html.RequireJs()

and if you were listening carefully to the story, you probably noticed that there was also Scripts/ngcommon.js file to manually bootstrap angular.js and have commonly used angular directives and services

require(['angular', 'jquery'], function() {
    angular.module("common",[]).directive('blabla', function() {
        return {
            restrict: 'A',
            scope: { value: "@blabla" },
            link: function(scope, element, attrs) {     }
        }
    });

    //manually bootstrap it to html body
    $(function(){
        angular.bootstrap(document.getElementsByTagName('body'), ["common"]);
    });
});

And here comes the magic: from now on if it was a javascript file in \Scripts\views named as controllerName-viewName.js as home-index.js for Home\Index.cshtml it would be automagically picked up by require.js and loaded. Beautiful isn't it?

But then the magician thought: What If I need to load something else (like ng-grid) and that something should not be injected into common angular module because not all the pages will be using it. Of course he could always manually bootstrap another module into a page element in each code-behind javascript where he needed, but he's not wise enough to find answer to the question: Is it possible to inject some angular.js component (like ng-grid) directly into a controller, without having it as a part of the app module?

Upvotes: 7

Views: 1345

Answers (2)

valerysntx
valerysntx

Reputation: 526

DI is the magic key for having separate angular codebehind in MVC views. You don't even need the requirejs at all, because angular is a dependency injector and module loader by nature, angular.bootstrap is the magic place to start.

So let wizard became more powerfull with the spell - $inject.

  var TmplController = function($scope, $compile, $http... // any module itself
    {
      this.parts = ['legs','arms','head'];
      $scope.dynamicPageTemplate = function($compile)
      {
        $compile('<div><p ng-repeat="each in parts">{{each}}</p></div>' )( $scope );
      }
    }

  TmplController.$inject = ['$scope','$comple', '$http']; //try legs or head

refer complete annotated source of angular-scenario.js from https://github.com/angular/bower-angular-scenario, and you will find how to inject code with define manner helpers.

Upvotes: 0

Dmytro Evseev
Dmytro Evseev

Reputation: 11581

If I understand magician's idea right, then it is possible to go on by splitting your application into sub-modules being defined as a collection of components.

It will work if he sets up dependencies for main myApp module like:

var myApp = angular.module('myApp', ['Constants', 'Filters', 'Services', 'Directives', 'Controllers']);
myApp.Constants = angular.module('Constants', []);
myApp.Controllers = angular.module('Controllers', []);
myApp.Filters = angular.module('Filters', []);
myApp.Services = angular.module('Services', []);
myApp.Directives = angular.module('Directives', []);

Then each of sub-modules: Services etc. - can be extended with single component, like:

myApp.Controllers.controller('MyController', function () {});
myApp.Services.factory('myService', function () {});
myApp.Directives.directive('myDirective', function () {});
myApp.Filters.filter('myFilter', []);
myApp.Constants.constant('myConstant', []);

That way main application module is loaded with several sub-modules, but each structure is not important. It makes possible to include individual controllers, services, directives and filters on each page served from back-end - magician just needs to be sure that all needed dependencies are loaded.

Upvotes: 1

Related Questions