Search code examples
angularmodal-dialogangular-templateangular-template-variable

passing data between ng-template to ng-content (Angular 17)


How to pass lists$ from list.component to ng-template

im provided code you could see the pipeline of function calls and how the ng-template passes to modal.compoent.html. ..................................................................................................................................................

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.sevice.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>

Solution

  • In Angular 17 instead of using ComponentFactoryResolver use ViewContainerRef this needs to be passed as a param from the component. Then the below code block, will help you achieve what you want!

    ...
    const contentViewRef = vcr.createEmbeddedView(
      content,
      { lists: options!.data }
      // {
      //   injector: this.injector,
      // }
    );
    const modalComponent = vcr.createComponent(ModalComponent, {
      projectableNodes: [contentViewRef.rootNodes],
      // environmentInjector: this.envInjector,
      // injector: this.injector,
    });
    modalComponent.setInput('size', options?.size);
    modalComponent.setInput('title', options?.title);
    modalComponent.setInput('data', options?.data);
    ...
    

    We can pass the second argument as context where we define the property { lists: options!.data, }, also we use setInput an inbuilt function for setting @Input values.

    We can also pass the injectors(environment, or normal if needed)

    In the HTML side, we must define a property on the template that will store this value!

    ...
    <ng-template #createCardTemplate let-lists="lists">
    ...
    

    FULL CODE:

    MAIN

    import { CommonModule } from '@angular/common';
    import { Component, Input, TemplateRef, ViewContainerRef } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import 'zone.js';
    import { CardListDto, ModalComponent } from './app/modal/modal.component';
    import { Observable, of } from 'rxjs';
    import { ModalService } from './app/modal.service';
    import { CardComponent } from './app/card/card.component';
    
    export interface CardDto {}
    
    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [CommonModule, ModalComponent, CardComponent],
      template: `
        <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 let-lists="lists">
                <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>
      `,
    })
    export class App {
      @Input()
      list: CardListDto = new CardListDto();
      @Input()
      cards: CardDto[] = [];
      lists$: Observable<CardListDto[]> | null = of([
        { id: 1, name: 'one' },
        { id: 2, name: 'two' },
        { id: 3, name: 'three' },
      ]);
    
      constructor(
        private vcr: ViewContainerRef,
        private modalService: ModalService
      ) {}
    
      openModal(modalTemplate: TemplateRef<any>) {
        this.modalService
          .open(this.vcr, 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));
      }
    }
    
    bootstrapApplication(App);
    

    MODAL TS

    import {
      Component,
      ElementRef,
      EventEmitter,
      Input,
      Output,
    } from '@angular/core';
    import { Observable } from 'rxjs';
    export class CardListDto {
      name!: string;
    }
    @Component({
      selector: 'app-modal',
      standalone: true,
      imports: [],
      templateUrl: './modal.component.html',
      styleUrl: './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 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>
    

    MODAL SERVICE

    import { DOCUMENT } from '@angular/common';
    import {
      EnvironmentInjector,
      Inject,
      Injectable,
      Injector,
      TemplateRef,
      ViewContainerRef,
    } from '@angular/core';
    import { Subject, of } from 'rxjs';
    import { ModalComponent } from './modal/modal.component';
    
    @Injectable({
      providedIn: 'root',
    })
    export class ModalService {
      private modalNotifier?: Subject<string>;
      constructor(
        private envInjector: EnvironmentInjector,
        private injector: Injector,
        @Inject(DOCUMENT) private document: Document
      ) {}
    
      open(
        vcr: ViewContainerRef,
        content: TemplateRef<any>,
        options?: { size?: string; title?: string; data?: any }
      ) {
        const contentViewRef = vcr.createEmbeddedView(
          content,
          { lists: options!.data }
          // {
          //   injector: this.injector,
          // }
        );
        const modalComponent = vcr.createComponent(ModalComponent, {
          projectableNodes: [contentViewRef.rootNodes],
          // environmentInjector: this.envInjector,
          // injector: this.injector,
        });
        modalComponent.setInput('size', options?.size);
        modalComponent.setInput('title', options?.title);
        modalComponent.setInput('data', options?.data);
        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();
      }
    }
    

    Stackblitz Demo