7

I'm trying to integrate asp.net mvc with an angular 2 application. I understand that this is not ideal, but I am being asked to integrate some existing Mvc functionality (think big legacy app) into a brand new Angular 2 spa.

What I would like to be able to do is have a cshtml view that has angular components in it, as well as pure mvc stuff...

<side-bar></side-bar>
<action-bar></action-bar>

@{
    Html.RenderPartial("_SuperLegacyPartialView");   
}

I'm struggling to find any way to do this. This blog post looked promising - http://www.centare.com/tutorial-angular2-mvc-6-asp-net-5/. It used a templateUrl value that pointed to a path rendered by Mvc, as well as AsyncRoute, but none of that works anymore in Angular 2. This post looked promising as well - http://gbataille.github.io/2016/02/16/Angular2-Webpack-AsyncRoute.html, but it uses AsyncRoute too, which is deprecated.

This used to be very easy in Angular 1. We used to either manually bootstrap angular into a Razor View, or render a partial view as the templateUrl of a component/directive. What is the best way to do this in the latest Angular 2 that uses Webpack?

Tim Hardy
  • 1,654
  • 1
  • 17
  • 36

5 Answers5

12

I came up with a solution that satisfied my needs at the time. I'm using angular-cli with WebPack, and this worked for my needs. I don't understand all the examples I've seen that say to use "templateUrl: '/Template/Index'", where the path is a path to an MVC view. That simply doesn't work because the path can't be found inside any of the bundled views that WebPack creates. Maybe those people aren't using angular-cli and WebPack.

This stackoverflow answer - How can I use/create dynamic template to compile dynamic Component with Angular 2.0? was very helpful in creating the following directive. This directive will take the output of an mvc partial view and compile it. It allows for Razor/server logic to take place, and some angular to be compiled as well. Although, actually including other components inside this MVC partial was problematic. If you get that working, please let me know what you did. In my case, I just needed the server rendering to happen and to place that exactly where I wanted it in my Angular 2 spa.

MvcPartialDirective

import {
  Component,
  Directive,
  NgModule,
  Input,
  ViewContainerRef,
  Compiler,
  ComponentFactory,
  ModuleWithComponentFactories,
  ComponentRef,
  ReflectiveInjector, OnInit, OnDestroy
} from '@angular/core';

import { RouterModule }  from '@angular/router';
import { CommonModule } from '@angular/common';
import {Http} from "@angular/http";
import 'rxjs/add/operator/map';

export function createComponentFactory(compiler: Compiler, metadata: Component): Promise<ComponentFactory<any>> {
  const cmpClass = class DynamicComponent {};
  const decoratedCmp = Component(metadata)(cmpClass);

  @NgModule({ imports: [CommonModule, RouterModule], declarations: [decoratedCmp] })
  class DynamicHtmlModule { }

  return compiler.compileModuleAndAllComponentsAsync(DynamicHtmlModule)
    .then((moduleWithComponentFactory: ModuleWithComponentFactories<any>) => {
      return moduleWithComponentFactory.componentFactories.find(x => x.componentType === decoratedCmp);
    });
}

@Directive({ selector: 'mvc-partial' })
export class MvcPartialDirective implements OnInit, OnDestroy {
  html: string = '<p></p>';
  @Input() url: string;
  cmpRef: ComponentRef<any>;

  constructor(private vcRef: ViewContainerRef, private compiler: Compiler, private http: Http) { }

  ngOnInit() {
    this.http.get(this.url)
      .map(res => res.text())
      .subscribe(
        (html) => {
          this.html = html;
          if (!html) return;

          if(this.cmpRef) {
            this.cmpRef.destroy();
          }

          const compMetadata = new Component({
            selector: 'dynamic-html',
            template: this.html,
          });

          createComponentFactory(this.compiler, compMetadata)
            .then(factory => {
              const injector = ReflectiveInjector.fromResolvedProviders([], this.vcRef.parentInjector);
              this.cmpRef = this.vcRef.createComponent(factory, 0, injector, []);
            });
        },
        err => console.log(err),
        () => console.log('MvcPartial complete')
      );

  }

