Dynamically bind model and template to DOM node in Angular 2

Short version

This Plunker defines a <view> component that can display an arbitrary template + template. This needs to be changed to replace previously processed content, rather than adding new peers.

EDIT: this now works thanks to the answer from user3636086.

One problem remains: unlike Angular 1, Angular 2 forces me to create a nested component to update the template (since templates are actually a static property of the component class), so I have a bunch of unnecessary DOM nodes added.


Long version

Angular 1

In our project, we would prefer that most of our code does not directly depend on the structure of the user interface. We have a viewmodel class that binds a model and a view. Here are simplified examples:

 interface IView { template: string; } class SalesView implements IView { sales: number = 100; get template() { return "<p>Current sales: {{model.sales}} widgets.<p>"; } } class CalendarView implements IView { eventName: string = "Christmas Party"; get template() { return "<p>Next event: {{model.eventName}}.<p>"; } } class CompositeView implements IView { calendarView = new CalendarView(); salesView = new SalesView(); get template() { return `<div view='model.salesView'></div> <div view='model.calendarView'></div>`; } } 

We have a view directive that can display one of these views:

 <div view='viewInstance'></div> 

If the viewInstance changes, a new View object is rendered (model + template) at this point in the DOM. For example, this Dashboard view may have an arbitrary list of views that it can display:

 class Dashboard implements IView { views: Array<IView> = [ new SalesView(), new CalendarView(), new CompositiveView() ]; activeView: View; get template() { return "<h1>Dashboard</h1> <div view='model.activeView'>"; } } 

The most important point is that it is compositional. <view> may contain <view> , which may contain <view> , and so on and so forth.

In Angular 1, our view directive looks something like this:

 .directive("View", [ "$compile", ($compile: ng.ICompileService) => { return <ng.IDirective> { restrict: "A", scope: { model: "=View" }, link(scope: ng.IScope, e: ng.IAugmentedJQuery, atts: ng.IAttributes): void { scope.$watch((scope: any) => scope.model, (newValue: any) => { e.html(newValue.template); $compile(e.contents())(scope.$new()); }); } }; } ]); 

Angular 2

I'm trying to port this to Angular 2, but dynamically loading a new template in the DOM location is very awkward, forcing me to create a new component type each time.

This is the best I came up with (updated with feedback from user3636086):

 @Component({ selector: 'view', template: '<span #attach></span>', }) export class MyView { @Input() model: IView; previousComponent: ComponentRef; constructor(private loader: DynamicComponentLoader, private element: ElementRef) { } onChanges(changes: {[key: string]: SimpleChange}) { var modelChanges = changes['model'] if (modelChanges) { var model = modelChanges.currentValue; @Component({ selector: 'viewRenderer', template: model.template, }) class ViewRenderer { model: any; } if (this.previousComponent) { this.previousComponent.dispose(); } this.loader.loadIntoLocation(ViewRenderer, this.element, 'attach') .then(component => { component.instance.model = model; this.previousComponent = component; }); } } } 

Something like this is used:

 @Component({ selector: 'app', template: ` <view [model]='currentView'></view> <button (click)='changeView()'>Change View</button> `, directives: [MyView] }) export class App { currentView: IView = new SalesView(); changeView() { this.currentView = new CalendarView(); } } 

EDIT: This had issues that were fixed.

The rest of the problem is that it creates a bunch of unnecessary nested DOM elements. I really want:

 <view>VIEW CONTENTS RENDERED HERE</view> 

Instead, we have:

 <view> <span></spawn> <viewrenderer>VIEW CONTENTS RENDERED HERE</viewrenderer> </view> 

This gets worse the more species we put in, without half the lines that are extraneous horseradish:

 <view> <span></spawn> <viewrenderer> <h1>CONTENT</h1> <view> <span></spawn> <viewrenderer> <h1>NESTED CONTENT</h1> <view> <span></spawn> <viewrenderer> <h1>NESTED NESTED CONTENT</h1> </viewrenderer> </view> </viewrenderer> </view> </viewrenderer> <viewrenderer> <h1>MORE CONTENT</h1> <view> <span></spawn> <viewrenderer> <h1>CONTENT</h1> </viewrenderer> </view> </viewrenderer> </view> 
+7
angular angular2-template angular2-directives
source share
2 answers

Short version

see https://github.com/angular/angular/issues/2753 (recent comments, not the original problem)


Long version

I have a similar use case and have been following chatter about recommended approaches to it.

At the moment, DynamicComponentLoader is indeed a de facto tool for dynamically compiling components (read: stand-in for $compile ), and the approach you used in your example is essentially identical to the one that @RobWormald posted in response to several similar questions on gitter .

Here 's another interesting example @EricMartinez gave me using a very similar approach.

But yes, this approach is also inconvenient for me, and I have yet to find (or come up with) a more elegant way to do this with DCL. The comments on the github issue above contain a third example, along with similar critics that have still remained unanswered.

It’s hard for me to believe that a canonical solution for a use case as widespread as this would be so awkward in the final release (especially considering the relative elegance of $compile ), but all that came next would be an assumption.

If you use grep "DCL" or "DynamicComponentLoader" in the gitter stream, there are some interesting conversations on this topic. One of the team’s main guys said something that β€œDCL is a powerful tool that we only expect will be used by people who are really involved in the wireframe,” which I found ... interesting.

(I would quote / link this directly if the gitter search didn't suck)

+3
source share

The correct behavior can be achieved with minor changes in your code: you must β€œdelete” a previously created component before adding a new one.

 savedComp: Component = null; ... if (this.savedComp) { this.savedComp.dispose(); } this.loader.loadIntoLocation(DynamicComponent, this.element, 'attach') then((res) => {res.instance.model = model; this.savedComp = res;}); 

Full solution here: http://plnkr.co/edit/KQM31HTubn0LfL7jSP5l

Hope this helps!

+2
source share

All Articles