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>