nageeb
nageeb

Reputation: 2042

How can I reproduce the Angular JS .broadcast() / .on() behaviour in Angular?

I'm working on rewriting an AngularJS application in Angular8. I have read of the different ways to communicate between components but can't seem to find the proper way to achieve this given my current requirements.

I currently have 2 sibling components which both use a common service that handles basic crud functionality. The form component calls a create method in the service and the list component calls a fetch method.

<page-component>
  <form component></form component>
  <list-component></list-component>
</page-component>

In AngularJS this would have been achieved by using a $scope.broadcast() and $scope.on() which is the effect that I'm looking to reproduce.

What I'm trying to figure out is the best way for the form component to emit an event to which the list component would (i assume) subscribe in order to tell it to refresh itself.

To be clear, I don't want to pass the updated values to the list component. I would like to simply communicate to it that its dataset has been updated and that it needs to re-fetch its records.

I have tried using an @Output in my form component:

export class FormComponent implements OnInit {

   @Output() valueChange = new EventEmitter();

   onUpdate() { 
     this.valueChange.emit(true);
   }
...
}

But I'm confused on how to implement the event listener as all the documentation I have read seems to point to events passed between parents and children and not to siblings as it were. Also the examples I have seen all seem to be focused on passing the actual data to the component instead of simply having it watch for an event and do the work on its own, as I require.

I have also seen methods that use @Input which require parameters to be passed into them on declaration. Somehow I feel that these components should be able to work on their own and not depend on this.

Upvotes: 3

Views: 185

Answers (6)

Tet Nuc
Tet Nuc

Reputation: 29

When I was migrating my app from AngularJS to Angular 2+. I choose to use RxJS, super easy to use, but there are learning curve.

You create service and RxJS BehaviorSubject. Any component can now subscribe for changes if needed, and any component can send updated data using .next() method.

This is your service:

import {Injectable} from "@angular/core";
import { BehaviorSubject } from "rxjs/BehaviorSubject";


    @Injectable()
    export default class yourService {
        valueChange = new BehaviorSubject<object>({});
      //add service functions if needed
      ...
    }

This is your component:

import { Component, OnInit, OnDestroy } from "@angular/core";
import YourService from "../path to your service";

@Component({
  selector: "name of component",
  template: require("your.html")

})
export class nameOfComponent implements OnInit, OnDestroy {

  private sub;
  privat val = "Your value";

  constructor( private YourService: YourService) {
  }

  ngOnInit() {
    this.sub = this.YourService.valueChange.subscribe( (value) => {
      this.YourService.valueChange.next(this.val); // If you need to pass some value to the service.
      this.val = value;
    })
  }
  ngOnDestroy() {
    this.sub.unsubscribe();
  }

Upvotes: 0

Shravan
Shravan

Reputation: 1202

Have a look at the Rxjs Subjects. Subjects makes the inter-components communication easier.

Since Forms and List component use a common CRUD service in your case, you can have a subject (say, newRecordSubject) and push any new records created into that subject. Now the List component should subscribe to this subject to be notified of any new records whenever it is created by the Forms component.

Have a look at the sample CRUD service below. I beleieve everything is very much self explainatory from the code snippets below.

crud.service.ts

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

import { Record } from './record.model';


@Injectable({providedIn: 'root'})
export class CrudService {
    private newRecordSubject = new Subject<Record>();

    get newRecordListener() {
        return this.newRecordSubject.asObservable();
    }

    public insertRecord(record: Record): void {
        // logic to pass the record to the backend
        this.newRecordSubject.next(record);
    }

