Как я могу предотвратить выполнение компонента Angular 8+ до тех пор, пока не будет установлено свойство Input и не будет готов ViewChild - PullRequest
0 голосов
/ 21 ноября 2019

Я написал угловой компонент для рендеринга русалок графиков с использованием пакета русалка npm .

Он работает как есть (см. Ниже), но яхотел бы найти способ его использования без родительского компонента, который должен включать *ngIf (чтобы не допустить его рендеринга при отсутствии данных). Мне бы хотелось, чтобы сам компонент мог решать, вызывать ли mermaid.render() или нет сам. Если *ngIf не используется, компонент ngAfterContentInit() может работать, когда graphDefinition равен null. Это приводит к ошибке библиотеки mermaid.

Я мог бы использовать ngOnChanges() или установщик свойства, чтобы определить, когда установлено значение, но поскольку компонент опирается на ViewChild, я думаю, что есть опасностьчто ngOnChanges() или установщик свойств будет выполнен до того, как будет готов ViewChild.

Вопрос: Можете ли вы предложить способ надежного улучшения этого компонента, чтобы *ngIf не требовался?

Использование

Пример использования, когда определение графа (строка) предоставляется в виде Observable<string> из HTTP-вызова в родительском компоненте:

parent.component.html

<mermaid-viewer
    *ngIf="graphDefinition$ | async as graphDefinition"
    [graphDefinition]="graphDefinition"
></mermaid-viewer>

Компонент

mermaid-viewer.component.html

<div #mermaid></div>

русалка-viewer.component.ts

import {
    AfterContentInit,
    Component,
    Input,
    ViewChild
} from "@angular/core";
import * as mermaid from "mermaid";

@Component({
    selector: "mermaid-viewer",
    templateUrl: "./mermaid-viewer.component.html"
})
export class MermaidViewerComponent implements AfterContentInit {

    @ViewChild("mermaid", { static: true })
    public mermaidDiv;

    @Input()
    public graphDefinition: string;

    public ngAfterContentInit(): void {

        mermaid.initialize({
            theme: "default"
        });

        const element: any = this.mermaidDiv.nativeElement;
        mermaid.render(
            "graphDiv",
            this.graphDefinition,
            (svgCode, bindFunctions) => {
                element.innerHTML = svgCode;
            }
        );
    }
}

Ответы [ 2 ]

1 голос
/ 21 ноября 2019

Я придумал это, вдохновленный ответом Cristian Traìna , но не используя BehaviourSubject или combineLatest.

Функция render() будет вызвана дважды. В первый раз будет доступен только один из graphDefinition или mermaidDiv, и он ничего не будет делать. Во второй раз оба значения будут доступны, и тогда он вызовет mermaid.render. Я думаю, что это немного проще, хотя, возможно, не так элегантно - и не будет хорошо масштабироваться, если потребуется несколько свойств.

import {
    Component,
    Input,
    ViewChild
} from "@angular/core";
import * as mermaid from "mermaid";

@Component({
    selector: "mermaid-viewer",
    templateUrl: "./mermaid-viewer.component.html"
})
export class MermaidViewerComponent {

    private hasRendered = false;

    private _mermaidDiv;
    @ViewChild("mermaid", { static: true })
    public set mermaidDiv(value) {
        this._mermaidDiv = value;
        this.render();
    }
    public get mermaidDiv() {
        return this._mermaidDiv;
    }

    private _graphDefinition: string;
    @Input()
    public set graphDefinition(value) {
        this._graphDefinition = value;
        this.render();
    }
    public get graphDefinition() {
        return this._graphDefinition;
    }

    private render() {
        if (this.graphDefinition && this.mermaidDiv) {

            this.hasRendered = true;

            mermaid.initialize({
                theme: "default"
            });

            const element: any = this.mermaidDiv.nativeElement;
            mermaid.render(
                "graphDiv",
                this.graphDefinition,
                (svgCode, bindFunctions) => {
                    element.innerHTML = svgCode;
                }
            );
        }
    }
}
1 голос
/ 21 ноября 2019

Полный ответ на ваш вопрос слишком сложен, и он будет включать BehaviourSubject и таких операторов, как combineLatest. Но я пытаюсь дать вам подсказку.

Вы можете объединить ваш Input с установщиком и получателем, чтобы получать уведомления при изменении вашего ввода:

private _graphDefinition: string;
@Input()
public set graphDefinition(value) {
  this._graphDefinition = value;
  // .next() on BehaviourSubject
}
public get graphDefinition() {
  return this._graphDefinition;
}

Вы можетесделать то же самое с ViewChild, в этом случае вы будете излучать только один раз, когда инициализируется представление элемента:

private _mermaidDiv;
@ViewChild("mermaid", { static: true })
public set mermaidDiv(value) {
   this._mermaidDiv = value;
   // .next() on another BehaviourSubject
}
public get mermaidDiv() {
  return this._mermaidDiv;
}

Затем вы можете использовать combineLatest на ngAfterContentInit (ngAfterViewInitя думаю, было бы неплохо):

ngAfterContentInit() {
  // ...
  combineLatest(
     bs1,
     bs2
  ).subscribe(result => {
     // do something with definition + view
     // result[0] => is your graph definition
     // result[1] => is your queried view
  });
  // ...
}

Теперь вы можете несколько раз изменить graphDefinition в родительском компоненте, и ваша конфигурация будет обновляться автоматически

...