angular2 学习笔记 ( Dynamic Component 动态组件)

后学
2023-12-01

更新: 2019-03-20 

今天遇到烦人的情况, 在做 carousel 幻灯片插件. 

<carousel>
  <item *ngFor="..." >
      <img lazyload ...>
  </item>
<carousel>

本来是想用 ng-content 让外部的组件负责 for loop 然后把图放入到组件里. 

后来发现要有 looping 的体验, 这样就需要做 clone element, 本来嘛, clone 也不是什么大件事, 直接操作 dom 也就是了. 

后来又要求图片需要 lazy load, 还好之前就有 lazyload 指令了. 

结果跑不起来,因为 lazyload 指令的设计是通过 onInit 去寻找 parent 然后做 intersectionObserver.observe(element) 

我 html clone 的 element 不跑 ng, lazyload 指令作废... 

看来 clone 行不通,就改用动态组件吧. 外部穿 template 进来,里面不用 clone 直接 createEmbedView.

本以为 transclude 换成 ng-template 是很容易的 

https://medium.com/@unrealprogrammer/angulars-content-projection-trap-and-why-you-should-consider-using-template-outlet-instead-cc3c4cad87c9

甚至有文章说多用 template 少用 transclude 呢. 

所以呢,我发现了. 使用 transclude template 和 translude element 是差很多的

 

<carousel>
<ng-template>
  <item>
      <img lazyload ...>
  </item>
</ng-template>
<carousel>

表面上看只是套了一层, 但是这让组件沟通变得很难. 

之前我可以用 @contentchild 直接获取到所有 item 

现在呢, 由于是外部只给了一个 template, 没有 ngfor 了, 反而是内部使用 template 生产 item 

这就导致了 @contentchild 获取不到 item (或者说要等到下一次 life cycle after content checked 才能获取到) 

另外我也间接体会到, 如果这个 template 不是用 transclude 的方式传递,而是用 input 的方式传递.. carousel 组件时完全没有方法可以获取到 item 的. 

由此看来 template 的局限还是很大的. 

相关问题 : 

https://github.com/angular/angular/issues/14842

最后就是想说,这 2 个方式并不是互相替代的. 某一些极端情况下, 2 个都不好用 >"<

 

更新 : 2019-01-07

不错的文章

refer : 

https://juejin.im/post/5ae00616f265da0b7e0bee78

https://juejin.im/post/5ab09a49518825557005d805

 

更新 2019 01-06:

有时候我们会想要把 component 缓存起来, 比如说当用户在切换页面的时候, 我们希望当用户切回同一页是, 能够使用缓存. 

做法很多. 

比如, 我们可以搞一个 state service, 所有页面上的交互状态都存起来, 当用户切回来的时候, 在把 state 输入 component 内. 

这种做法在 redux 的情况下比较容易实现,但是如果没有使用 redux 呢?

总不能为了这个就搞 redux 吧, 小题大做了.

那还有一个办法是使用 reusestrategy 

它的实现原理其实很简单.

动态创建 component insert to container, 在 remove 的时候, 我们不调用 remove, 而是调用 detech 

这样它只是把 view 拿走并不清除掉, 留意哦, 这时 dom 虽然没有 component 了, 但我们的 component 依然存在的, 如果里面有 interval 依然会执行.

然后呢就是把它 inert 放回去就可以了.

private cacheComponent: ComponentRef<AbcComponent>;

  add() {
    let factory = this.componentFactoryResolver.resolveComponentFactory(AbcComponent);
    const component = this.cacheComponent = factory.create(this.injector);
    this.viewContainerRef.insert(component.hostView, 0);
  }

  remove() {
    this.viewContainerRef.detach();    
  }

  back() {
    console.log('cache component value', this.cacheComponent.instance.value);
    this.viewContainerRef.insert(this.cacheComponent.hostView);
  }

通过这个方法, 我们可以直接保留整个 component 和 view, state 自然也保存了. 

 

 

更新 2018-02-07

详细讲一下 TemplateRef 和 ViewContainerRef 的插入 

refer : 

https://segmentfault.com/a/1190000008672478

https://stackoverflow.com/questions/45438304/insert-a-dynamic-component-as-child-of-a-container-in-the-dom-angular2

https://stackoverflow.com/questions/46992280/viewcontainerref-index-parameter

<ng-template #temp >
    <dynamic-dada></dynamic-dada>
