且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

将模型和模板动态绑定到 Angular 2 中的 DOM 节点

更新时间:2022-10-21 10:45:02

简短版本

参见 https://github.com/angular/angular/issues/2753(最近的评论,不是原问题)

长版

我有一个类似的用例,并且一直在关注有关推荐方法的讨论.

截至目前,DynamicComponentLoader 确实是用于动态组件编译的事实上的工具(阅读:$compile 的替代品)以及您在示例中采用的方法基本上与 这个,@RobWormald 在回答几个类似的问题关于 gitter.

这是另一个有趣的例子 @EricMartinez 给我的,使用了非常相似的方法.

但是,是的,这种方法对我来说也很笨拙,而且我还没有找到(或想出)使用 DCL 执行此操作的更优雅的方法.关于上面链接的github问题的评论包含第三个示例,以及类似的迄今为止尚未得到答复的批评.

我很难相信像这样常见的用例的规范解决方案在最终版本中会如此笨拙(特别是考虑到 相对 $compile),但除此之外的任何事情都将是猜测.

如果你在 gitter 线程中 grep "DCL" 或 "DynamicComponentLoader",那里有几个关于这个主题的有趣对话.一名核心团队成员说了一些大意:DCL 是一种强大的工具,我们只希望它会被真正做框架相关事情的人使用"——我发现……很有趣.

(如果 gitter 的搜索不烂,我会直接引用/链接到那个)

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>

Short version

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


Long version

I have a similar use-case and have been keeping an eye on chatter about recommended approaches to it.

As of now, DynamicComponentLoader is indeed the de-facto tool for dynamic component compilation (read: stand-in for $compile) and the approach you've taken in your example is essentially identical to this one, which @RobWormald has 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 feels clunky to me too, and I've yet to find (or come up with) a more elegant way of doing this with DCL. The comments on the github issue I linked above contain a third example of it, along with similar criticisms that have so far gone unsanswered.

I have a hard time believing that the canonical solution for a use-case as common as this will be so clunky in the final release (particularly given then relative elegance of $compile), but anything beyond that would be speculation.

If you grep "DCL" or "DynamicComponentLoader" in the gitter thread, there are several interesting conversations on this topic there. One of the core team guys said something to the effect of "DCL is a power-tool that we only expect will be used by people doing really framework-ey things" - which I found... interesting.

(I'd have quoted/linked to that directly if gitter's search didn't suck)