4

In Angular 5 application with TypeScript. I faced the problem called Circular Dependency when tried implement communication between components. There are two component radio and radio-group:

<radio-group [(value)]='selected'>
  <radio value='1'></radio>
  <radio value='2'></radio>
  <radio value='3'></radio>
</radio-group>

They communicate with each other when a user selecting and deselecting items.

Components implementation example RadioGroupComponent:

import { Component, forwardRef, Input, Optional, ContentChildren, 

QueryList, EventEmitter, Output, ChangeDetectorRef, ChangeDetectionStrategy, AfterContentInit, OnChanges } from '@angular/core';
import { RadioGroup } from './radio-group.component';

@Component({
  selector: 'radio',
  styles: [`:host{cursor:pointer;}`],
  template: `<div (click)='check()'>
  <span *ngIf='!checked'>⚪️</span>
  <span *ngIf='checked'>🔘</span>
  <span>Click me. Value: {{value}} Checked: {{checked}}</span>
  </div>`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class Radio  {
  @Input() value: any;
  checked = false;
  private _radioGroup: RadioGroup;
  constructor(@Optional() radioGroup: RadioGroup, public cd: ChangeDetectorRef){
    this._radioGroup = radioGroup;
  }
  check(){
    this.checked = true;
    if(this._radioGroup){
      this._radioGroup.selected = this;
    }
    this.markForCheck();
  }
  markForCheck(){
    this.cd.markForCheck();
  }
}

RadioComponent:

import { Component, forwardRef, Input, Optional, ContentChildren, QueryList, EventEmitter, Output, ChangeDetectorRef, ChangeDetectionStrategy, AfterContentInit, OnChanges } from '@angular/core';
import { Radio } from './radio.component';

@Component({
  selector: 'radio-group',
  template: `<ng-content></ng-content>`,
})
export class RadioGroup implements AfterContentInit, OnChanges{
  set selected (component:Radio){
    this._selected = component;
    this.valueChange.emit(component.value);
  } 
  private _selected:Radio = null;
  @Input() value:any;
  @Output() valueChange = new EventEmitter();
  @ContentChildren(forwardRef(() => Radio)) radioComponents: QueryList<Radio>;

  ngAfterContentInit() { this.checkParentComponents();}
  ngOnChanges(){ this.checkParentComponents();}
  checkParentComponents():void{
    this.radioComponents 
    && this.radioComponents.forEach(item=>{
        item.checked = item.value==this.value;
        if(item.checked){ this._selected = item;}
        item.markForCheck();
    });
  }
}

Online examples

Working example with all declarations in one file (stackblitz.com)

Broken example with separated files (stackblitz.com)

Problem

How I can solve this issue with circular dependency and put all components and implementations into separated files? With time components become heavy, how can I slice them into pieces?

4 Answers 4

2

You should not edit RadioGroup attributes from Radio. Communication between child and parent components should be done via @Input and @Output.

So remove RadioGroup from Radio constructor. Instead you can do following,

import { 
   Component, 
   Input,
   EventEmitter,
   Output,
   ChangeDetectorRef,
   ChangeDetectionStrategy
} from '@angular/core';

@Component({
  selector: 'radio',
  styles: [`:host{cursor:pointer;}`],
  template: `
     <div (click)='check()'>
        <span *ngIf='!checked'>⚪️</span>
        <span *ngIf='checked'>🔘</span>
        <span>Click me. Value: {{value}} Checked: {{checked}}</span>
     </div>`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class Radio  {
  @Input() value: any;
  @Output() valueChange: EventEmitter<any> = new EventEmitter();
  checked = false;
  constructor(public cd: ChangeDetectorRef){
  }
  check(){
    this.checked = true;
    this.valueChange.emit(this.value);
    this.markForCheck();
  }
  markForCheck(){
    this.cd.markForCheck();
  }
}

RadioGroup.component

@Component({
  selector: 'radio-group',
  template: `<ng-content></ng-content>`,
})
export class RadioGroup implements AfterContentInit, OnChanges, OnDestroy {
  set selected(component: Radio) {
    this._selected = component;
    this.valueChange.emit(component.value);
  }
  private _selected: Radio = null;
  @Input() value: any;
  @Output() valueChange = new EventEmitter();
  @ContentChildren(forwardRef(() => Radio)) radioComponents: QueryList<Radio>;

  subscriptionList = [];

  ngAfterContentInit() { this.checkParentComponents(); }
  ngOnChanges() { this.checkParentComponents(); }
  checkParentComponents(): void {
    if (this.radioComponents) {
      this.subscriptionList = this.radioComponents.map(item => {
        item.checked = item.value === this.value;
        if (item.checked) { this._selected = item; }
        item.markForCheck();
        // subscribe to each child "valueChange" event and return these subscriptions.
        return item.valueChange.subscription(value => this.selected = value);
      });

    }
  }

  ngOnDestroy() {
      // don't forget to unsubscribe.
      if (this.subscriptionList && this.subscriptionList.length ) {
          this.subscriptionList.forEach(sub => sub.unsubscribe());
      }
  }
}
Sign up to request clarification or add additional context in comments.

Comments

0

Try in Radio component constructor instead of

constructor(@Optional() radioGroup: RadioGroup, public cd: ChangeDetectorRef)

this code

constructor(@Inject(forwardRef(() => RadioGroup)) radioGroup: RadioGroup, public cd: ChangeDetectorRef)

Comments

0

When you need set up communication between components

And CAN NOT use @Output and @Inputs, use providers. DependencyInjection allow you override injected in constructor class whenever you need it.

DependencyInjection (angular.io)

Avoid circular dependencies(angular.io)

radio-group.component.ts:

import { RadioGroupBase } from './radio-group-base';

@Component({
  selector: 'radio-group',
  ...
  ==> providers: [{provide: RadioGroupBase, useExisting: RadioGroup}]
})
export class RadioGroup implements AfterContentInit, OnChanges{
...
}

radio.component.ts:

import { RadioGroupBase } from './radio-group-base';

@Component({
  selector: 'radio',
  ...
})
export class Radio  {
  constructor(
   ==> @Optional() radioGroup: RadioGroupBase, 
  ){
    //variable radioGroup will be undefined when there is no provider.
  }

radio-group-base.ts:

export class RadioGroupBase {
  selected: any;
}

Working solution:

https://stackblitz.com/edit/angular-gyqmve

Comments

-1

Use a shared Service https://angularfirebase.com/lessons/sharing-data-between-angular-components-four-methods/ Section: "Unrelated Components: Sharing Data with a Service"

Angular 2 - Using Shared Service

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.