Short version
This Plunker defines a <view>
component which can render an arbitrary model+template. This needs to be changed to replace the previously rendered contents rather than appending new peers.
EDIT: This is working now, thanks to the response by user3636086.
One problem still remains: unlike Angular 1, Angular 2 forces me to create a nested component to update a template (since templates are effectively a static property of a component's class), so I have a bunch of unnecessary DOM nodes being added.
Long Version
Angular 1
In our project we'd prefer most of our code to have no direct dependency on a UI framework. We have a viewmodel class which ties together a model and 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 viewInstance
changes, a new View object is rendered (model + template) at that location in the DOM. For instance, this Dashboard view can have an arbitrary list of views that it can render:
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'>"; }
}
A crucial point is that this is composable. The <view>
can contain a <view>
which can contain a <view>
, 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 at a DOM location is very clunky, forcing me to create a new component type every time.
This is the best I've come 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;
});
}
}
}
Used something like this:
@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 problems that have now been fixed.
The remaining problem is that it creates a bunch of unnecessary nested DOM elements. What I really want is:
<view>VIEW CONTENTS RENDERED HERE</view>
Instead we have:
<view>
<span></spawn>
<viewrenderer>VIEW CONTENTS RENDERED HERE</viewrenderer>
</view>
This gets worse the more views we have nested, without half the lines here being extraneous crap:
<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>