Our AngularJS project had start it's long way to the modern Angular.
The ngMigration util recommend me to remove all the $rootScope dependecies because Angular doesn't contain a similar concept like $rootScope. It is pretty simple in some cases but I don't know what to do with event subscription mechanisms.
For example I have a some kind of Idle watchdog:
angular
.module('myModule')
//...
.run(run)
//...
function run($rootScope, $transitions, Idle) {
$transitions.onSuccess({}, function(transition) {
//...
Idle.watch(); // starts watching for idleness
});
$rootScope.$on('IdleStart', function() {
//...
});
$rootScope.$on('IdleTimeout', function() {
logout();
});
}
On which object instead of $rootScope I have to call the $on function if I want to get rid of the $rootScope?
UPD
The question was not about "how to migrate on Angular2 event system". It was about how to remove a $rootScope dependencies but keep a event system. Well it seems to be impossible.
I don't know what to do with event subscription mechanisms.
Angular 2+ frameworks replace the $scope/$rootScope event bus with observables.
From the Docs:
Transmitting data between components
Angular provides an EventEmitter class that is used when publishing values from a component. EventEmitter extends RxJS Subject, adding an emit() method so it can send arbitrary values. When you call emit(), it passes the emitted value to the next() method of any subscribed observer.
A good example of usage can be found in the EventEmitter documentation.
For more information, see
Angular Developer Guide - Observables in Angular
You can implement TimeOutService which will do the log out after x minutes (in this case 15 min) of inactivity or it will reset the timer after certain action(s).
import { Injectable, OnDestroy } from '#angular/core';
import { Router } from '#angular/router';
import { Observable, Subject, Subscription, timer } from 'rxjs';
import { startWith, switchMap } from 'rxjs/operators';
import { AuthService } from 'path/to/auth.service';
#Injectable()
export class TimeoutService implements OnDestroy {
limitMinutes = 15;
secondsLimit: number = this.limitMinutes * 60;
private reset$ = new Subject();
timer$: Observable<any>;
subscription: Subscription;
constructor(private router: Router,
private authService: AuthService,
) {
}
startTimer() {
this.timer$ = this.reset$.pipe(
startWith(0),
switchMap(() => timer(0, 1000))
);
this.subscription = this.timer$.subscribe((res) => {
if (res === this.secondsLimit) {
this.logout();
}
});
}
resetTimer() {
this.reset$.next(void 0);
}
endTimer() {
if (typeof this.subscription !== 'undefined') {
this.subscription.unsubscribe();
}
}
logout(): boolean {
this.authService.signOut().subscribe((res) => {
this.subscription.unsubscribe();
});
return false;
}
ngOnDestroy():void {
this.subscription.unsubscribe();
}
}
And in the AppComponent have listener which will reset timeout on certain actions
In case as bellow it listens for keyboard strokes, mouse wheel, or mouse click
constructor(
private timeoutService: TimeoutService
) {
}
#HostListener('document:keyup', ['$event'])
#HostListener('document:click', ['$event'])
#HostListener('document:wheel', ['$event'])
resetTimer () {
this.timeoutService.resetTimer();
}
Related
I want to test my service in angular with karma and jasmine, I begin with unit tests and I don't found any solution for my case or because I don't know how I can fix the issue. I always have a problem with my fields which are often undefined I don't understand why. If you can explain me what is the real problem It will be very nice to be able to progress in my tests.
My service
import { Injectable } from '#angular/core';
#Injectable({
providedIn: 'root'
})
export class CanvasElementService {
public canvasContainer: HTMLDivElement;
private scaleProperty: string = 'scale(1)';
public updateScale(scale: number) {
this.scaleProperty = `scale(${scale})`;
this.update();
}
public update(): void {
this.canvasContainer.style.transform = this.scaleProperty;
}
}
And my test
import { Component } from '#angular/core';
import { TestBed } from '#angular/core/testing';
import { CanvasElementService } from './canvas-element.service';
fdescribe('CanvasElementService', () => {
let service: CanvasElementService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(CanvasElementService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
describe("updateScale", () => {
it("updatescale 2", () => {
let scale = 3;
spyOn(service, 'update').and.callThrough();
service.updateScale(scale);
expect(service['scaleProperty']).toBe('scale(3)');
});
});
});
After run test I got :
thanks for help If you have any idea to fix it.
Im not quite sure how to access a htmlElement in services. I think its only meant to be used by components (which have access to elements via #ViewChild() decorator).
You can try to achieve it by injecting your document into the service constructor and search for your element. Something like this:
constructor(#Inject(DOCUMENT) private document: HTMLDocument) {}
But its really not recommend to use services to access your dom. Use components or directives for it.
https://medium.com/#smarth55/angular-bad-practices-patterns-service-that-touch-the-dom-57ea978dca92
Angular's Location service has a method onUrlChange that registers url events that popstate or hashchange don't, and I need that for a part of my project.
/**
* Registers a URL change listener. Use to catch updates performed by the Angular
* framework that are not detectible through "popstate" or "hashchange" events.
*
* #param fn The change handler function, which take a URL and a location history state.
*/
onUrlChange(fn: (url: string, state: unknown) => void) {
this._urlChangeListeners.push(fn);
this.subscribe(v => { this._notifyUrlChangeListeners(v.url, v.state); });
}
Other than usually, there's no subscription returned, so we can't unsubscribe on destroy. The listener is still intact after navigating away from the route that needs to listen to those events.
My ugly hack for the moment is to filter Locations private _urlChangeListeners onDestroy, but that relies on String(fn) !== '(change) => this.urlFileHandler(change)' and clearly isn't a nice way.
Is there any other possibility to remove that listener from the listeners?
Not really an answer to the question but I decided to subscribe to it once and use an observable. For example:
import { Location } from '#angular/common';
export class MyService {
public urlChanged = new Subject();
constructor(private location: Location) {
// This is a shared service so the code only gets called once
location.onUrlChange((url, state) => {
this.urlChanged.next({ url, state });
});
}
}
Then subscribe to it in the normal way, for example:
private sub;
ngOnInit() {
this.sub = this.myService.urlChanged.subscribe(e => {
//Do stuff
});
}
ngOnDestroy() {
this.sub.unsubscribe();
}
Since I only use one event suscription, I used this when I need to remove it:
(this.location as any)._urlChangeListeners = [];
So, I probably don't understand Observables well. I have snippet like this, and would like to access todos stored in this service via function (from another component) defined in service. Unfortunately, I have no idea how to do this.
todos;
// fetching todos from api
fetchTodos() :Observable<Todo[]>{
return this.http.get<Todo[]>(api_url);
}
constructor(private http:HttpClient) {
this.fetchTodos()
.subscribe(data => this.todos = data)
}
To do it right solve your problem as follows.
SERVICE
import { BehaviorSubject, Observable } from 'rxjs';
/* make the plain object a subject you can subscribe to */
todos: BehaviorSubject<Todo[]> = new BehaviorSubject<Todo[]>([]);
constructor(private http:HttpClient) {
/* get the data and put it in the subject */
/* every subscriber to this event will be updated */
this.fetchTodos().subscribe(data => this.todos.next(data));
}
getTodos(): Observable<Todo[]> {
return this.todos.asObservable();
}
// fetching todos from api
private fetchTodos(): Observable<Todo[]> {
return this.http.get<Todo[]>(api_url);
}
COMPONENT
constructor(private service: Service) {}
ngOnInit(): void {
/* here you go. this subscription gives you the current content of todos[] */
/* whenever it gets updated */
this.service.getTodos().subscribe(data => {
console.log(data);
});
}
PLEASE NOTE
Subscriptions to an Observable should always be finished when you leave a component. The best way to reach this goal in your case is:
modified COMPONENT
import { Subscription } from 'rxjs';
private subscription: Subscription = new Subscription();
constructor(private service: Service) {}
ngOnInit(): void {
/* add each subscriber to the subscription */
this.subscription.add(
this.service.getTodos().subscribe(data => {
console.log(data);
});
);
}
ngOnDestroy(): void {
/* unsubscribe all subscribers at once */
this.subscription.unsubscribe();
}
When subscribing to changes in an Angular Abstract Control using valueChanges, is it necessary to unsubscribe()?
I often do this:
// this.form is a FormGroup within a Component.
this.form.valueChanges.subscribe(_ => {
console.log(this.form.value);
});
But should I be managing the subscription myself (as I do with ngrx in general)?:
import { Subscription } from 'rxjs';
// this.subscription is ngrx Subscription.
this.subscription = this.form.valueChanges.subscribe(_ => {
console.log(this.form.value);
});
public ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
The only reason I have not done this previously is because tutorials, examples and documentation on Angular Forms generally omit storing a reference to the subscription, and instead, just use valueChanges as is.
Conversely, ngrx tutorials seem to highlight the importance of unsubscribing to avoid memory leaks.
Yes it is necessary, but you could use take until instead.
private unsubscribe$: Subject<void> = new Subject<void>();
this.subscription = control.valueChanges
pipe(takeUntil(this.unsubscribe$))
.subscribe(_ => {
console.log(this.form.value);
});
ngOnDestroy() {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
https://medium.com/#benlesh/rxjs-dont-unsubscribe-6753ed4fda87
Looking at this authguard which is called from canactivate :
#Injectable()
export class AuthGuard implements CanActivate {
constructor(private loginServicePOST:LoginService, private router:Router) { }
canActivate(next:ActivatedRouteSnapshot, state:RouterStateSnapshot) {
return this.loginServicePOST({...}).map(e => {
if (e) {
return true;
}
}).catch(() => {
return Observable.of(false);
});
}
}
This code is working and an http request is invoked to server.
Question :
This is a cold observable and no one .subscribes to it - so I don't understand how this post request is invoked and why.
subscribe must be written IMHO.
NB
I already know that canactivate can return bool/Promise<bool>/Observable<bool>
The router is subscribing to the observable returned by canActivate which invokes the observable returned by loginService(...).map(...)