Home > Enterprise >  How to use event binding on an ng-container component to pass an @Output from its child to its paren
How to use event binding on an ng-container component to pass an @Output from its child to its paren

Time:09-02

In this setup, I have 3 components, the subject component below is the component "B".

  • A = grandparent
  • B = parent (subject of this code)
  • C = child

Both "B" and "C" have an @Output event emitter named "valueChange".

I am attempting to pass the valueChange event from the child "C" to the subject "B" to the parent "A", so I am setting up event binding via @Output event emit in the chain.

Apparently ng-container is not capable to holding an event binding as I have it setup in the component code below. Is there a means to work around it?

@Component({
  selector: 'select-widget-widget',
  template: `
    <ng-container #widgetContainer (valueChange)="doValueChange($event)"></ng-container>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectWidgetComponent implements OnChanges, OnInit {
  newComponent: ComponentRef<any> = null;
  @Input() layoutNode: any;
  @Input() layoutIndex: number[];
  @Input() dataIndex: number[];
  @Input() rowIndex: number;
  @Input() childTemplateModel: any;
  @Input() flexContainerClass: string;
  @Output() valueChange: EventEmitter<any> = new EventEmitter<any>();
  @ViewChild('widgetContainer', { static: true, read: ViewContainerRef }) widgetContainer: ViewContainerRef;

  constructor(private componentFactory: ComponentFactoryResolver, private changeDetectorRef: ChangeDetectorRef) {}

  ngOnInit() {
    this.updateComponent();
  }

  ngOnChanges() {
    this.updateComponent();
  }

  updateComponent() {
    if (!this.newComponent && (this.layoutNode || {}).widget) {
      this.newComponent = this.widgetContainer.createComponent(this.componentFactory.resolveComponentFactory(this.layoutNode.widget));
    }
    if (this.newComponent) {
      for (const input of ['layoutNode', 'layoutIndex', 'dataIndex', 'rowIndex', 'childTemplateModel', 'flexContainerClass']) {
        this.newComponent.instance[input] = this[input];
      }
    }
    // Update with suggested Answer from Anton M
    this.newComponent.instance.valueChange.subscribe(
      event => this.doValueChange(event)
    );
  }

  doValueChange($event: any) {
    console.log('doValueChange called from select-widget with $event: ', $event);
    this.valueChange.emit($event);
    this.changeDetectorRef.markForCheck();
  }

}

Component "C" code:

@Output() valueChange: EventEmitter<any> = new EventEmitter<any>();


ngOnInit() {
  this.options = cloneDeep(this.layoutNode.options) || {};
  this.jsf.initializeControl(this);

  if (!hasValue(this.controlValue) && hasValue(this.options.defaultValue)) {
    this.controlValue = this.options.defaultValue;
    this.jsf.triggerSyncComponents();
  }

  this.syncComponentsSubscription = this.jsf.syncComponents.subscribe((value: SyncComponents) => {
    if (!value.targets.length || value.targets.includes(this.controlName)) {
      if (has(value, `data.${this.controlName}`)) {
        this.controlValue = value.data[this.controlName];
      }
      this.syncChanges();
    }
  });

  this.jsf.registerComponentInit({ componentId: this.componentId, name: this.controlName });
}


syncChanges() {
  let value: any;
  const values = this.jsf.formGroup.value;
  this.valueChange.emit(this.controlValue);
  this.changeDetectorRef.markForCheck(); // required to update the spans in case of valuechange because of onpush CD
}

Update: Thanks to the accepted answer, I just had to modify the code in the subject component ("B") to get the emitter to pass through the ng-container component like so:

updateComponent() {
    if (!this.newComponent && (this.layoutNode || {}).widget) {
      this.newComponent = this.widgetContainer.createComponent(this.componentFactory.resolveComponentFactory(this.layoutNode.widget));
    }
    if (this.newComponent) {
      for (const input of ['layoutNode', 'layoutIndex', 'dataIndex', 'rowIndex', 'childTemplateModel', 'flexContainerClass']) {
        this.newComponent.instance[input] = this[input];
      }
    }

 if (this.newComponent.instance.valueChange) {
  this.newComponent.instance.valueChange.subscribe(
    event => this.doValueChange()
  );
 }
}

doValueChange() {
    this.valueChange.emit('select says hidden component emitted');
    this.changeDetectorRef.markForCheck(); // required because of onpush CD
  }

CodePudding user response:

You have wrong element usage here with binding event to ng-container. Actually ng-container is just a wrapper, or a placeholder as in your case. And it cannot be used as element, but you can create some component within.

In you case you just have to bind events from your child instance component to current event emitter, like this:

...
updateComponent() {
    if (!this.newComponent && (this.layoutNode || {}).widget) {
      this.newComponent = this.widgetContainer.createComponent(this.componentFactory.resolveComponentFactory(this.layoutNode.widget));
    }
    if (this.newComponent) {
      for (const input of ['layoutNode', 'layoutIndex', 'dataIndex', 'rowIndex', 'childTemplateModel', 'flexContainerClass']) {
        this.newComponent.instance[input] = this[input];
      }
    }

    // add this:
    this.valueChange = this.newComponent.instance.valueChange;
    
    }
...

Or if you need to parse events from child, you can do this way:

...
updateComponent() {
    if (!this.newComponent && (this.layoutNode || {}).widget) {
      ...
    }
    if (this.newComponent) {
      ...
    }

    // add this:
    this.newComponent.instance.valueChange.subscribe(
      event => doValueChange(event)
    );
    }
...
  • Related