Angular - When observable is complete, do something - javascript

I have a 2 part process that I am working with.
Component 1 allows me to import a list of usernames and submit it to a service. That service then returns the user profile data which I use in Component 2.
My issue is that I am trying to do something when I receive the data back from the observable I am subscribed to but it doesn't appear to be firing.
Component 1:
import { Component, EventEmitter, NgModule, OnInit, Output } from '#angular/core';
import { FormControl, FormGroup, FormBuilder, Validators } from '#angular/forms';
import { MassEmpService } from "app/mass-change/shared/mass.service";
import { Observable } from 'rxjs/Observable';
#Component({
selector: 'app-import-list',
templateUrl: './import-list.component.html',
styleUrls: ['./import-list.component.css'],
})
export class ImportListComponent implements OnInit {
// Define the data types used for the import list
importForm: FormGroup;
message: {};
error: string;
constructor(
private fb: FormBuilder,
private _massEmpService: MassEmpService
) {
}
ngOnInit() {
this.createForm();
}
// Generate the form
createForm() {
this.importForm = this.fb.group({
dataType: ['-1', Validators.required],
data: ['', Validators.required]
});
}
// Process the data for the form
onProccess(data) {
// Define our vars
let i: number;
const dat: string = data.data.split('\n');
const dataType: string = data.dataType;
const validArray = [];
const invalidArray = [];
// Loop over each line
for (i = 0; i < dat.length; i++) {
// Validate our data point
if (this.validateData(dataType, dat[i])) {
validArray.push(dat[i]);
} else {
invalidArray.push(dat[i]);
}
}
// Do we have any invalid data?
if (invalidArray.length) {
this.renderMessage('danger', 'fa-warning', 'Oops! Please check for invalid data.', false);
} else {
// Receive the data based on the imported criteria.
this._massEmpService.processImport(dataType, validArray)
.subscribe(
data => { this._massEmpService.fetchImportedResults(data); },
error => { this.error = error.statusText; }
);
}
}
... Other Code Here ...
}
Component 2:
export class EmployeeSelectionComponent implements OnInit {
// Define our search results
public searchResults: ImportResults[] = [];
constructor(
private _massEmpService: MassEmpService
) {
}
ngOnInit() {
this.fetchData();
}
fetchData() {
// Push our results to the array if they don't already exist
this._massEmpService.importedResults
.subscribe(
data => {
this.searchResults.push(...data);
console.log('I made it here');
},
() => {
console.log('.. but not here');
}
);
}
}
Service:
import { Injectable } from '#angular/core';
import { Http, Response, Headers } from '#angular/http';
import { RouterLink } from '#angular/router';
import { FrameworkService } from '#aps/framework';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
#Injectable()
export class MassEmpService {
// API URL
baseUrl = 'https://internal/api';
// Define the token headers for the service calls
headers: Headers = new Headers({
"Authorization": this._frameworkService.getSessionInfo().token
});
// Create a subject to observe the results and changes over time
public importedResults = new Subject<any>();
public transitionFields = new Subject<any>();
constructor(
private _http: Http,
private _frameworkService: FrameworkService
) { }
// Given a dataset, return the users based on data points submitted
processImport(dataType, dataSet): Observable<any> {
return this._http.post(this.baseUrl + '/fetchEmployeesFromImport', { "dataType": dataType, "data": dataSet }, { "headers": this.headers })
.map((result: Response) => result.json())
.share()
.catch(this.handleError);
};
// Pass the data received from the import process through our subject to observe
fetchImportedResults(data){
this.importedResults.next(data);
}
}
The Question:
In component 2, I am trying to check when I get data back so I can do something else in that component. I don't seem to reach the completed part of the observable though.
Any thoughts as to what I am doing wrong?

The first part of the problem lies in this snippet:
this._massEmpService.importedResults
.subscribe(
data => {
this.searchResults.push(...data);
console.log('I made it here');
},
() => {
console.log('.. but not here');
}
);
The second callback you are passing is for error notifications - not completion notifications. You will need to pass an additional callback to handle completion notifications.
The second part of the problem is that importedResults is a Subject and as such won't complete until its complete method is called. And there is no indication in the snippets that you are calling that method.

Related

Can't get deeper into the response data object in subscribe's callback function. Why?

