Angular 17:如何将lists$从list.component传递到ng-template
在Angular 17中如何将ListComponent的lists$传递到ng-template
我需要解决Angular 17环境下,把ListComponent中的lists$ Observable传递到模板内ng-template的问题,以下是当前相关代码:
List.component.html
<div class="list"> <div class="header"> <h3>{{list.name}}</h3> <span style=" display: flex; width: 25%; justify-content: space-between;"> <h3>{{cards.length}}</h3> <button class="list-options-btn"></button> </span> </div> <div class="btn-container"> <button class="card-add-btn" (click)="openModal(createCardTemplate)">Add new card</button> </div> <div class="cards-container" *ngIf="lists$ | async as lists"> <app-card *ngFor="let card of cards" [card]=card [lists]=lists ></app-card> </div> <ng-template #createCardTemplate> <div> <label for="name">Name</label><br> <input id="name" formControlName="name" type="text"> </div> <div> <label for="description">Description</label><br> <input id="description" formControlName="description" type="text"> </div> <div> <label for="dueDate">Due Date</label><br> <input id="dueDate" formControlName="dueDate" type="date"> </div> <div> <label for="priority">Priority</label><br> <select id="priority" formControlName="priority"> <option value="1">Low</option> <option value="2">Medium</option> <option value="3">High</option> </select> </div> <div> <label for="listId">List</label><br> <select id="listId" formControlName="listId"> <option *ngFor="let list of lists$ | async" value={{ list.id }}> {{ list.name }} </option> </select> </div> </ng-template> </div>
List.component.ts
import { Component, Input, TemplateRef } from '@angular/core'; import { CardDto, Priority } from 'src/Dtos/CardDto'; import { CardListDto } from 'src/Dtos/CardListDto'; import { Observable } from 'rxjs'; import { ListsService } from 'src/services/lists.service'; import { ModalService } from 'src/services/modal.service'; @Component({ selector: 'app-list', templateUrl: './list.component.html', styleUrls: ['./list.component.css'] }) export class ListComponent { @Input() list: CardListDto = new CardListDto; @Input() cards: CardDto[] = []; lists$: Observable<CardListDto[]> | null = null; constructor( private listsService: ListsService, private modalService: ModalService){} openModal(modalTemplate: TemplateRef<any>) { this.modalService .open(modalTemplate, { title: 'New Card', data: this.lists$ }) .subscribe((action) => { console.log('modalAction', action); }); } ngOnInit(){ this.lists$ = this.listsService.getLists(); this.lists$.subscribe(lists => console.log(lists)); } }
Modal.service.ts
import { DOCUMENT } from '@angular/common'; import { ComponentFactoryResolver, Inject, Injectable, Injector, TemplateRef, } from '@angular/core'; import { Subject } from 'rxjs'; import { ModalComponent } from 'src/app/modal/modal.component'; @Injectable() export class ModalService { private modalNotifier?: Subject<string>; constructor( private resolver: ComponentFactoryResolver, private injector: Injector, @Inject(DOCUMENT) private document: Document ) {} open(content: TemplateRef<any>, options?: { size?: string; title?: string; data?: any }) { const modalComponentFactory = this.resolver.resolveComponentFactory(ModalComponent); const contentViewRef = content.createEmbeddedView(null); const modalComponent = modalComponentFactory.create(this.injector, [contentViewRef.rootNodes]); modalComponent.instance.size = options?.size; modalComponent.instance.title = options?.title; modalComponent.instance.data = options?.data; // Pass lists$ as a property of the ModalComponent modalComponent.instance.closeEvent.subscribe(() => this.closeModal()); modalComponent.instance.submitEvent.subscribe(() => this.submitModal()); modalComponent.hostView.detectChanges(); this.document.body.appendChild(modalComponent.location.nativeElement); this.modalNotifier = new Subject(); return this.modalNotifier?.asObservable(); } closeModal() { this.modalNotifier?.complete(); } submitModal() { this.modalNotifier?.next('confirm'); this.closeModal(); } }
Modal.component.ts
import { Component, ElementRef, EventEmitter, Input, Output, } from '@angular/core'; import { Observable } from 'rxjs'; import { CardListDto } from 'src/Dtos/CardListDto'; @Component({ selector: 'modal', templateUrl: './modal.component.html', styleUrls: ['./modal.component.css'], }) export class ModalComponent{ @Input() size? = 'md'; @Input() title? = 'Modal title'; @Input() data: any; @Output() closeEvent = new EventEmitter(); @Output() submitEvent = new EventEmitter(); constructor(private elementRef: ElementRef) {} ngOnInit(){ (<Observable<CardListDto[]>>this.data).subscribe( data => { console.log(data) } ) } close(): void { this.elementRef.nativeElement.remove(); this.closeEvent.emit(); } submit(): void { this.elementRef.nativeElement.remove(); this.submitEvent.emit(); } }
Modal.component.html
<div class="modal {{ size }}"> <div class="modal-header"> {{ title }} <span class="modal-close" (click)="close()">✕</span> </div> <div class="modal-content"> <ng-content></ng-content> </div> <div class="modal-footer"> <button (click)="submit()">Submit</button> </div> </div> <div class="modal-backdrop" (click)="close()"></div>
解决方案
当前代码中,ng-template直接使用lists$ | async会因为模板上下文隔离无法获取到ListComponent中的lists$,可以通过以下两种方式解决:
方式一:提前订阅数据,直接传递数组
这种方式避免模板中多次使用async管道导致的重复订阅问题,实现最简单:
- 在ListComponent中添加属性存储订阅后的列表数据:
lists: CardListDto[] = []; ngOnInit(){ this.lists$ = this.listsService.getLists(); this.lists$.subscribe(lists => { console.log(lists); this.lists = lists; // 存储已解析的数组 }); }
- 修改ng-template中的下拉列表代码,直接使用已订阅的数组:
<ng-template #createCardTemplate> <!-- 其他表单内容保持不变 --> <div> <label for="listId">List</label><br> <select id="listId" formControlName="listId"> <option *ngFor="let list of lists" value={{ list.id }}> {{ list.name }} </option> </select> </div> </ng-template>
方式二:利用TemplateRef上下文传递Observable
如果需要保持Observable的响应式特性,可以通过模板上下文传递数据:
- 修改ListComponent的openModal方法,创建EmbeddedView时传入上下文:
openModal(modalTemplate: TemplateRef<any>) { // 传入包含lists$的上下文 const contentViewRef = modalTemplate.createEmbeddedView({ lists$: this.lists$ }); this.modalService .open(modalTemplate, { title: 'New Card', contentViewRef }) .subscribe((action) => { console.log('modalAction', action); }); }
- 修改ModalService的open方法,使用传入的contentViewRef:
open(content: TemplateRef<any>, options?: { size?: string; title?: string; contentViewRef?: any }) { const modalComponentFactory = this.resolver.resolveComponentFactory(ModalComponent); // 使用传入的已创建好的contentViewRef const modalComponent = modalComponentFactory.create(this.injector, [options?.contentViewRef.rootNodes]); modalComponent.instance.size = options?.size; modalComponent.instance.title = options?.title; modalComponent.instance.closeEvent.subscribe(() => this.closeModal()); modalComponent.instance.submitEvent.subscribe(() => this.submitModal()); modalComponent.hostView.detectChanges(); this.document.body.appendChild(modalComponent.location.nativeElement); this.modalNotifier = new Subject(); return this.modalNotifier?.asObservable(); }
- 修改ng-template,声明接收上下文变量:
<ng-template #createCardTemplate let-lists$="lists$"> <!-- 其他表单内容保持不变 --> <div> <label for="listId">List</label><br> <select id="listId" formControlName="listId"> <option *ngFor="let list of lists$ | async" value={{ list.id }}> {{ list.name }} </option> </select> </div> </ng-template>
额外优化:适配Angular 17新API
Angular 17推荐使用ViewContainerRef替代ComponentFactoryResolver,可以重构ModalService:
import { DOCUMENT } from '@angular/common'; import { Inject, Injectable, Injector, TemplateRef, ViewContainerRef, } from '@angular/core'; import { Subject } from 'rxjs'; import { ModalComponent } from 'src/app/modal/modal.component'; @Injectable() export class ModalService { private modalNotifier?: Subject<string>; constructor( private injector: Injector, @Inject(DOCUMENT) private document: Document, private viewContainerRef: ViewContainerRef ) {} open(content: TemplateRef<any>, options?: { size?: string; title?: string; data?: any }) { // 使用createComponent创建模态框组件 const modalComponent = this.viewContainerRef.createComponent(ModalComponent); modalComponent.instance.size = options?.size; modalComponent.instance.title = options?.title; modalComponent.instance.data = options?.data; modalComponent.instance.closeEvent.subscribe(() => this.closeModal(modalComponent)); modalComponent.instance.submitEvent.subscribe(() => this.submitModal(modalComponent)); // 插入模板内容到模态框 const contentViewRef = content.createEmbeddedView(null); modalComponent.instance.contentContainer.insert(contentViewRef); this.document.body.appendChild(modalComponent.location.nativeElement); this.modalNotifier = new Subject(); return this.modalNotifier?.asObservable(); } closeModal(component: any) { component.destroy(); this.modalNotifier?.complete(); } submitModal(component: any) { this.modalNotifier?.next('confirm'); this.closeModal(component); } }
同时修改ModalComponent添加内容容器:
import { Component, ElementRef, EventEmitter, Input, Output, ViewChild, ViewContainerRef, } from '@angular/core'; import { Observable } from 'rxjs'; import { CardListDto } from 'src/Dtos/CardListDto'; @Component({ selector: 'modal', templateUrl: './modal.component.html', styleUrls: ['./modal.component.css'], }) export class ModalComponent{ @Input() size? = 'md'; @Input() title? = 'Modal title'; @Input() data: any; @ViewChild('contentContainer', { read: ViewContainerRef }) contentContainer!: ViewContainerRef; @Output() closeEvent = new EventEmitter(); @Output() submitEvent = new EventEmitter(); close(): void { this.closeEvent.emit(); } submit(): void { this.submitEvent.emit(); } }
Modal.component.html调整内容容器:
<div class="modal {{ size }}"> <div class="modal-header"> {{ title }} <span class="modal-close" (click)="close()">✕</span> </div> <div class="modal-content" #contentContainer></div> <div class="modal-footer"> <button (click)="submit()">Submit</button> </div> </div> <div class="modal-backdrop" (click)="close()"></div>
内容的提问来源于stack exchange,提问作者someone
相关产品推荐
相关产品推荐

