Objective:
I'm working with a service. The objective is to create a service that will serve as a creation for confirmation dialog and will return whether confirm or cancel was clicked in the popup.
Code:
product.component.html
<button class="edit-button" (click)="confirmFeatureProductDialog(data)">Confirm</button>
product.component.ts
confirmFeatureProductDialog(product){
let message = "Do you want to set this product?";
let result;
this._popupDialogService.confirmationPopup(message).subscribe(
(res)=>{
console.log("Running")
result = res;
if(result){
this.featureProduct(product._id);
}
}
);
}
popupDialog.service.ts
import { Injectable } from "#angular/core";
import { Observable, Subject } from "rxjs";
import Swal from 'sweetalert2'
Injectable();
export class PopupDialogService {
public mySubject = new Subject<boolean>();
confirmationPopup(textDetail?): Observable<any> {
Swal.fire({
title: 'Are you sure?',
text: textDetail,
icon: 'success',
showConfirmButton: true,
showCancelButton: true,
}).then((result) => {
if (result.value) {
this.mySubject.next(true);
return;
}
Swal.close();
});
return this.mySubject.asObservable();
}
}
Result & Issue:
When I run this the first time everything seems to be working fine.
When I click on the button the 2nd time after clicking either cancel or confirm the response from the product.component.ts is duplicated twice i.e the console.log from the subscribe response is repeated twice and console.log shows "Running" "Running".
When I click the button third time (once the dialog closes after clicking confirm/cancel) the response is repeated thrice and console shows "Running" 3 times.
Things I've tried so far:
Add the service to app.module.ts provider instead of product.module.ts providers. Doesn't change anything.
Checked if the service's constructor is called multiple times on 2nd and onwards execution. It doesn't execute multiple times. Only the subscription response is executed multiple times.
Summary:
Running subscription on a function returning observable from service. First time the response of the subscription is okay however when the function is re-opened the 2nd time it throws the response twice, after closing and re-running the function gives the response thrice and so on.
You are subscribing to the same subject multiple times without unsubscribing from the previous subscription.
Subscribe to the subject on init and remove the asobservable return from the service
// component code
ngOnInit() {
let result;
this._popupDialogService.mySubject.subscribe(
(res)=>{
console.log("Running")
result = res;
if(result){
this.featureProduct(product._id);
}
}
);
}
confirmFeatureProductDialog(product){
let message = "Do you want to set this product?";
this._popupDialogService.confirmationPopup(message)
}
// Service code
confirmationPopup(textDetail?) {
Swal.fire({
title: 'Are you sure?',
text: textDetail,
icon: 'success',
showConfirmButton: true,
showCancelButton: true,
}).then((result) => {
if (result.value) {
console.log(result)
this.mySubject.next(true);
return;
}
Swal.close();
});
}
It cause by every time you clicked, You have create new Subscription to mySubject on PopupDialogService
Solution: You need to move subscription part to ngOnInit of the component to prevent the subscription duplicated.
.component.ts
export class AppComponent implements OnInit, OnDestroy {
sub: Subscription;
constructor(private _popupDialogService: PopupDialogService) {}
ngOnInit() {
this.sub = this._popupDialogService.mySubject.subscribe((res) => {
console.log('Running');
// result = res;
// if (result) {
// this.featureProduct(product._id);
// }
});
}
ngOnDestroy() {
this.sub.unsubscribe();
}
confirmFeatureProductDialog() {
let message = 'Do you want to set this product?';
let result;
this._popupDialogService.confirmationPopup(message);
}
}
Here is example: stackblitz
this issue is occurs because of observer of this._popupDialogService.confirmationPopup(message)
you need to declare in product component for subscription:
subscription!: Subscription
confirmFeatureProductDialog(product){
let message = "Do you want to set this product?";
let result;
this.subscription =this._popupDialogService.confirmationPopup(message).subscribe(
(res)=>{
console.log("Running")
result = res;
if(result){
this.featureProduct(product._id);
}
}
);
}
in ondestory lif cycle hook just unsubscribe it.
ngOnDestroy() {
this.subscription.unsubscribe()
}
try to change your method like:
sub: Subscription;
confirmFeatureProductDialog(product){
let message = "Do you want to set this product?";
let result;
this.sub = this._popupDialogService.confirmationPopup(message).subscribe(
(res)=>{
console.log("Running")
result = res;
if(result){
this.featureProduct(product._id);
}
this.subs.unsubscribe();
}
);
}
That it could fix your issue, but I think in your case you need a different approach to the problem. Maybe it's better using a service like that:
export class DialogService {
dialogConfig: EventEmitter<DialogConfig> = new EventEmitter(null);
afterClosed: EventEmitter<DialogResponse> = new EventEmitter(null);
constructor() { }
/** OPEN DIALOG DEFINED IN APP.COMPONENT.HTML
*
* #title define the optional title of a dialog (title to translate with pipe translate)
* #action action define the action component to use
* #message define the message of a dialog (message to translate with pipe translate)
*
*/
open(title: string, action: string, message?: string, params?: Object): EventEmitter<DialogResponse> {
this.afterClosed = new EventEmitter(null);
this.dialogConfig.emit({
visibile: true,
title,
action,
message,
params
});
return this.afterClosed;
}
/** CLOSE DIALOG DEFINED IN APP.COMPONENT.HTML */
close(res: boolean, note?: string) {
this.dialogConfig.emit({ visibile: false });
note ? this.afterClosed.emit({ response: res, note: note }) : this.afterClosed.emit({ response: res });
}
}
used in your components like this for open:
this.dialog.open("title", "action", "message").subscribe(res => {
...stuffs
})
and for close:
this.dialog.close(true | false)
Related
I need to maintain an alert box on the Registration page indicating the user has registered successfully. However, by redirecting to the Login form this box disappears, because the page refreshes.
I utilize the Alert component to manage this scenario. All of the features work flawlessly but this problem really makes me confused. I shared my code and hope you assist me in getting to the root of this predicament.
alert.component.ts
import { Component, OnInit, OnDestroy, Input } from '#angular/core';
import { Router, NavigationStart } from '#angular/router';
import { Subscription } from 'rxjs';
import { Alert, AlertType } from 'src/app/_models/alert';
import { AlertService } from 'src/app/_services/alert.service';
#Component({ selector: 'alert',
templateUrl: 'alert.component.html',
styleUrls: ['./alert.component.scss'] })
export class AlertComponent implements OnInit, OnDestroy {
#Input() id = 'default-alert';
#Input() fade = true;
alerts: Alert[] = [];
alertSubscription: Subscription;
routeSubscription: Subscription;
constructor(private router: Router, private alertService: AlertService) { }
ngOnInit() {
// subscribe to new alert notifications
this.alertSubscription = this.alertService.onAlert(this.id)
.subscribe(alert => {
// clear alerts when an empty alert is received
if (!alert.message) {
// filter out alerts without 'keepAfterRouteChange' flag
this.alerts = this.alerts.filter(x => x.keepAfterRouteChange);
// remove 'keepAfterRouteChange' flag on the rest
this.alerts.forEach(x => delete x.keepAfterRouteChange);
return;
}
// add alert to array
this.alerts.push(alert);
setTimeout(() => this.removeAlert(alert), 5000);
});
// clear alerts on location change
this.routeSubscription = this.router.events.subscribe(event => {
if (event instanceof NavigationStart) {
this.alertService.clear(this.id);
}
});
}
ngOnDestroy() {
// unsubscribe to avoid memory leaks
this.alertSubscription.unsubscribe();
this.routeSubscription.unsubscribe();
}
removeAlert(alert: Alert) {
// check if already removed to prevent error on auto close
if (!this.alerts.includes(alert)) return;
if (this.fade) {
// fade out alert
this.alerts.find(x => x === alert).fade = true;
// remove alert after faded out
setTimeout(() => {
this.alerts = this.alerts.filter(x => x !== alert);
}, 250);
} else {
// remove alert
this.alerts = this.alerts.filter(x => x !== alert);
}
}
cssClass(alert: Alert) {
if (!alert) return;
const classes = ['toast'];
const alertTypeClass = {
[AlertType.Success]: 'toast-success',
[AlertType.Error]: 'toast-error',
[AlertType.Info]: 'toast-info',
[AlertType.Warning]: 'toast-warning'
}
classes.push(alertTypeClass[alert.type]);
if (alert.fade) {
classes.push('fade');
}
return classes.join(' ');
}
}
alert.service.ts
import { Injectable } from '#angular/core';
import { Observable, Subject } from 'rxjs';
import { filter } from 'rxjs/operators';
import { Alert, AlertType } from '../_models/alert';
#Injectable({ providedIn: 'root' })
export class AlertService {
private subject = new Subject<Alert>();
private defaultId = 'default-alert';
// enable subscribing to alerts observable
onAlert(id = this.defaultId): Observable<Alert> {
return this.subject.asObservable().pipe(filter(x => x && x.id === id));
}
// convenience methods
success(message: string, options?: any) {
this.alert(new Alert({ ...options, type: AlertType.Success, message }));
}
error(message: string, options?: any) {
this.alert(new Alert({ ...options, type: AlertType.Error, message }));
}
info(message: string, options?: any) {
this.alert(new Alert({ ...options, type: AlertType.Info, message }));
}
warn(message: string, options?: any) {
this.alert(new Alert({ ...options, type: AlertType.Warning, message }));
}
// main alert method
alert(alert: Alert) {
alert.id = alert.id || this.defaultId;
this.subject.next(alert);
}
// clear alerts
clear(id = this.defaultId) {
this.subject.next(new Alert({ id }));
}
}
This is a piece of code in which an alert message is called (It should be noted that the keepAfterRouteChange is set to True):
onSubmit() {
this.submitted = true;
// reset alerts on submit
this.alertService.clear();
// stop here if form is invalid
if (this.form.invalid) {
return;
}
this.loading = true;
this.accountService
.register(this.form.value)
.pipe(first())
.subscribe((data) => {
this.loading = false;
this.submitted = false;
if (data.hasError) {
this.alertService.error(data.errorMessage);
} else {
this.alertService.success('Registration successfully completed.', {
keepAfterRouteChange: true,
});
localStorage.setItem('regCount',JSON.parse(localStorage.getItem('regCount')) + 1);
this.router.navigate(['/login']).then(() => {
window.location.reload();
});
}
},
() => {
this.loading = false;
this.submitted = false;
this.alertService.error('Something went wrong.');
});
}
Your problem probably comes from window.location.reload(); when window is reloaded all components and services are flushed. Find other ways to clear services if that's the point this line. Or find other way to store info that alert should be showing (e.g storing the need to show an alert with info and duration in SessionStorage or LocalStorage) - which doesn't seem like a good idea though. Normally we want to avoid reloading windows - for the same reason, losing all data and forcing the client to reload all resources.
I am trying to share data between components using the rxjs subject and i've used that data in component
Component.html
<div class="spinner-container" *ngIf="loading">
<div class="spinner-item">
<nx-spinner nxSize="large"></nx-spinner>
</div>
</div>
component.ts
ngOnInit(){
setTimeout(()=>{
this.commonService.spinnerTrigger.subscribe((trigger)=>{
this.loading = trigger;
})
},100)
}
Here is the error
ExpressionChangedAfterItHasBeenCheckedError: Expression has changed
after it was checked. Previous value: 'ngIf: false'. Current value:
'ngIf: true'.
I found a workaround using changedetectref but I don't think its good practice is ther any other way to solve this issue
You can manually trigger change detection using the detectChanges() method of the ChangeDetectorRef
Try like this:
import { ChangeDetectorRef} from '#angular/core';
constructor(private cdr: ChangeDetectorRef) { }
ngOnInit(){
setTimeout(()=>{
this.commonService.spinnerTrigger.subscribe((trigger)=>{
this.loading = trigger;
if (this.cdr && !(this.cdr as ViewRef).destroyed) {
this.cdr.detectChanges();
}
})
},100)
}
Making the next callback async worked for me once:
this.commonService.spinnerTrigger.subscribe(async (trigger) => {
this.loading = await trigger;
});
Or adding a zero delay:
this.commonService.spinnerTrigger.pipe(delay(0)).subscribe((trigger) => {
this.loading = trigger;
});
This is an open issue in Github,
Github issue => https://github.com/angular/angular/issues/15634
And they provided a workaround using setTimeout() for now and still there aren't any updates regarding this issue.
And also you can try changeDetector that may solve your issue.
import { ChangeDetectorRef } from '#angular/core';
constructor(private cdRef:ChangeDetectorRef) {}
ngAfterViewChecked()
{
this.cdRef.detectChanges();
}
I don't see any need here to mess around with change detection / setTimeout (which triggers change detection).
Stackblitz
Use a spinner service which parent and child can use.
spinner.service.ts
#Injectable()
export class SpinnerService {
private loading = new BehaviorSubject<boolean>(true)
loading$: Observable<boolean> = this.loading.asObservable()
setSpinner(bool: boolean) {
this.loading.next(bool)
}
}
Example - Component setting spinner
ngOnInit() {
this.service.getChildData().pipe(
// handle any errors
catchError(err => {
console.log('Error caught: ', err)
this.data = err
return throwError(err)
}),
// no matter what set spinner false
finalize(() => {
this.spinnerService.setSpinner(false)
}),
// subscription clean up
takeUntil(this.destroyed$)
).subscribe(data => this.data = data)
}
Example - parent / container displaying spinner
ngOnInit() {
this.loading$ = this.spinnerService.loading$
this.spinnerService.setSpinner(true) // if needed
}
<div *ngIf="loading$ | async">
I am a spinner
</div>
I am trying to implement a shared service for managing Roles on my app, with an Observable so that, from other components, you can either change the current role and be notified when it changes. The problem I have is that when I publish a new value through the service, the components that subscribe to the observable always recieve the same value (the initial one). Then, I never receive the new role number and I can't update the component state.
Apparently
I have the following set of components:
RolesService: The shared Service, which manages role change, gets the available roles from the user token, manages persistence of the current role for the logged in user. It uses localStorage to persist the role index. It receives changes
HeaderComponent: This is an example of a component receiving changes for the role change, because it needs to update the title of the user. It subscribes to the observable and changes the title accordingly
EscullRolComponent: And this is an example of a component that changes the role the user is currently using (by action of the user, of course). It has some buttons and sends to the service the new index.
Here is the relevant code for this issue:
// RolesService file
#Injectable()
export class RolesService {
private _currentRole: BehaviorSubject<Rol> = new BehaviorSubject(null);
currentRole = this._currentRole.asObservable();
private get currentIndex(): number {
const ras = localStorage.getItem('current_role');
// console.log("Guardat aixo: ", ras);
if (ras === '' || ras === null || ras === 'NaN' || ras === '-1' || parseInt(ras, 10) === NaN) {
return 0;
} else {
return parseInt(ras, 10);
}
}
private set currentIndex(val) {
localStorage.setItem('current_role', val.toString());
}
currentToken: NbAuthJWTToken;
constructor(private authService: NbAuthService,
private http: HttpClient) {
// console.log(this.currentRole);
this.authService.getToken().subscribe((token: NbAuthJWTToken) => {
if (token.isValid()) {
this.currentToken = token;
console.log("Executing token change");
this.setRolActual(0);
}
});
}
protected publishCurrentRol(i: number): void {
console.log("Publishing rol id: ", i); // LOG 1
this._currentRole.next(this.getUserRoles()[i]);
}
setRolActual(i: number) {
this.publishCurrentRol(i);
this.currentIndex = i;
}
}
The following is the component the user has to change the role, and that calls the service with the new role.
#Component({
templateUrl: 'escull-rol.component.html',
styleUrls: ['escull-rol.component.scss'],
})
export class EscullRolComponent {
rols: Array<Rol> = [];
actual: number;
constructor( private rolesService: RolesService,
private route: ActivatedRoute,
private router: Router,
private location: Location ) {
this.rols = this.rolesService.getUserRoles();
this.actual = this.rolesService.getRolActualIndex();
}
buttonRolClicked(index: number) {
this.rolesService.setRolActual(index);
this.router.navigate(['inici']);
// console.log('Boto del rol ' + index + ' clicat');
}
}
And here the header, which changes its state depending on the role:
#Component({
selector: 'ngx-header',
styleUrls: ['./header.component.scss'],
templateUrl: './header.component.html',
})
export class HeaderComponent implements OnInit {
#Input() position = 'normal';
user: any = {};
picture: string;
client: stream.Client;
logoPath = '';
logoEra = '';
rol: string;
ids: Array<string>;
constructor(private sidebarService: NbSidebarService,
/* ... more injections .. */
private imatgesService: ImatgesService,
private notificacionsService: NotificacionsService) {
this.logoEra = 'assets/images/logoEra.png';
this.authService.onTokenChange()
.subscribe((token: NbAuthJWTToken) => {
if (token.isValid()) {
if (token.getValue()) {
this.user = token.getPayload(); // Posem les dades del token a l'objecte user
// console.log('token changed, changing user in header', token);
}
}
}, error => {
console.error('Error en carregar el token en el header');
throw error;
});
this.rolesService.currentRole.subscribe((rol: Rol) => {
// This subscription here should receive the change from the service
if(rol) {
console.log("Changing rol on header to ", rol.getIndex()); // LOG 2
this.rol = rol.getTitle();
this.ids = rol.getRolIDs();
}
});
this.imatgesService.getProfileImagePath().subscribe((path: string) => {
this.picture = path;
}, (err) => {
this.picture = '';
});
}
}
The behaviour that I'm seeing is, the EscullRol component calling the setRolActual(id) method with the new id, and then the service calling its internal method publishCurrentRole(id) with the same id, so at LOG 1 I can see the expected outoput. But then immediately next I can see the output form LOG 2 at the Header Component with the wrong id, which is always the number that we had initially saved at the localStorage when the app started up.
I don't really know if the issue is with how I use the observables, with the service-component communication model or with how components and observables are initailsed and treated in angular.
Few thing to try
First make your service as a singleton using
#Injectable({ providedIn: "root" })
Improvement
Also, make sure that the service is not provided on child modules, as that would create their own instance and it wouldn't be a singleton anymore. If there are more than one instance of the service, the Subject-Observer pattern will not work across all the app.
Then this code
currentRole = this._currentRole.asObservable();
You should create a function to return the data not defined as an variable like
getCurrentRole() {
return this._currentRole.asObservable();
}
I am testing an angular app and especially this HTML input:
<form name="editForm" role="form" novalidate (ngSubmit)="save()" #editForm="ngForm">
<input type="text" name="nombre" id="field_nombre"
[(ngModel)]="paciente.nombre" required/>
(etc. f.e. button on submit...)
Here is my component:
imports....
export class PacienteDialogComponent implements OnInit {
paciente: Paciente;
....
save() {
this.isSaving = true;
if (this.paciente.id !== undefined) {
this.subscribeToSaveResponse(
this.pacienteService.update(this.paciente));
} else {
this.subscribeToSaveResponse(
this.pacienteService.create(this.paciente));
}
}
}
Here is my patient.model.ts
export class Paciente implements BaseEntity {
constructor(
public id?: number,
public nombre?: string,
public sexo?: Sexo,
.....
I want to test the form which means that on submit it is really calling teh save() function.
I have this in my test:
describe('Paciente Management Dialog Component', () => {
let comp: PacienteDialogComponent;
let fixture: ComponentFixture<PacienteDialogComponent>;
let debugElement: DebugElement; //create a debgElement for testing
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [OncosupTestModule,
OncosupSharedModule,
BrowserModule,
FormsModule,
],
declarations:...
],
providers: [
...
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PacienteDialogComponent);
comp = fixture.componentInstance;
debugElement = fixture.debugElement;
});
//a default generated test which controls if the save method really saves a new patient with its name, id, sex, etc.
it('Should call create service on save for new entity',
inject([],
fakeAsync(() => {
// GIVEN
const entity = new Paciente();
spyOn(service, 'create').and.returnValue(Observable.of(new HttpResponse({body: entity})));
comp.paciente = entity;
// WHEN
comp.save();
tick(); // simulate async
// THEN
expect(service.create).toHaveBeenCalledWith(entity);
expect(comp.isSaving).toEqual(false);
expect(mockEventManager.broadcastSpy).toHaveBeenCalledWith({ name: 'pacienteListModification', content: 'OK'});
expect(mockActiveModal.dismissSpy).toHaveBeenCalled();
})
)
);
// And teh second thing I want to test is if ngSubmit is really calling the save() function
it ('should call the onSubmit method', async(() => {
//fixture.detectChanges();
spyOn(comp,'save');
var1 = debugElement.query(By.css('button')).nativeElement;
console.log('print button ' + var1);
var1.click();
expect(comp.save).toHaveBeenCalledTimes(0);//verify...
}));
//And also if isSaving is set to true
it ('should set isSaving to true', async(() => {
comp.save();
expect(comp.isSaving).toBeTruthy();
}));
1.Now I have these questions: The first test is generated by default and not written by me. In this line const entity = new Paciente(); should I call parameters of Paciente? Like id, sex, name or leave it like this by default without parameters. Th epurpose of this first test if to check if really the save() function saves a patient and his data like id, sex, etc.
2.For the second test I read it in a tutorial of angular that: HaveBennCalled(0) is the right thing to test if this spy is called and how many times. But anyway does it really tests if the button calls the function save(). I think it only checks if thebutton havenĀ“t been called before, but not if it is callled right now in save function.
3.And are these 3 tests enough and complete for a form submitting?
Following my comments, here is how to test if a form is submitted correctly.
Let's say you have an interface Patient :
export interface Patient {
id: number;
name: string;
}
In your component, you have a form, and you submit it through submit() :
submit() {
this.patientService.savePatient(this.patient).subscribe(result => {
console.log('Patient created');
});
}
Now your service make the HTTP call and checks if the fields are okay :
savePatient(patient: Patient): Observable<any> {
if (typeof patient.id !== number) { return Observable.throw('ID is not a number'); }
if (typeof patient.name !== string) { return Observable.throw('Name is not a string'); }
return this.http.post<any>(this.url, patient);
}
Then your tests should look like this. First, the component :
it('Should call the service to save the patient in DB', () => {
// Spy on service call
// Expect spy to have been called
});
it('Should log a message on success', () => {
// Spy on console log
// Expect console log to have been called with a string
});
You can also test if the error is treated correctly, if you have error codes, etc.
Now in the service :
it('Should throw an error if the ID is not a number', () => {
// Mock a patient with a string ID
// Expect an error to be thrown
});
// Same thing for the name, you get the idea
it('Should make an HTTP call with a valid patient', () => {
// Spy on the HttpTestingController
// Expect the correct endpoint to have been called, with the patient as the payload
});
The general idea of those tests is to cover any case that could happen. This will allow you to prevent side effects : for instance, if one day you decide to pass your ID to string, the unit test will fail and tell you
You expect me to send a string but I pass only with a number
This is the purpose of a unit test.
I have a subject that is subscribed to and fires when a user searches.
let searchView;
this.searchSubject
.switchMap((view: any) => {
searchView = view;
this.http.post(this.url, view);
})
.subscribe(page => {
this.searchHistoryService.addRecentSearch(searchView).subscribe();
})
searchHistoryService.addRecentSearch records this search so the user can see their recent searches.
I don't think this is good practice as the observable is subscribed to everytime, I would rather use a subject which I'm calling .next() on, or combine the history call with the search call itself.
If searchHistoryService.addRecentSearch returns a Subject I can call .next() but where would I subscribe to it?
I tried adding this in the searchHistoryService's constructor
this.searchHistorySubject.do(observableIWantToCall()).subscribe()
and then replacing the subscription to 'addRecentSearch' with this:
this.searchHistoryService.searchHistorySubject.next(searchView)
But it doesnt work.
The inner observable, observableIWantToCall() gets called but the observable returned isnt subscribed to.
What's wrong with this and what is best practice for subscribing to an observable when another is finished emitting?
I think you can do something like this:
let searchView;
private searchHistorySubject$: Subject<any> = new Subject<any>();
constructor(){
this.searchHistoryService.addRecentSearch(searchView).first().subscribe(
response => {
//It will entry when you send data through next
},
error => {
console.log(error);
}
);
}
...
addRecentSearch(searchView) {
...
return this._searchHistorySubject$.asObservable();
}
setSearchHistoryEvent(value: any) {
this._searchHistorySubject$.next(value);
}
this.searchSubject
.switchMap((view: any) => {
searchView = view;
this.http.post(this.url, view);
})
.subscribe(page => {
this.searchHistoryService.setSearchHistoryEvent(searchView);
}
)