  ngOnDestroy() {
    if(this.cmpRef) {
      this.cmpRef.destroy();
    }
  }
}

in some-component.html (assuming your mvc app shares the domain with your spa)

<mvc-partial [url]="'/stuffs/mvcstuff'"></mvc-partial>

MvcStuff.cshtml

@{
    ViewBag.Title = "This is some MVC stuff!!!";
}
<div>
    <h2>MVC Stuff:</h2>
    <h4>@ViewBag.Title</h4>
    <h2>Angular Stuff:</h2>
    <h4>{{1 + 1}}</h4>
</div>

in StuffsController.cs

public PartialViewResult MvcStuff() => PartialView();
Community
  • 1
  • 1
Tim Hardy
  • 1,654
  • 1
  • 17
  • 36
  • This is the right answer. Works like a charm. I think sometime it makes sense to pre-render Angular templates on the server, so you can easily render the UserName or do other user-specific business logic there(permissions etc). – Skorunka František Apr 04 '17 at 10:27
  • This is the only way to achieve it ? – k11k2 Aug 21 '17 at 10:20
  • getting `'undefined' is not assignable to type 'ComponentFactory'` error – k11k2 Aug 21 '17 at 13:57
  • I haven't touched this code in a while. My guess is that a change in angular might be causing this. If I get time, I will try to reproduce it. – Tim Hardy Aug 22 '17 at 21:37
  • I just saw your other question concerning this. It sounded like TypeScript was complaining that createComponentFactory could possibly return undefined. Was the solution to change the return type of createComponentFactory to Promise> | Promise? – Tim Hardy Aug 22 '17 at 21:47
  • @TimHardy yes ! `Promise | undefined>` and `this.cmpRef = this.vcRef.createComponent(factory!, 0, injector, []);` does the trick ` **!** ` is non-null assertion operator which assures it is not null or undefined. – k11k2 Aug 23 '17 at 06:22
  • @TimHardy did you try any button action in cshtml and call it function in angular? – k11k2 Aug 23 '17 at 13:46
  • @TimHardy have a look [LINK](https://stackoverflow.com/questions/45856730/dynamic-component-click-event-binding-angular-2) – k11k2 Aug 24 '17 at 16:11
  • I have not tried a button action from the cshtml. Most of what I've used it for is generate pure MVC content that I can consume in Angular. I don't do much Angular in the cshtml. – Tim Hardy Aug 24 '17 at 19:32
  • Angular 5 users: In MvcPartialDirective, replace import {Http}... with import { HttpModule } from '@angular/http'; Also make the same change in app.module.ts to get the piece of code working. – taylorswiftfan Nov 14 '18 at 01:13
1

I did it like this.

@Component({
    templateUrl: '/Template/Index'
})
export class TemplateComponent {}

"/Template/Index" is the URL in your MVC Controller, and then the method.

public IActionResult Index()
  {
    return PartialView();
  }

My problem is i don't know how the refresh the view to call controller method every time is loaded.

Federico
  • 21
  • 3
1

For those who are on Angular 7, you will need to change the accepted answer a little bit to make it work.

In MvcPartialDirective:

Update Http to HttpClient so that it reads:

import { HttpClient } from '@angular/common/http';

In ngOnInit(), specify the responseType:

this.http .get(this.url, {responseType: "text"})...

Update to pipe:

.pipe(map(res => res.toString())) (note toString() insteadd of .text())

Optionally is to use app prefix to directive specification:

@Directive({ selector: 'appActionResult' })

End result:

import {
  Component,
  Directive,
  NgModule,
  Input,
  ViewContainerRef,
  Compiler,
  ComponentFactory,
  ModuleWithComponentFactories,
  ComponentRef,
  ReflectiveInjector, OnInit, OnDestroy
} from '@angular/core';

import { RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';

export function createComponentFactory(compiler: Compiler, metadata: Component): Promise<ComponentFactory<any>> {
  const cmpClass = class DynamicComponent { };
  const decoratedCmp = Component(metadata)(cmpClass);

  @NgModule({ 
    imports: [CommonModule, RouterModule], 
    declarations: [decoratedCmp],
    schemas: [NO_ERRORS_SCHEMA] })
  class DynamicHtmlModule { }

  return compiler.compileModuleAndAllComponentsAsync(DynamicHtmlModule)
    .then((moduleWithComponentFactory: ModuleWithComponentFactories<any>) => {
      return moduleWithComponentFactory.componentFactories.find(x => x.componentType === decoratedCmp);
    });
}

@Directive({
  selector: 'appActionResult'
})
export class ActionResultDirective implements OnInit, OnDestroy {
  html = '<p></p>';
  @Input() url: string;
  cmpRef: ComponentRef<any>;

  constructor(private vcRef: ViewContainerRef, private compiler: Compiler, private http: HttpClient) {}

  ngOnInit() {
    this.http
      .get(this.url, {responseType: "text"})
      .pipe(map(res => res.toString()))
      .subscribe(
        (html) => {
          this.html = html;
          if (!html) { return; }

          if (this.cmpRef) {
            this.cmpRef.destroy();
          }

          const compMetadata = new Component({
            selector: 'dynamic-html',
            template: this.html,
          });

          createComponentFactory(this.compiler, compMetadata)
            .then(factory => {
              const injector = ReflectiveInjector.fromResolvedProviders([], this.vcRef.parentInjector);
              this.cmpRef = this.vcRef.createComponent(factory, 0, injector, []);
            });
        },

        err => console.log(err),
        () => console.log('MvcPartial complete')
      );

  }

  ngOnDestroy() {
    if (this.cmpRef) {
      this.cmpRef.destroy();
    }
  }
}
taylorswiftfan
  • 1,371
  • 21
  • 35
0

I needed to use MVC PartialView html in my angular 4 application, called by the HttpClient .get method.

I used AMD's post

to convert my partial view to an html string. I returned this in a container json object, and set it into a variable that set the html of a div on my page..thus:

    ...in the template 
       <div  class="files"  [innerHtml]="myTemplate">
       </div>

... in the component .ts file
      export interface htmldata {
          html: string; 
      }


... inside component

   getDivHtml(path: string): Promise<htmldata> {
            return this.http
                .get<htmldata>(`${this.baseUrl}/MVC/Index?path=` + path , { withCredentials: true })
                .toPromise();
   }

   ngOnInit() { 
       this.getDivHtml('').then(
           data => { this.loadData(data); },
       ).catch( error => { console.log(error);  }); 
   }

   loadData(data: htmldata) {
      this.myTemplate = data.html;
   }

...on server

  public class HtmlReturn
  {
      public string html { get; set; }
  }

  [Produces("application/json")]
  [Route("api/MVC/[action]")]
  public class MVCController : Controller
  {

      private readonly ViewRender view; 

      public MVCController(ViewRender view)
      {           
           this.view = view;
      }

      public IActionResult Index(string path)
      {
           data.html = this.view.Render("viewpath", viewModel);
           return Json(data);
      }
}

Please note: this only works well with static html that doesn't need event listeners. I was not able to add click events to the loaded html with renderer2, although I am not an expert and it may be possible.

You will need to create the ViewRender class and add an injection instruction into the startup.cs file as shown in AMDs post

davaus
  • 1,145
  • 13
  • 16
-1

Using Systemjs: https://github.com/VahidN/MVC5Angular2 Using Webpack: http://blog.stevensanderson.com/2016/10/04/angular2-template-for-visual-studio/

chandan7
  • 83
  • 3
  • 10
  • Thanks for responding, but neither of the above tutorials addresses my question, and that's why it has been so hard finding an answer. Like every other tutorial on the web, those just describe how to setup an Angular spa inside of MVC. They don't address how to render MVC partials and angular components on the same page. – Tim Hardy Jan 05 '17 at 17:02