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.
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();
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.
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)
}
}
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:
I'm converting Subject to Observable so that the subject is available as read-only to methods outside Crud Service.
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.
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
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();
}
Related
I have a form that is used to choose customer details. It is used in more than one places in my app but with extended logic and UI.
I would like to be able to reuse my basic component in other components but also to extend(or override) it's template and logic. To don't have to copy paste the same UI & logic in each component and have one common basic place for changes.
base component with template & logic:
#Component({
selector: 'app-base',
template: `<h1> Base component </h1>
<label>getLabel() </label>
<ng-content> </ng-content> `
})
export class BaseComponent implements OnInit {
constructor() {
// base init
}
ngOnInit(): void {
// base ngOnInit
}
getLabel(): string {
return "label";
}
}
Parent component that is suppose to reuse template, being able to extend methods and lifehooks and extend the UI as well.
#Component({
selector: 'app-parent',
template: `<app-base> <h1> extended </h1> </app-base>`,
})
export class ParentComponent extends BaseComponent {
constructor() {
super();
}
ngOnInit(): void {
super.ngOnInit();
// extended fancy stuff
}
getLabel(): string {
let a = super.getLabel();
return a + "a"; // extended logic
}
}
It sort of works using this approach but one problem that I noticed is that the ngOnInit is called twice. Once from the base component since I'm using its template in parent component and second time because of parentComponent ngOnInit being triggered.
Question:
Is there a way to reuse the template like using <app-base></app-base> with all shared logic in one place but without actually executing the lifehooks twice from both components?
Solution(?):
The only thing that I came up is to wrap the basic component logic with a service. So there will be something like this:
#Component({
selector: 'app-base',
template: `<h1> Base component </h1>
<label>getLabel() </label>
<ng-content> </ng-content> `})
export class BaseComponent implements OnInit {
constructor(IBasicService basicService) { // <-- injected service
// base init
}
ngOnInit(): void {
this.basicService.onInit(); // <-- using ngOnInit from service
// base ngOnInit
}
getLabel(): string {
return this.basicService.getLabel(); <-- using getLabel from service
}
}
With this approach I'll be able to simply reuse my base component but with different injected services that will implement IBasicService interface and also simply extend the UI using or but is this correct way? Isn't there any better solution?
Thanks in advance!
Is there a way to call a child class method from an abstract service? When I do this, the if statement doesn't execute because onInit doesn't exist. I am not sure why it doesn't exist though. Maybe there is an "angular" way of doing this instead of triggering this on the init. Or maybe I just need to call the onInit manually in the component instead. Basically what I am doing is trying to get the initial application data from the server.
#Injectable({providedIn:'root'})
export class RootService {
public constructor(httpClient: HttpClient) {
if(typeof this.onInit === 'function') {
this.onInit()
}
}
}
#Injectable({providedIn:'root'})
export class MyService extends RootService {
public onInit() {
// Do stuff
}
}
#Component()
export MyComponent {
public constructor(myService: MyService) {}
}
Services do not have lifecycle events. However, components have lifecycle hooks such as:
ngOnChanges()
ngOnInit()
ngDoCheck()
...
So you can load data when your component is initialized. If yes, then just use ngOnInit:
import { Component, OnInit } from '#angular/core';
#Component()
export MyComponent implements OnInit {
yourData;
public constructor(myService: MyService) {}
ngOnInit(){
myService.loadData()
.subscribe(s=> yourData = s);
}
}
According to Angular docs:
OnInit is a lifecycle hook that is called after Angular has
initialized all data-bound properties of a directive.
If you add the service to the providers of your component, it will be in your components scope then you can call onInit in your service as well.
But the downside of this is you can no longer share the same instance of the service among your components.
This will be valid if your service only serves one component.
I am trying to write a service in which some events are triggered via respective methods in it so that any component in my application can listen to it.
So, I created a custom event in a service as follows
export class EventsService {
#Output() expandNav: EventEmitter<Number> = new EventEmitter();
trigExpandNav() {
this.expandNav.emit(1);
}
constructor() { }
}
Then I included this service as a provider in my component in which I wanted to emit this event via the trigExpandNav method as follows :
import {
Component,
OnInit,
Output,
EventEmitter
} from '#angular/core';
import {
EventsService
} from '../../services/events.service';
#Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.css'],
providers: [EventsService]
})
export class HeaderComponent implements OnInit {
eventsService: EventsService;
fun() {
this.eventsService.trigExpandNav();
}
constructor() {}
ngOnInit() {}
}
But I am getting this error
ERROR in src/app/layout/header/header.component.ts(21,7): error TS2349: Cannot invoke an expression whose type lacks a call signature. Type 'EventEmitter<Number>' has no compatible call signatures.
I don't know where I am doing it wrong, so please help and Thanks in advance.
IMPORTANT
What I am trying to achieve is :
Suppose I have 2 components. I want to trigger an event from 1st component when a button or link is clicked and listen that event in 2nd component from the html file like this sda
For that I made a service in which I am emitting the event through a method. The thing is it's working as expected.
You shouldn't be using event emitters for Service's this is a better place for a RxJS BehaviorSubject or Subject. depending on your need.
#Ouput()s and EventEmitters are for Components only
So your service should become something along the lines of:
export class EventsService {
private expandNavSubject = new BehaviorSubject<number>(null);
trigExpandNav() {
this.expandNavSubject.next(1);
}
get expandNav$(){
this.expandNavSubject.asObservable();
}
constructor() { }
}
}
then in your header component inject the service via the constructor with:
constructor(private eventsService: EventsService){}
and call the trigger expand nav function with this.eventsService.trigExpandNav(); in your fun() function
I am having trouble trying to get the queryparams into a component. For now, I just want to console.log(...) it.
I am using the ActivatedRoute from #angular/router for this task.
I am redeveloping a certain platform for work so unfortunately some irrelevant code will have be to substituted with "..."
My Component.ts code:
import { Component, OnInit, ViewEncapsulation } from '#angular/core';
import { RelevantReportService } from './../reportServices/relevantReports.service';
import { ActivatedRoute ,Params, Router } from '#angular/router';
#Component({
selector: 'vr-reports',
templateUrl: './reports.component.html',
styleUrls: ['./reports.component.scss'],
providers: [RelevantReportService],
encapsulation: ViewEncapsulation.None
})
export class ReportsComponent implements OnInit {
reportSections: any;
constructor( private relevantReportService: RelevantReportService,
private router: Router,
private activatedRoute : ActivatedRoute
) { }
ngOnInit() {
...
console.log(this.activatedRoute.queryParams.value.reportName)
// console.log(this.activatedRoute.queryParams._value.reportName)
}
...
}
When I do console.log(this.activatedRoute.queryParams.value.reportName), the console spits out the queryparams (which is exactly what I wanted) HOWEVER it also says
"Property 'value' does not exist on type 'Observable' "
so I believe this not the correct way of tackling it.
It's observable in order to be able to monitor for changes in the params (by subscribing to observable). To get currently passed query params use:
this.activatedRoute.snapshot.queryParams
You could also use ActivatedRouteSnapshot instead of ActivatedRoute
Nothing surprising there!
activatedRoute.queryParams is an observable, and therefore you need to subscribe to it as per https://angular.io/api/router/ActivatedRoute#queryParams
You need to do the following :
ngOnInit() {
this.activatedRoute.queryParams.subscribe(values => {
console.log(values);//Which will print the properties you have passed
});
}
For Angular5 i would say the best option is using URL tree.
Since a router state is a tree, and the URL is nothing but a serialized state, the URL is a serialized tree. UrlTree is a data structure that provides a lot of affordances in dealing with URLs
Details
https://angular.io/api/router/UrlTree
In Angular 2 are there any specific pitfalls regarding memory management, I should be aware of?
What are the best practices to manage the state of components in order to avoid possible leaks?
Specifically, I've seen some people unsubscribing from HTTP observables in the ngOnDestroy method. Should I always do that?
In Angular 1.X I know that when a $scope is destroyed, all listeners on it are destroyed as well, automatically. What about observables in Angular 2 components?
#Component({
selector: 'library',
template: `
<tr *ngFor="#book of books | async">
<td>{{ book.title.text }}</td>
<td>{{ book.author.text }}</td>
</tr>
`
})
export class Library {
books: Observable<any>;
constructor(private backend: Backend) {
this.books = this.backend.get('/texts'); // <-- does it get destroyed
// with the component?
}
};
As requested by #katspaugh
In your specific case there's no need to unsubscribe manually since that's the Async pipe's job.
Check the source code for AsyncPipe. For brevity I'm posting the relevant code
class AsyncPipe implements PipeTransform, OnDestroy {
// ...
ngOnDestroy(): void {
if (isPresent(this._subscription)) {
this._dispose();
}
}
As you can see the Async pipe implements OnDestroy, and when it's destroyed it checks if is there some subscription and removes it.
You would be reinventing the wheel in this specific case (sorry for repeating myself). This doesn't mean you can't/shouldn't unsubscribe yourself in any other case like the one you referenced. In that case the user is passing the Observable between components to communicate them so it's good practice to unsubscribe manually.
I'm not aware of if the framework can detect any alive subscriptions and unsubscribe of them automatically when Components are destroyed, that would require more investigation of course.
I hope this clarifies a little about Async pipe.
You do not have to unsubscribe from standard subscriptions like after http.get().
But you DO have to unsubscribe from subscription on your custom Subjects. If you have some component and inside it you subscribing to some Subject in your service, then every time you showing that component new subscription will be added to the Subject.
Please check this out: Good solution to make your components 'clean'
My personal approach - all my components are extended from this nice class:
import { OnDestroy, OnInit } from '#angular/core';
import { Subject } from 'rxjs/Subject';
/**
* A component that cleans all subscriptions with oneself
* https://stackoverflow.com/questions/38008334/angular-rxjs-when-should-i-unsubscribe-from-subscription
* #class NeatComponent
*/
export abstract class NeatComponent implements OnDestroy, OnInit {
// Add '.takeUntil(this.ngUnsubscribe)' before every '.subscrybe(...)'
// and this subscriptions will be cleaned up on component destroy.
protected ngUnsubscribe: Subject<any> = new Subject();
public ngOnDestroy() {
this.ngUnsubscribe.next();
this.ngUnsubscribe.complete();
}
public ngOnInit(){}
}
And I just adding super() call to constructor and .takeUntil(this.ngUnsubscribe) before every subscribe:
import { NeatComponent } from '../../types/neat.component';
#Component({
selector: 'category-selector',
templateUrl: './category-selector.component.pug'
})
export class CategorySelectorComponent extends NeatComponent {
public constructor(
private _shopService: ShopsService
) { super(); }
public ngOnInit() {
this._shopService.categories.takeUntil(this.ngUnsubscribe)
.subscribe((categories: any) => {
// your code here
})
}
}