angularangularjsporting

How do I port a non-SPA AngularJS app to Angular, one component per page?


I have a fairly large legacy ASP.Net MVC app that uses different AngularJS controllers on each of about 20 different pages. Many modules, services, and directives are shared, but this application has never used AngularJS's routing; each page is effectively its own app, with its own ng-app tag that loads when the .NET MVC view renders. Each page is very dynamic in the normal AngularJS way, but routing between pages is handled exclusively by MVC.

I'm very experienced in AngularJS but new to Angular 2+, having only done the Tour of Heroes tutorial and a few other samples. I assumed I would be able to do something very similar with Angular--basically rewriting each controller as a component and developing everything inside one Angular application, building it with the CLI, linking the appropriate dist/ scripts into each MVC view, but only using one component on each MVC view.

For example, I was expecting to put this onto the Billing MVC view:

<app-root>
   <app-billing-component></app-billing-component>
</app-root>

and this onto the Plan view

<app-root>
   <app-plan-component></app-plan-component>
</app-root>

But when I tried a proof of concept, the build process gave me only the one very basic index.html page with the empty <app-root> element on it, because everything including all the HTML templating gets pushed into the main.js file. Trying to reference the individual components in that file (or any other file, like my MVC views) doesn't work because main.js is getting compiled "all or nothing" as if this were an SPA and no other HTML page/URL would ever be loaded into the browser as part of this app. I can't figure out how I would handle putting one component into each view without developing them in separate "apps" and building a different main.js file for each one.

I can't imagine my need is unique, but I haven't been able to dig up an example of someone trying to do the same thing. "This is not the normal way to do Angular" goes without saying; my question is: how can I do something like this? Is there a way to tweak the build? Do I have to find a way to link specific files into my MVC views without using ng build?


Solution

  • Reading this 9 months later, it was hard to put myself back into the mindset I had back then, and even understand my own question. The solution turned out to be: get into the Angular mindset, and find a way to make it work within a .NET MVC application, by rendering only the one component specified by the route in the URL.

    Details:

    1. Architect the Angular portion of the application normally, with all components built into one application, and every .NET page load activating that same application.
    2. Use Angular Routing to determine which component gets activated, based on the URL path, even though we're not using Angular Routing to route between components.
    3. To facilitate this, I added a new .NET Controller that simply reads the entirety of the index.html file that Angular builds, including the main.[hash].js, polyfills.[hash].js, runtime.[hash].js, and styles.[hash].js files that the compiler generates in both the development and production build processes. Without this step, I would have had to either not use the hashed filenames, or update the link paths every time new filenames got created. This is the entirety of the code of that controller:
        public class NgController : Controller
        {
            public ActionResult Index()
            {
                string relativePath = "~/dist/index.html";
                string absolutePath = Server.MapPath(relativePath);
    
                if (System.IO.File.Exists(absolutePath))
                {
                    string fileText = System.IO.File.ReadAllText(absolutePath);
                    return Content(fileText, "text/html");
                }
                else
                {
                    return HttpNotFound();
                }
            }
        }
    
    1. I integrate the Angular tightly into the "directory" structure of the MVC app by giving each Angular route an empty Action in the MVC application. This Action is simply a placeholder that actually hands off control to the NgController. In this way, if I want my Billing component to appear at /Utilities/Billing, and I already have other Actions on my .NET UtilitiesController, I just add the following to that controller:
            [NgLayoutExecute]
            public ActionResult Billing()
            {
                throw new NotImplementedException("This action should be intercepted by the NgLayoutExecute filter attribute and this code should not be reached.");
            }
    
    1. The NgLayoutExecute attribute hands off control to NgController, which in turn simply returns the contents of Angular's index.html file:
        public class NgLayoutExecuteAttribute : ActionFilterAttribute
        {
            public override void OnActionExecuting(ActionExecutingContext filterContext)
            {
                var ngApplicationRootController = new NgController();
    
                ngApplicationRootController.ControllerContext = filterContext.Controller.ControllerContext;
    
                // return the result of the Index action method of the NgController -- this is basically the index.html file
                var result = ngApplicationRootController.Index();
    
                // Set the result to be rendered
                filterContext.Result = result;
    
                base.OnActionExecuting(filterContext);
            }
        }
    
    1. I added rewrite rules to my web.config to invisibly pull the built main.js, runtime.js, etc. files from the dist/ directory, again so I don't have to edit index.html or do any other changes to the Angular application as the compiler outputs it:
            <rule name="AngularFiles" stopProcessing="true">
              <match url="^(runtime|polyfills|vendor|main)(\.[a-f0-9]{10,20})?(.js)(.map)?$" />
              <action type="Rewrite" url="dist/{R:0}" logRewrittenUrl="true" />
            </rule>
            <rule name="AngularStyles" stopProcessing="true">
              <match url="^styles(\.[a-f0-9]{10,20})?(.css)$" />
              <action type="Rewrite" url="dist/{R:0}" logRewrittenUrl="true" />
            </rule>
    

    Kind of a lot to it, but it integrates really well, and the only way the users can tell the Angular components aren't developed with 100% the same technology as the standard MVC pages is because there are a few small style differences in the CSS files.