</ng-template>

模板就是一个不会马上被渲染出来的东西,等我们想要的时候才去调用渲染它. 

上面 ng-template 就表达了这是个模板,我们通过 #temp + viewchild 来获取它的指针来准备后续的调用. 

@ViewChild('temp', { read: TemplateRef })
  temp: TemplateRef<any>

等到我们想调用的时候, 就调用 createEmbeddedView, 

let embeddedView = this.temp.createEmbeddedView(null); // 参数是 template context 可以看之前的文章有提到, 诸如 let-i=index 之类的变量

调用这个方法之后, 我们会得到 ViewRef, 这时 template 就已经渲染成 element 了, 不过呢它并还没有运行 detechChange

所以任何 binding 这时是无效的, 里面有组件的话也统统还没有执行 OnInit 等. element 的 parentNode 目前也是 null 

这时如果我们调用  detechChange() 那么模板会开始执行binding 和 compoent 实例化, 实例化的 component 会接上当前的 component tree . 但是呢, 这样做是不真确的, 我们应该要先把 element 插入到我们的 document 里头, 才 detechChange

(不然会有意想不到的结果, 比如你的 parent viewchildrenQueryList 没有触发change 等等, 顺便一提. templateRef insert and detechChange 完全好了之后 parent 的viewchildQueryList.change 才会触发哦)

接着就要介绍一下 "家“ 了 viewContainerRef 

ng 替我们封装了一个 viewContainerRef 来负责 element 的插入或移除. 

首先我们说说如何获取到这个 viewContainerRef 

方法一 : 在组件注入 host 的 containerRef 

constructor(
  private vcr: ViewContainerRef,
  private cdr: ChangeDetectorRef
) { }

方法二 : 

<ng-container #mycontainer ></ng-container>
<div #mycontainertwo > <span>child</span> </div>

然后通过 viewchild 去获取 

@ViewChild('mycontainer', { read : ViewContainerRef })
  mycontainer : ViewContainerRef

这时我们就可以使用 viewcontainerref 来插入我们刚才的 viewref 了 

this.mycontainer.insert(embeddedView, 0);
  embeddedView.detectChanges();

insert 的第2参数是位置. 

这里有一个重要的概念要说 

insert 之后 element 会在 container 的 sibling 而不是 child 

因为在 ng 看来 container 其实只是一个标签. 并不是一个具体的 element 

即使你写 <div #mycontainer></div> 

insert 之后 element 也不会去到这个 div 的 child 里面而是 sibling 

<div #mycontainer ></div>

<inserted-elem>

所以如果你使用刚才说的方法一,注入 viewcontainer

也是同样的原理 ”记住是 sibling 而不是 child“ 

有了 TemplateRef + ViewContainerRef 我们就可以很容易做动态模板啦 ^^

 

 

 

时间 : 2017-01-25

一样这一篇最要讲概念而已.

 

refer :

http://blog.rangle.io/dynamically-creating-components-with-angular-2/ (例子)
https://www.ag-grid.com/ag-grid-angular-aot-dynamic-components/ (动态 entryComponents)

http://stackoverflow.com/questions/40106480/what-are-projectable-nodes-in-angular2 (Projectable nodes, something like transclude)

http://stackoverflow.com/questions/38888008/how-can-i-use-create-dynamic-template-to-compile-dynamic-component-with-angular ( dynamic Jit version )

https://medium.com/@isaacplmann/if-you-think-you-need-the-angular-2-runtime-jit-compiler-2ed4308f7515#.72ln3bcsy (many way Aot version)

https://www.youtube.com/watch?v=EMjTp12VbQ8 (youtube)

 

动态组件分 2 种 

1. Jit

2. Aot

Jit 的情况下你可以动态的写组件模板, 最后 append 出去, 类似 ng1 的 $compile 

Aot 的话, 模板是固定的, 我们只是可以动态创建 component 然后 append 出去

这一篇只会谈及 Aot 

 

要知道的事项 : 

1. 所有要动态的组件除了需要 declarations之外还要声明到 entryComponents 里头, 原因是 ng 是通过扫描模板来 import component class 的, 动态组件不会出现在模板上所以我们要用另一个 way 告诉 ng。

            providers: [
                {provide: ANALYZE_FOR_ENTRY_COMPONENTS, useValue: [AComponentClass, BComponentClass], multi: true}
            ]

