3

I have a parent component which observes child component's Output event emitter (topicsChanged).

Parent component:

import {
  ChangeDetectionStrategy,
  Component,
  EventEmitter,
  Input,
  OnInit,
  Output
} from "@angular/core";
import { Language } from "../language";
import { TemplateTopic } from "../template-topic";

@Component({
  selector: "at-main-topics",
  templateUrl: "./main-topics.component.html",
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class MainTopicsComponent implements OnInit {
  @Input() templates: string[] = [];
  @Input() templateTopics: TemplateTopic[] = [];
  @Input() languages: Language[] = [];
  @Output() templateTopicChanged = new EventEmitter<TemplateTopic>();

  constructor() {}

  ngOnInit(): void {}

  get availableTemplateTopics(): TemplateTopic[] {
    return this.templates
      .map(x => +x)
      .map(template => {
        const existingTopic = this.templateTopics.find(
          x => x.template === template
        );

        return (
          existingTopic || 
          { //observer will disappear for this empty created object.
            template: template,
            topics: []
          }
        );
      });
  }

  onTopicsChanged(templateTopic: TemplateTopic) {
    // This will not be triggered for 3rd template which is created in availableTemplateTopics getter, because it doesn't exist in initial data (templateTopics)
    this.templateTopicChanged.emit(templateTopic);
  }
}

  <at-template-topic *ngFor="let templateTopic of availableTemplateTopics"
                     [templateTopic]="templateTopic"
                     [languages]="languages"
                     (topicsChanged)="onTopicsChanged($event)">
  </at-template-topic>

In one strange case, this event emitter loses it's parent component's observer. That is - in child component I am opening a dialog. Before dialog is opened, if I inspect the emitter, the observer is there, but once the dialog is closed, observer is gone.

Child component:

import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Language } from '../language';
import { TemplateTopic } from '../template-topic';
import { Topic } from '../topic';
import { TranslationDialogModel } from '../translation-dialog.model';
import { TranslationDialogComponent } from '../translation-dialog/translation-dialog.component';

@Component({
  selector: 'at-template-topic',
  templateUrl: './template-topic.component.html'
})
export class TemplateTopicComponent implements OnInit {
  @Input() templateTopic: TemplateTopic;
  @Input() languages: Language[] = [];
  @Output() topicsChanged = new EventEmitter<TemplateTopic>();

  private dialogTitle: string = 'lorem ipsum'

  constructor(
    private dialog: MatDialog
  ) { }

  ngOnInit(): void {
  }

  onCreateTopic(): void {
    this.openDialog();
  }

  onEditTopic(topic: Topic): void {
    this.openDialog(topic);
  }

  private openDialog(topic?: Topic): void {
    // this.topicsChanged always has observer at this point
    const dialogRef = this.dialog.open(TranslationDialogComponent, {
      data: {
        pageTitle: this.dialogTitle,
        availableLanguages: this.languages,
        translations: topic?.translations
      } as TranslationDialogModel
    });

    dialogRef
      .beforeClosed()
      .subscribe(translations => {
        if (translations) {
          if (topic) {
            topic.translations = translations;
            topic.title = translations[0].title;
          } else {
            this.templateTopic.topics.push({ translations, title: translations[0].title })
          }
          // When called via onCreateTopic method for a category which was created as an empty placeholder, this.topicsChanged has lost it's observer. However if category had initial data, then observer is still there.
          this.topicsChanged.emit(this.templateTopic);
        }
      })
  }
}

There is nothing shady going in the dialog, it simply returns some data and that's it. This is somehow connected to the getter get availableTemplateTopics in parent component from which list of child components are created. In getter there is a list of templates representing each child component which is either populated from already existing data or an empty placeholder is created. And the issue is with the empty placeholder objects.

Snippet:

  get availableTemplateTopics(): TemplateTopic[] {
    return this.templates
      .map(x => +x)
      .map(template => {
        const existingTopic = this.templateTopics.find(
          x => x.template === template
        );

        return (
          existingTopic || 
          { //observer will disappear for this empty created object.
            template: template,
            topics: []
          }
        );
      });
  }

I found that I can solve all of this simply by moving the getter logic one level up, but I would still like to understand this weird behavior. How can observer disappear just like that and how is it connected to the getter?

Stackblitz for full code: https://stackblitz.com/edit/angular-kjewu7?file=src/app/main-topics/main-topics.component.ts

Jānis
  • 1,773
  • 1
  • 21
  • 30

0 Answers0