4

I have looked all over the internet for a resolution to this to no avail (different cases etc.) so please forgive the code dump below, the problem I am having is to do with circular dependencies. The code dump is to provide the context.

Side note: I am fairly new to Angular and Typescript.

The Concept

I am trying to build a set of nested components that all extend a base class to simplify coding, of course. These components can contain children of any of each other. To load the child components, the base class uses a directive which needs to decide which component to load in its place. The best example is to think of nesting <div> and <section> blocks.

Here's my code:

The directive directive.ts

Loads its respective component when load() is called

import { ComponentFactoryResolver, Directive, ViewContainerRef } from '@angular/core';

import { DivComponent } from './div'; // < -- Part of the dependency issues
import { SectionComponent } from './section'; // < -- Part of the dependency issues

@Directive({
    selector: '[child-node]',
})
export class ChildNodeDirective {

    @Input() type: string;
    @Input() content: any;

    constructor(
        private container: ViewContainerRef,
        private resolver: ComponentFactoryResolver,
    ){}

    load(): void {

        let component: Type<any>;

        switch (this.type) {
            case 'div' : component = DivComponent; break;
            case 'section' : component = SectionComponent; break;
        }

        if (component) {
            const factory = this.resolver.resolveComponentFactory(component);
            const componentRef = this.container.createComponent(factory);
            componentRef.instance.content = this.content;
        }

    }

}

The base class base.ts

This class is the base for all the components which;

  1. Uses @ViewChildren and ngAfterContentChecked to load its child directives,
  2. Sets childNodes when set content is invoked so that the component can render its <child-node> elements
import { AfterContentChecked, QueryList, ViewChildren } from '@angular/core';

import { ChildNodeDirective } from './directive'; // < -- Part of the dependency issues

export abstract class Base implements AfterContentChecked {

    // These are elements that the template will render into the directive
    public childNodes: {[type: string]: any} = {};

    private childrenLoaded = false;

    @ViewChildren(ChildNodeDirective) protected children?: QueryList<any>;

    ngAfterContentChecked(): void {
        if (!this.childrenLoaded && this.children) {
            this.children.forEach((child: ChildNodeDirective) => {
                child.load();
            });
            this.childrenLoaded = true;
        }
    }

    set content(content: any) {
        this.childNodes = content.childNodes;
    }

}

The div component div.ts

This component extends the base and simply renders its child nodes

import { Component } from '@angular/core';
import { Base } from './base'; // < -- Part of the dependency issues

@Component({
    selector: 'custom-div',
    templateUrl: './div.html',
})
export class DivComponent extends Base {

    public textContent: string;

    set content(content: any) {

        super.content = content;

        this.textContent = content.text;

    }

}

The div template div.html


<div>{{ textContent }}</div>

<div *ngFor="let child of childNodes">
  <ng-template child-node [content]="child.content" [type]="child.type"></ng-template>
</div>


TL;DR The Problem

All of this seems to work. I am able to produce all kinds of content and deep nesting of children etc. I cannot speak to the "correctness" of the code / implementation but the only issue I am having is circular dependency warnings.

WARNING in Circular dependency detected:
div.ts -> base.ts -> directive.ts -> div.ts

WARNING in Circular dependency detected:
section.ts -> base.ts -> directive.ts -> section.ts

PLEASE help me get rid of them...

SOLUTION

Based on Kai's last advise, I created a decorator to collect the meta data for use inside the directive.

Changes in directive.ts


export const HtmlElementMap: {component: Type<any>, map: string[]}[] = [];

export function HtmlComponent(config: {map: string[]}) {
    return (target: Type<any>) => {
        ElementMap.push({component: target, map: config.map});
    };
}

@Directive({
    selector: '[child-node]',
})
export class ChildNodeDirective {

    ...

    load(): void {

        let component: Type<any>;

        HtmlElementMap
          .filter(v => v.map.indexOf(this.type) > -1)
          .forEach(
              v => {
                  if (undefined === component) {
                      component = v.component;
                  }
              }
          );

        if (component) {
            const factory = this.resolver.resolveComponentFactory(component);
            const componentRef = this.container.createComponent(factory);
            componentRef.instance.content = this.content;
        }

    }

}

And div.ts as well as subsequent components


@HtmlComponent({map: ['div']})
@Component({
    selector: 'custom-div',
    templateUrl: './div.html',
})
export class DivComponent extends Base {

   .
   .
   .

}

Shadowalker
  • 402
  • 1
  • 7
  • 17
Prof
  • 2,898
  • 1
  • 21
  • 38
  • When Itry this I get an error "Cannot access 'HtmlElementMap' before initialization". How did you get around this? – sbonkosky Nov 09 '22 at 14:04

1 Answers1

4

In my opinion the red line in my diagramm is the problem here. So you can try to solve and remove this dependency if you register your components (need for the switch statement) during runtime. For example Base class can collect all available components and pass them to the ChildNodeDirective class. Or you can use a service to register them. If you have a working stackblitz sample, we can help you with the code.

enter image description here

kai
  • 6,702
  • 22
  • 38
  • I LOVE your picture XD So you're suggesting creating a provider, on construct of section/div register self and then get the same provider in the directive to access the map? I'll try that out, if it works I'll mark as correct. I didn't think of that... You're a hero! – Prof Jun 25 '20 at 08:33
  • @Prof83 Exactly. It may be not the cleanest solution but it sounds okay for me and i cannot think of a better one for the moment. – kai Jun 25 '20 at 08:38
  • @kai I also like the graphic very much. What did you use to create it? – Fab2 Jun 25 '20 at 08:43
  • 2
    @Fab2 Thanks, i created it with the awsome open source tool https://excalidraw.com/ – kai Jun 25 '20 at 08:46
  • @kai I have updated my question with a new issue, we are almost there. Just how to combat the registry not being constructed? – Prof Jun 25 '20 at 09:13
  • @Prof83 Oops, thats a detail i forgot about :/ But i have a solution for that which is even cleaner than using an angular service and DI. Use a decorator something like `@HtmlComponent()`. This decorator received the class and can get the string name like `div` as a parameter. And inside the decorator you can create your registry and import it into `ChildNodeDirective` without using DI. – kai Jun 25 '20 at 10:31
  • @kai that worked perfectly. Updating my question and I will mark your answer as correct – Prof Jun 25 '20 at 12:10