Cancel local requests on component destroy - angularjs

Consider you have some component e.g. autocomplete that sends GET request to server:
...
someObject = create();
vm.search = someFactory.getHttp(params).then(result => {
someObjet.prop = result;
});
vm.$onDestroy = () => {
someObject = null;
}
If component is destroyed while request is pending, callback will throw js error.
I know that in this concrete example I can solve this using simple If, however quite obvious that it is better to canel this request:
var canceler = $q.defer();
vm.search = someFactory.getHttp(params, canceler)...
vm.$onDestroy = () => {
canceler.resolve();
someObject = null;
}
This works perfectly, but having such code in each component seems weird. I would like to have something like:
vm.search = someFactory.getHttp(params, $scope.destroyPromise)
But such thing does not seem to exist...
Question: Is there any easy way to cancel requests on component destroy?
both in Angularjs or in Angular

One thing I've done in Angular is use RXjs for requests, using subscriptions, and adding those subscriptions to an array that we iterate over and cancel on destroy, like this:
import { Component, OnInit, OnDestroy} from "#angular/core";
import { ActivatedRoute } from "#angular/router";
export class AppComponent implements OnInit, OnDestroy {
private subscriptions = [];
constructor(private route AppRoute) {}
ngOnInit() {
this.subscriptions.push(this.route.data.subscribe());
}
ngOnDestroy() {
for (let subscription of this.subscriptions) {
subscription.unsubscribe();
}
}
}

Why not assign the created object to an object that doesn't get destroyed? This way your behaviour is maintained and the you can do a simple check to prevent any errors.
...
var baseObj = {};
baseObj.someObject = create();
vm.search = someFactory.getHttp(params).then(result => {
if (baseObj.someObject != null) {
baseObj.someObjet.prop = result;
}
});
vm.$onDestroy = () => {
baseObj.someObject = null;
}

Related

Why Can't Iterate over an array in my model using the map() function

i have angular 7 component which is tied to a model and there is an array inside that model, the array was populated from a service. and it's populated.
the problem is i can't map over the array although it has elements there.
when i console it it shows the array has element. then i tried to console typeOf(array) it always gives object although it is an array !!.
i tried using this soluation but it didn't help either.
any help please?
export class FooModel {
foo : Foo
bars: Bar[];
}
export class SomeComponent implements OnInit {
model: FooModel;
constructor(private service: ProjectService) {
this.model = new FooModel();
this.model.bars = [];
}
ngOnInit() {
this.service.getFoos().subscribe((result: any) => {
// data is populated fine
this.model= <FooModel>result.data;
});
Console.log(this.model); // the model has data at this point
const arr = this.model.bars.map(a=> {
// never comes here
return a;
});
console.log(arr); // nothing is displayed here
// this works why ??
const arr2 = [1,2,3].map(s=> {
return s;
}
console.log(arr2); // it displays [1,2,3]
}
}
As the request is asynchronous, you might need to place the logic within the subscribe,
this.service.getFoos().subscribe((result: any) => {
// data is populated fine
this.model= <FooModel>result.data;
const arr = this.model.bars.map(a=> {
// never comes here
return a;
});
console.log(arr);
});
subscription is asynchronous so while it is still working the next line operation in the execution stack will be performed in this case the map you have after the subscription meanwhile it is still being populated in the background. You can try mapping in another life cycle hook say viewChecked hopefully it works. #cheers
Please look at the comments
export class FooModel {
foo : Foo
bars: Bar[];
}
export class SomeComponent implements OnInit {
model: FooModel;
constructor(private service: ProjectService) {
this.model = new FooModel();
this.model.bars = [];
}
ngOnInit() {
this.service.getFoos().subscribe((result: any) => {
// data is populated fine
this.model= <FooModel>result.data;
});
// the following starts to execute even before the model is populated above.
const arr = this.model.bars.map(a=> {
// never comes here because this.model.bars is empty at here and the length is 0 and nothing inside map executes
return a;
});
console.log(arr); // nothing is displayed here because it has nothing inside
// this works why ?? because you are using on an array which has some items.
const arr2 = [1,2,3].map(s=> {
return s;
}
console.log(arr2); // it displays [1,2,3]
}
}
So as Sajeetharan suggested, you have keep it inside subscribe()

Angular 7/Typescript : Create queue/array of methods

I have a requirements that some functions should be called after some method completes execution.
Below is my code of processing the queue.
processQueue() {
while (this.queue.length) {
var item = this.queue.shift();
item.resolve(item.func(item.types));
}
}
This is one of the sample function to push method in queue
getAllValues() {
let promise1 = new Promise((resolve, reject) => {
if (this.isReady) {
resolve(this._getAllValues());
} else {
this.queue.push({
resolve: resolve,
func: this._getAllValues
});
}
});
return promise1;
}
And this is one of the function which will be called on processing the queue
_getAllValues() {
var results = {}, values = this.enumInstance.enumsCache.values;
for (var type in values) {
if (values.hasOwnProperty(type)) {
results[type] = values[type][this.enumInstance.lang];
}
}
return results;
}
The issue i am facing is when i call _getAllValues() directly then i am able to access this.enumInstance.
But when same method is being accessed through processQueue() i am unable to access this.enumInstance. It gives me undefined. I think this is not referred to main class in this case.
So can anyone help me here. How can i resolve this?

How to consume an Observable multiple times?

I am new to Angular and RxJS. I wonder what is the best/correct way to consume a value from an Observable multiple times.
My setup:
I have a component which calls a service which uses a REST service.
After the server returns the result I want to
use this result in the service at hand
AND return the result to the component.
// foo.component.ts
onEvent() {
this.fooService.foo()
.subscribe((data: FooStatus) => doSomething());
}
// foo.service.ts
private lastResult: FooStatus;
constructor(protected http: HttpClient) {
}
foo(): Observable<FooResult> {
return this.http.get('/foo')
.map((data: FooStatus) => {
this.lastResult = data; // use the data...
return data; // ... and simply pass it through
});
}
Using subscribe() multiple times will not work because the request would be send multiple times. This is wrong.
At the moment I use map() to intercept the result. But I am not comfortable with this because I introduce a side effect. Seems like a code smell to me.
I experimented with
foo(onSuccess: (result: FooResult) => void, onFailure: () => void): void {
...
}
but this looks even worse, I loose the Observable magic. And I do not want to have to write these callbacks in every service method myself.
As another way I considered the call to subscribe() in the service and then to create a fresh Observable I then can return to the component. But I could not get it work... seemed to complicated, too.
Is there a more elegant solution?
Is there a helpful method on Observable I did miss?
There are a number of ways to do this, and the answer will depend on your usage.
This codepen https://codepen.io/mikkel/pen/EowxjK?editors=0011
// interval observer
// click streams from 3 buttons
console.clear()
const startButton = document.querySelector('#start')
const stopButton = document.querySelector('#stop')
const resetButton = document.querySelector('#reset')
const start$ = Rx.Observable.fromEvent(startButton, 'click')
const stop$ = Rx.Observable.fromEvent(stopButton, 'click')
const reset$ = Rx.Observable.fromEvent(resetButton, 'click')
const minutes = document.querySelector('#minutes')
const seconds = document.querySelector('#seconds')
const milliseconds = document.querySelector('#milliseconds')
const toTime = (time) => ({
milliseconds: Math.floor(time % 100),
seconds: Math.floor((time/100) % 60),
minutes: Math.floor(time / 6000)
})
const pad = (number) => number <= 9 ? ('0' + number) : number.toString()
const render = (time) => {
minutes.innerHTML = pad(time.minutes)
seconds.innerHTML = pad(time.seconds)
milliseconds.innerHTML = pad(time.milliseconds)
}
const interval$ = Rx.Observable.interval(10)
const stopOrReset$ = Rx.Observable.merge(
stop$,
reset$
)
const pausible$ = interval$
.takeUntil(stopOrReset$)
const init = 0
const inc = acc => acc+1
const reset = acc => init
const incOrReset$ = Rx.Observable.merge(
pausible$.mapTo(inc),
reset$.mapTo(reset)
)
app$ = start$
.switchMapTo(incOrReset$)
.startWith(init)
.scan((acc, currFunc) => currFunc(acc))
.map(toTime)
.subscribe(val => render(val))
You will notice that the reset$ observable is used in two other observables, incOrReset$ and stopOrReset$
You can also introduce a .multicast() operator, which will explicitly allow you to subscribe multiple times. See description here: https://www.learnrxjs.io/operators/multicasting/
Take a look at share, it allows you to share a single subscription to the underlying source. You could do something like the following (it's not tested, but should give you the idea):
private lastResult: FooStatus;
private fooObs: Observable<FooStatus>;
constructor(protected http: HttpClient) {
this.fooObs = this.http.get('/foo').share();
this.fooObs.subscribe(((data: FooStatus) => this.lastResult = data);
}
foo(): Observable<FooResult> {
return this.fooObs.map(toFooResult);
}
as mentioned by #Mikkel in his answer that approach is cool however you can make use of following methods from Observable and achieve this.
these important methods are publishReplay and refCount,
use them as below,
private lastResult: FooStatus;
private myCachedObservable : Observble<FooResult> = null;
constructor(protected http: HttpClient) {
}
foo(): Observable<FooResult> {
if(!myCachedObservable){
this.myCachedObservable = this.http.get('/foo')
.map((data: FooStatus) => {
this.lastResult = data; // use the data...
return data; // ... and simply pass it through
})
.publishReplay(1)
.refCount();
return this.myObsResponse;
} else{
return this.myObsResponse;
}
}
Now in your component simply subscribe the observable returned from your method as above and notice the network request, you will see only one network request being made for this http call.

ngShow expression is evaluated too early

I have a angular component and controller that look like this:
export class MyController{
static $inject = [MyService.serviceId];
public elements: Array<string>;
public errorReceived : boolean;
private elementsService: MyService;
constructor(private $elementsService: MyService) {
this.errorReceived = false;
this.elementsService= $elementsService;
}
public $onInit = () => {
this.elements = this.getElements();
console.log("tiles: " + this.elements);
}
private getElements(): Array<string> {
let result: Array<string> = [];
this.elementsService.getElements().then((response) => {
result = response.data;
console.log(result);
}).catch(() => {
this.errorReceived = true;
});
console.log(result);
return result;
}
}
export class MyComponent implements ng.IComponentOptions {
static componentId = 'myId';
controller = MyController;
controllerAs = 'vm';
templateUrl = $partial => $partial.getPath('site.html');
}
MyService implementation looks like this:
export class MyService {
static serviceId = 'myService';
private http: ng.IHttpService;
constructor(private $http: ng.IHttpService) {
this.http = $http;
}
public getElements(): ng.IPromise<{}> {
return this.http.get('./rest/elements');
}
}
The problem that I face is that the array elements contains an empty array after the call of onInit(). However, later, I see that data was received since the success function in getELements() is called and the elements are written to the console.
elements I used in my template to decide whether a specific element should be shown:
<div>
<elements ng-show="vm.elements.indexOf('A') != -1"></elements>
</div>
The problem now is that vm.elements first contains an empty array, and only later, the array is filled with the actual value. But then this expression in the template has already been evaluated. How can I change that?
Your current implementation doesn't make sense. You need to understand how promises and asynchronous constructs work in this language in order to achieve your goal. Fortunately this isn't too hard.
The problem with your current implementation is that your init method immediately returns an empty array. It doesn't return the result of the service call so the property in your controller is simply bound again to an empty array which is not what you want.
Consider the following instead:
export class MyController {
elements: string[] = [];
$onInit = () => {
this.getElements()
.then(elements => {
this.elements = elements;
});
};
getElements() {
return this.elementsService
.getElements()
.then(response => response.data)
.catch(() => {
this.errorReceived = true;
});
}
}
You can make this more readable by leveraging async/await
export class MyController {
elements: string[] = [];
$onInit = async () => {
this.elements = await this.getElements();
};
async getElements() {
try {
const {data} = await this.elementsService.getElements();
return data;
}
catch {
this.errorReceived = true;
}
}
}
Notice how the above enables the use of standard try/catch syntax. This is one of the many advantages of async/await.
One more thing worth noting is that your data services should unwrap the response, the data property, and return that so that your controller is not concerned with the semantics of the HTTP service.

Why is that the UI is not refreshed when data is updated in typescript and angularjs program

I learn typescript and angularjs for a few days,and now I have a question that confuses me for days, I want to make a gps tracking system, so I try to write a service like this:
1.
module Services {
export class MyService {
getGpsPeople(): Array<AppCommon.GPSPerson> {
var gpsPeople = new Array<AppCommon.GPSPerson>()
for (var i = 0; i < 10; i++) {
var tempPerson = new AppCommon.GPSPerson({ name: "username" + i.toString() });
gpsPeople.push(tempPerson);
}
return gpsPeople;
}
} // MyService class
}
A controller like this:
module AppCommon {
export class Controller {
scope: ng.IScope;
constructor($scope: ng.IScope) {
this.scope = $scope;
}
}
}
module Controllers {
export interface IMyScope extends ng.IScope {
gpsPeople: Array<AppCommon.GPSPerson>;
}
export class MyController extends AppCommon.Controller {
scope: IMyScope;
static $inject = ['$scope','myService'];
constructor($scope: IMyScope,service:Services.MyService) {
super($scope);
$scope.gpsPeople = service.getGpsPeople();
}
}
}
3.The GPSPerson class like this:
export class GPSPoint {
latitude = 0;
longtitude = 0;
constructor(la: number, lg: number) {
this.latitude = la;
this.longtitude = lg;
}
}
export interface IPerson {
name: string;
}
export class GPSPerson
{
name: string;
lastLocation: GPSPoint;
countFlag = 1;
historyLocations: Array<GPSPoint>;
timerToken: number;
startTracking() {
this.timerToken = setInterval(
() => {
var newGpsPoint = null;
var offside = Math.random();
if (this.countFlag % 2 == 0) {
newGpsPoint = new GPSPoint(this.lastLocation.latitude - offside, this.lastLocation.longtitude - offside);
}
else {
newGpsPoint = new GPSPoint(this.lastLocation.latitude + offside, this.lastLocation.longtitude + offside);
}
this.lastLocation = newGpsPoint;
this.historyLocations.push(newGpsPoint);
console.log(this.countFlag.toString() + "+++++++++++++++++++" + this.lastLocation.latitude.toString() + "----" + this.lastLocation.longtitude.toString());
this.countFlag++;
}
, 10000);
}
stopTracking() {
clearTimeout(this.timerToken);
}
constructor(data: IPerson) {
this.name = data.name;
this.lastLocation = new GPSPoint(123.2, 118.49);
this.historyLocations = new Array<GPSPoint>();
}
}
The problem is:
1.Should I make the GPSPerson class a Controller?
2.The setinterval works but the UI dose not change(when I hit button ,it changes,the button do nothing )?
I'm a beginner of ts and angular,and have no experience with js, I do not know if I have explained it clearly, hope someone can help me, thanks!
setInterval works but the ui dose not change
This is because the angular $digest loop does not run on completion of setInterval. You should use $interval service https://docs.angularjs.org/api/ng/service/$interval as that tells Angular to do its dirty checking again.
You just need to provide MyService access to $interval though. Inject it using $inject (see https://www.youtube.com/watch?v=Yis8m3BdnEM&hd=1).
1.should i make the GPSPerson class a Controller?
No. Its an array of JavaScript objects inside the controller and that is fine.
I would separate the tracking logic and keep just the data in GPSPerson. For the tracking logic I would make a factory.
My examples are not in Typescript but I'm sure you will have no problem in converting the code if you want.
This is a link to a Plunk
I've made a much simpler example but I think you'll understand the idea.
The factory would have two methods for start and stop tracking. They will take a person as parameter.
app.factory('tracking',function($interval){
var trackingInterval;
var trackingFn = function(person){
var currentPos = Math.floor(Math.random()*10);
var newPosition = {id:person.positions.length, position:currentPos};
person.positions.push(newPosition);
};
var startTracking = function(person){
person.interval = $interval(function(){
trackingFn(person);
},2000);
};
var stopTracking = function(person){
console.log('STOP');
$interval.cancel(person.interval);
};
var getNewTrack = function(){
};
return {
startTracking: startTracking,
stopTracking: stopTracking,
};
});
I've also made a very simple directive to show the data
app.directive('position',function(){
return {
templateUrl: 'positionTemplate.html',
link: function(scope, element,attrs){
}
}
});
and the template look like this
<div>
<button ng-click="startTracking(person)">Start tracking</button>
<button ng-click="stopTracking(person)">Stop tracking</button>
<p>{{person.name}}</p>
<ul>
<li ng-repeat="pos in person.positions">{{pos.position}}</li>
</ul>
</div>
And the directive would be called this way
<div ng-repeat="person in people">
<div position></div>
</div>
I'm not saying that this is a better solution but that is how I would do it. Remember it is just a model and needs a lot of improvement.

Resources