I'm fetching data from RandomUser api with Angular HttpClient. I've created a method in a service calling GET, mapping and returning a Observable. Then I subscribe on this method in a component importing this service and in subscribe's callback I am trying to store the response data in a local variable. The problem is I can't get "deeper" into this response object than:
this.randomUser.getNew().subscribe(data => {
this.userData = data[0];
})
If I'm trying to reach any further element of that response object, and log it to console it I get "undefined". To be precise I cant reference to, for example:
this.randomUser.getNew().subscribe(data => {
this.userData = data[0].name.first;
})
If I store the "data[0]" in a variable first I can get into these unreachable properties. What is the reason of it? Please, help. Let me know what important piece of fundamental JS (or Angular) knowledge I'm not aware of. As far as I know I should be able to do what I am trying to do :)
service looks like these
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';
#Injectable({
providedIn: 'root'
})
export class RandomUserService {
url: string = " https://randomuser.me/api/ "
constructor(private http: HttpClient) { }
public getNew(): Observable<any> {
return this.http.get(this.url)
.pipe(map(responseData => {
const returnDataArray = [];
for (const key in responseData) {
returnDataArray.push(responseData[key])
}
return returnDataArray;
}))
}
}
component looks like these:
import { Component, OnInit } from '#angular/core';
import { RandomUserService } from 'src/app/shared/random-user.service';
import { Observable } from 'rxjs';
#Component({
selector: 'app-single-character',
templateUrl: './single-character.component.html',
styleUrls: ['./single-character.component.scss']
})
export class SingleCharacterComponent implements OnInit {
userData: object;
fname: string;
constructor(private randomUser: RandomUserService) {
this.randomUser.getNew().subscribe(data => {
this.userData = data[0];
})
}
ngOnInit(): void {
}
}
You are not parsing the returned data correctly in getNew().
The returned data looks like this:
So you need to access the user data like:
this.randomUser.getNew().subscribe(data => {
this.userData = data[0][0]; // note 2nd [0]
})
or for first name:
this.randomUser.getNew().subscribe(data => {
this.userData = data[0][0].name.first;
})
See stackblitz here: https://stackblitz.com/edit/so-http-parse?file=src/app/app.component.ts

In Angular 9, how do I update a component's data field to show in the DOM without re-instantiating it?

I'm fairly new to Angular 9. I have a program where a user enters in a name - which, upon submitting - a POST HTTP request is sent and the name is stored. I then have an unrelated component for a sub-header that lists the names that have been stored using a GET HTTP request using ngOnInit(). However, I need the sub-header to update that list of names dynamically each time a new list is entered rather than just whenever the component instantiates.
I'm unsure how to proceed. I'm sure I could simply add a button that fetches and updates said list, but trying for something more dynamic. Thanks in advance!
//SERVICE.TS...
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { NewList } from './new-list.model';
import { map } from 'rxjs/operators';
#Injectable({
providedIn: 'root'
})
export class ListService {
createdLists: NewList[] = [];
constructor(private http: HttpClient) { }
createList(postData) {
return this.http
.post(
'API_KEY',
postData
);
}
getLists() {
return this.http
.get<NewList>(
'API_KEY'
).pipe(map(responseData => {
const responseArray: NewList[] = [];
for (const key in responseData) {
responseArray.push(responseData[key])
}
return responseArray;
})
);
}
}
// NEW-LIST-MENU.TS (USER ENTERS A NAME)...
import { Component, OnInit } from '#angular/core';
import { NgForm } from '#angular/forms';
import { Router } from '#angular/router';
import { ListService } from 'src/app/shared/list.service';
import { NewList } from 'src/app/shared/new-list.model';
import { UIService } from 'src/app/shared/ui.service';
#Component({
selector: 'app-new-list-menu',
templateUrl: './new-list-menu.component.html',
styleUrls: ['./new-list-menu.component.css']
})
export class NewListMenuComponent implements OnInit {
constructor(private listService: ListService,
private uiService: UIService,
private router: Router) { }
ngOnInit(): void {
}
onSubmit(form: NgForm) {
const listName = form.value.listname;
const newListObj = new NewList(listName, []);
this.listService.createList(newListObj)
.subscribe(() => {
this.router.navigate(['']);
});
const lists = this.listService.updateLists(newListObj);
form.reset();
}
onCancel() {
this.router.navigate(['']);
}
}
// SUB-HEADER.TS...
import { Component, OnInit, Output } from '#angular/core';
import { Router } from '#angular/router';
import { ListService } from 'src/app/shared/list.service';
import { NewList } from 'src/app/shared/new-list.model';
import { faWindowClose } from '#fortawesome/free-solid-svg-icons';
import { faPlusCircle } from '#fortawesome/free-solid-svg-icons';
import { faList } from '#fortawesome/free-solid-svg-icons';
import { faSignOutAlt } from '#fortawesome/free-solid-svg-icons';
import { Subject } from 'rxjs';
#Component({
selector: 'app-sub-header',
templateUrl: './sub-header.component.html',
styleUrls: ['./sub-header.component.css']
})
export class SubHeaderComponent implements OnInit {
createdLists: NewList[];
faWindowClose = faWindowClose;
faPlusCircle = faPlusCircle;
faList = faList;
faSignOutAlt = faSignOutAlt;
#Output() closeSub = new Subject();
constructor(private listService: ListService,
private router: Router) { }
ngOnInit(): void {
this.listService.getLists().subscribe((responseData) => {
this.createdLists = responseData;
});
}
onCloseSelect() {
this.closeSub.next();
}
onNewListSelect() {
this.onCloseSelect();
this.router.navigate(['new-list-menu']);
}
onLogOutSelect() {
}
}```
You can accomplish this in many ways, as these components are not related to each other, you can introduce a state service and use observables. see below possible solution
Create a new state service ListStateService
export class ListStateService {
private listData = new BehaviorSubject<NewList >({} as NewList);
listData$ = this.listData .asObservable();
}
Inject ListStateService into NewListMenuComponent
In the onSubmit, after you update,
const lists = this.listService.updateLists(newListObj);
this.listData .next(lists );
Inject ListStateService into SubHeaderComponent
In the ngOnInit(), subscribe to the ListStateService.listData$ and here you will get the value on changes
In your service, use an event emitter (very useful):
import { EventEmitter } from "#angular/core";
#Output() myEvent: EventEmitter<any> = new EventEmitter();
then emit new data to your sub header component through your service like so:
emitEvent (newData: Array<string>) {
this.myEvent.emit({
data: newData,
});
}
Subscribe to new data in your sub header component ngOnInit and use it:
this.myService.myEvent.subscribe((newData: Array<string>) => {
console.log(JSON.stringify(newData.data));
});
Note: Subscriptions will cause memory leaks if constantly re-subscribed in the component, so you can save the subscription and call unsubscribe() on it in the ngOnDestroy callback.
It's a little unclear what you are trying to do, but if you are trying to pass data from a parent component to a child component, you can do this either with Input fields or a ViewChild
to use Input fields your parent might looks like this:
<app-sub-header [names]="names"></app-sub-header>
then use an "Input" field in the child. Updating names in the parent should update the same named variable in the child in real time.

Angular 2 API service to display error UI message

I'm new to Angular 2, so excuse me if the question is silly.
I have to fetch data from the server and display it in the component. The server has some API methods, so I've created the api.service.ts which looks like this:
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
const protocol = 'http';
const domain = 'mydomain.ng';
const port = ':4200';
#Injectable()
export class ApiService {
constructor(private http: HttpClient) { }
buildQuery(apiMethod: string) {
return `${protocol}://${domain}${port}/${apiMethod}`;
}
get(apiMethod: string): Observable<Response> {
const query = this.buildQuery(apiMethod);
return this.http.get<Response>(query)
.map(
resp => {
if (resp.ok) {
return resp;
} else { // Server returned an error
// here I need to show UI error in the component
}
}
)
.catch( // Error is on the client side
err => {
// here I need to show UI error in the component
}
);
}
getGeneralReport(): Observable<Response> {
return this.get('generalReport');
}
}
Server API has a lot of methods, so the get() method is designed to perform the actual request and handle common mistakes. Then I will have methods like getGeneralReport() which will call the get method with the parameter specifying which API method should be used.
Also I have a component called general.component.ts where the api.service is injected:
import { Component, OnInit } from '#angular/core';
import { ApiService } from '../../shared/api/api.service';
#Component({
selector: 'app-general',
templateUrl: './general.component.html',
styleUrls: ['./general.component.scss']
})
export class GeneralComponent implements OnInit {
generalInfo: Response;
constructor(private apiService: ApiService) { }
ngOnInit() {
this.apiService.getGeneralReport().subscribe(
data => {
this.generalInfo = data;
// Display the received data
}
);
}
}
There will be more components like general.component which will use the api.service. Now I'm stuck because I need to pop up the UI window in all the components which use api.service if the error occurs in api.service. Is it possible or should I use some different approach?
Yes it is possible, do it like this:
this.apiService.getGeneralReport().subscribe(
data => {
this.generalInfo = data;
// Display the received data
},
err => {
// yourPopupmethod(err)
}
);
and in service throw error. So update your service by adding HandleError method:
handleError(error: Response | any) {
return Observable.throw(new Error(error.status))
}
get(apiMethod: string): Observable<Response> {
const query = this.buildQuery(apiMethod);
return this.http.get<Response>(query)
.map(
resp => {
if (resp.ok) {
return resp;
} else { // Server returned an error
this.handleError(resp);
}
}
)
.catch(
err => {
this.handleError(err);
}
);
}

Angular 2 Interface throwing error of non existing property

I have an Angular 2 interface books.ts
export interface Books {
artists: Object;
tracks: Object;
}
This is the my service file where I am using it with http request searchService.ts
import { Injectable } from '#angular/core';
import { Http, Response, Headers, RequestOptions } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import { Books } from 'app/pages/search-results/books';
import 'rxjs/add/operator/map'
#Injectable()
export class SearchService {
constructor(private _http:Http) { }
getBook(keyword): Observable<Books[]>{
return this._http.get('https://api.spotify.com/v1/search?q=' + keyword + '&type=track,artist')
.map((response: Response) => <Books[]> response.json());
}
}
And this is my component where I am using interface searchResults.ts
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute, Router } from '#angular/router';
import { SearchService } from 'app/shared/search/search.service';
import { Books } from 'app/pages/search-results/books';
#Component({
selector: 'app-search-results',
templateUrl: './search-results.component.html',
styleUrls: ['./search-results.component.css'],
providers: [SearchService]
})
export class SearchResultsComponent implements OnInit {
keyword: any;
sub: any;
books: Books[];
errMessage: string;
arists: Object;
constructor(private _route: ActivatedRoute, private _router: Router, private _search: SearchService) { }
ngOnInit() {
this.sub = this._route
.queryParams
.subscribe(params => {
// Defaults to 0 if no query param provided.
this.keyword = params['keyword'] || 0;
this.getBooks(this.keyword);
});
//
}
getBooks(value) {
this._search.getBook(value)
.subscribe(
res => {
this.books = res;
console.log(res.artists);
},
error => { this.errMessage = <any>error }
);
}
}
The error comes when I try to console the res.artists. The error says Property 'artists' does not exist on type 'Books[]'. I am new to Angular 2 and doesn't know how to fix that.
The response is looks like
{artists:{limit: 20, item:[]}, tracks:{limit: 20, item:[]}}
I'm not sure but I think you try to get res.artist from collection of books. You can check it by for or e.g res[0].artist to get concrete artist.
getBook function in class SearchService return an array of Books object (Books[])
so, the res in getBooks function in SearchResultsComponent will be an Array of Books.
You can console.log(res) to see detail, if you want access to artists please try with res[0].artists if the res is not an empty array
The problem is that I am getting Object in response and I am assigning it to an Array which is causing the error. I have simply changes the both types to object and it solved my problem.
From this
books: Books[];
To this
books: Books;

Angular 2 Tour of Heroes doesn't work

I tried to write application based on tour of heroes.
I have Spring application which shares resources and client app which should get this data. I know that resources get to client app, but I can't print it.
import { HeroesService } from './shared/HeroesService';
import { Observable } from 'rxjs/Observable';
import { Hero } from './shared/Hero';
import { OnInit } from '#angular/core';
import { Component } from '#angular/core';
#Component({
selector: 'app',
template: require('app/app.component.html!text')
})
export class AppComponent implements OnInit {
errorMessage: string;
items: Hero[];
mode: string = 'Observable';
firstItem: Hero;
constructor(private heroesService: HeroesService) { }
ngOnInit(): void {
this.getHeroes();
console.log(this.items);
//this.firstItem = this.items[0];
}
getHeroes() {
this.heroesService.getHeroes()
.subscribe(
heroes => this.items = heroes,
error => this.errorMessage = <any>error
);
}
}
import { Injectable } from '#angular/core';
import { Http, Response } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import { Hero } from './Hero';
#Injectable()
export class HeroesService {
private heroesUrl = 'http://localhost:8091/heroes';
constructor(private http: Http) { }
getHeroes(): Observable<Hero[]> {
return this.http.get(this.heroesUrl)
.map(this.extractData)
.catch(this.handleError);
}
private extractData(res: Response) {
let body = res.json();
console.log(body);
return body || { };
}
private handleError(error: Response | any) {
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Observable.throw(errMsg);
}
}
In method extract data when I printed by console.log(body.data) I get undefined, but when I printed console.log(body) I get list of objects, therefore I return body instead body.data.
And when I print objects in extractData I get list of objects, but in AppComponent when I print console.log(this.items) I get undefined.
What's going on?
this.getHeroes() returns an Observable which means that you can't get data out of it unless you subscribe to it. Think about it like a magazine subscription, by calling this.getHeroes(), you have registered for the magazine but you don't actually get the magazine until it gets delivered.
In order to get a console.log of the data that comes back in AppComponent, replace the .subscribe block with the following:
.subscribe(
(heroes) =>{
console.log(heroes);
this.items = heroes;
},
error => this.errorMessage = <any>error
);
To further the magazine analogy, inside the subscribe block, you have received the magazine and here we are console logging its contents.
Hope this helps

Categories

Resources