    public fetch(): Record[] {
        // logic to fetch the inital records from the backend
        return [];
    }
}

Now in your List component, inject this CRUD service and subscribe to the newRecordsSubject as below.

list.component.ts

import { OnInit, Component, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';

import { CrudService } from './crud.service';
import { Record } from './record.model';

@Component({
    selector: 'list-component',
    template: ''
})
export class ListComponent implements OnInit, OnDestroy {
    records: Record[];

    private sub: Subscription;

    constructor(private service: CrudService) {}

    ngOnInit() {
        this.records = this.service.fetch();
        this.sub = this.service.newRecordListener.subscribe(record => this.records.push(record));
    }

    ngOnDestroy() {
        if (this.sub) {
            this.sub.unsubscribe();
        }
    }
}

Note:

  1. I'm converting Subject to Observable so that the subject is available as read-only to methods outside Crud Service.

  2. It is always a best practice to unsubscribe to the Subscriptions of Subjects and Observables, to avoid memory leaks.

Hope this helps! Cheers and Happy Coding.

Upvotes: 1

Ahamed Ishak
Ahamed Ishak

Reputation: 1062

Create a @Input property in your ListComponent and pass all the items that list component will render from PageComponent. Whenever create component emit a value fetch the new list in PageComponent and update passed property.

Implement the OnChange lifecycle hook in ListComponent and capture the property change in ngOnChange(changes) method.

If you don't like to pass the list from PageComponent then there are few options that can be used to solve the issue.

Option 1:

instead of passing a list:Array<any> pass a list:Observable<Array<any>> and subscribe the observable in your ListComponent so technically you http call will happen inside list component. But you have to reassign the list with new Observable<Array<any>> everytime FormComponent emit a value so ngOnChange will notified.

Option 2 :

You can pass a Subject to ListComponent as a propery and Subscribe to subject in ListComponent. Then you can fetch the product in ListComponent. When ever your FormComponent emit value call next() method in subject you passed. Every time you call next method your subscribles will notify and you can fetch the details from API.

reference links

https://angular.io/api/core/OnChanges

https://angular.io/api/core/Input

@Component({
  //Component Metadata
})
export class ListComponent implement OnChange{

  @Input() listOfAny:Array<any>=[];


  ngOnChanges(changes: SimpleChanges) {
   this.listOfAny =changes.listOfAny.currentValue
  }

}

@Component({
      //Component Metadata
    })
    export class FormComponent {

      @Output() valueChange = new EventEmitter();

       onUpdate() { 
         this.valueChange.emit(true);
       }

    }


@Component({
  //Component Metadata

   template:` 
  <page-component>
       <form component (valueChange)="valueChanges($event)" ></form component>
       <list-component [listOfAny]="list"></list-component>
  </page-component>`
})
export class PageComponent {

   list=[]

  valueChanges(newValue){
    // you can fetch items from API if needed.

    this.list.push(newValue)
  }

}

Upvotes: 1

Barremian
Barremian

Reputation: 31125

I believe there are multiple ways to achieve this.

In case of unrelated components, like the sibling components, one method would be to use a Subject Observable of RxJS module in the common service to which Form component will push a new value that can be subscribed to by the List component.

Service:

import { Subject } from 'rxjs/Subject';

@Injectable()
export class SampleService {
  public triggerSource = new Subject<boolean>();
  public trigger$ = this.triggerSource.asObservable();
}

Form Component:

import { SampleService } from '../services/sample.service';

@Component({
  selector: 'form-component',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.css'],
})
export class FormComponent implements OnInit {
  constructor(private _sampleService: SampleService) {
  }

  private setTriggerState(state: boolean) {
    this._sampleService.triggerSource.next(state);
  }
}

List Component:

import { SampleService } from '../services/sample.service';

@Component({
  selector: 'list-component',
  templateUrl: './list.component.html',
  styleUrls: ['./list.component.css'],
})
export class ListComponent implements OnInit {
  constructor(private _sampleService: SampleService) {
    this._sampleService.trigger$.subscribe(value => {
      if (value) {
         // value is true (refresh?)
      } else {
         // value is false (don't refresh?)
      }
    });
  }
}

And since we are returning an Observable, there are numerous operators to refine the outflow of data. For eg., in your case operator distinctUntilChanged() can be used to avoid pushing redundant information. Then the declaration of the observable would be

public trigger$ = this.triggerSource.asObservable().distinctUntilChanged();

Upvotes: 4

Hitech Hitesh
Hitech Hitesh

Reputation: 1635

You can create a function in parent that sets the value in the child component that you need. In the component


<page-component>
  <form component (value change)="formValueFunction($event)" emitedformValue="formEmitiedValue"></form component>
  <list-component (list change)="listValueFunction($event)" emitedListValue="listEmitedValue"></list-component>
</page-component>

In the component you can set the value to the other components like this

formEmitiedValue:any;
listEmitedValue:any;

formValueFunction(event){
this.formEmitiedValue= event;
}
listValueFunction(event)
{this.listEmitedValue=event;

}

check the event value using logs and if there are changes in form input use OnChange event hook in the components

Upvotes: 0

qiAlex
qiAlex

Reputation: 4346

Answer to this question could be a topic for a nice holywar, you know )

My opinion based on experience is in following:

Let's assume user typing something and than performs refresh page. Ask ourself what should happen?

If User's changes should recover to "before editing" state - than the parent component is enough to share such events.

If User's changes should be as it was before refresh page action, so if you need to store it localStorage or database or elsewhere, than it's better to involve services for it.

Upvotes: 1

Related Questions