要方便扩展的话可以用 provides, ng-router 也是用这个实现的.

2. 目前只有动态组件,没有动态指令 (呃..所以动态创建的组件就不能附带指令咯.../.\)

3. 例子 

@Component({
    selector: 'aaa',
    template: ``
})
export class AAAComponent implements OnInit, AfterContentInit {
    constructor(
        private vcr: ViewContainerRef,
        private cfr: ComponentFactoryResolver
    ) { }

    @ContentChildren("dynamic", { read: ElementRef }) elem: QueryList<ElementRef>  //read 的作用是强转类型

    ngOnInit() {


    }

    ngAfterContentInit() {                   
        let providers = ReflectiveInjector.resolve([AbcService]); //为组件添加 providers
        let injector = ReflectiveInjector.fromResolvedProviders(providers, this.vcr.parentInjector); //创建注入器给 component (记得要继承哦)
        let factory = this.cfr.resolveComponentFactory(AbcComponent); //创建 component 工厂
        let component = factory.create(injector,[[this.elem.first.nativeElement],[this.elem.last.nativeElement] ]); //创建 component, 这是就把注入器放进了, 后面的 array 是给 ng-content 用的
        component.instance.name = "keatkeat"; // 对 input, output 做点东西 
        this.vcr.insert(component.hostView, 0); // 插入到模板中  0 是 position, 如果是 0 其实可以不用放. 

        // 如果不需要设定 providers 的话,可以省略一些 : 
        // let factory = this.resolver.resolveComponentFactory(AbcComponent);  
        // let component = this.vcr.createComponent(factory, 0);
        // component.instance.name = "keatkeat";   
    }
}

里头说的 ng-content, 就是 Projectable nodes , 我个人认为这个做法还不太理想,因为 ng-content 应该是可以通过 select 找到对应的 tranclude 的,不过这里的参数 array 已经固定了 tranclude 的位置. 

所以目前, 如果你要做类似 tranclude 的事情, 改用 input 传递 templateRef 反而会比较容易控制. 

类似这样 

@Component({    
    template : `
        <p>final</p>
        <template [ngTemplateOutlet]="template" [ngOutletContext]="{ innerValue : 'huhu' }" ></template>
    `,
    selector : "final"
})
export class FinalComponent implements OnInit {
    constructor(
    ) { }

    @Input()
    template : TemplateRef<any> //传进来

    ngOnInit() {       
        console.log(this.template);
    }
}

 4. 个人的想法 

一个动态组件应该和平时的组件必须是一样的,意思是我们可以随时把任何一个组件改成动态调用的方式. 

不过目前 ng 支持的不是很好

-input, output (支持)

-tranclude (Projectable nodes 显然和 ng-content配合不上)

-在 component 上放指令 (动态创建的 component, 没办法加上指令, 这导致了 dynamic accessor 很难写)

例子 

@Component({
    templateUrl : "./debugTwo.component.html"
})
export class DebugTwoComponent implements OnInit, AfterViewInit {

    constructor(
        private fb : FormBuilder,
        private cfr : ComponentFactoryResolver
    ) { }

    form : FormGroup
    @ViewChild("target", { read : ViewContainerRef }) target : ViewContainerRef
    ngOnInit() { 

        this.form = this.fb.group({
            age : [0]
        });
    }

    ngAfterViewInit()
    {
        let factory = this.cfr.resolveComponentFactory(AccessorComponent);  
        let component = this.target.createComponent(factory, 0);
        let ctrl = this.form.controls["age"];
        component.instance.writeValue(ctrl.value); //需要手动去调用 writeValue, registerOnChange, registerOnTouched, 如果可以直接加上 formControlName 指令,就方便多了.
        component.instance.registerOnChange((v) => {
            ctrl.setValue(v);
        });
        //component.instance.template = this.template;
    }
}

 

5. 一些常用到的类 

-ViewContainerRef : 好比一个 root div, 通常我们引用它目的就是 append element 进去.

 常用 : createEmbeddedView, createComponent, insert

-TemplateRef : 指的是 <template> 里面的内容.  

 常用 : createEmbeddedView

-ElementRef : dom 

 常用 : nativeElement (获取 dom 对象引用)

 

 

 
 

 

转载于:https://www.cnblogs.com/keatkeat/p/6349485.html

 类似资料: