Angular2 shared observable - reactjs

Doing some experimentation with Angular2 and curious about how to solve a situation where a service exposes a shared observable. Where one component is responsible for getting the data and another is responsible for displaying the data. Here is some code:
The common HttpService responsible for getting the data
import { Injectable } from '#angular/core';
import { Http, Response } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
#Injectable()
export class Service {
subject = new Subject<string[]>;
observable$ = this.subject.asObservable();
constructor(private http: Http) {}
observable: Observable<string[]>;
get(link: string): Observable<string[]> {
this.observable$ = this.http.get('myapi.com')
.map((res: Response) => this.subject.next(res.json())
.catch(this.handleError);
return this.observable$;
}
/**
* Handle HTTP error
*/
private handleError (error: any) {
// In a real world app, we might use a remote logging infrastructure
// We'd also dig deeper into the error to get a better message
let errMsg = (error.message) ? error.message :
error.status ? `${error.status} - ${error.statusText}` : 'Server error';
console.error(errMsg); // log to console instead
return Observable.throw(errMsg);
}
}
The GetComponent responsible for getting the data
import { Component, OnInit, OnDestroy } from '#angular/core';
import { Service } from '../shared/index';
#Component({
moduleId: module.id,
selector: 'get',
templateUrl: 'get.component.html',
providers: [Service],
})
export class GetComponent {
constructor(public service: Service) {}
submit() {
this.service.get(this.url).subscribe();
}
}
The DisplayComponent responsible for displaying the data
import { Component, OnInit, OnDestroy } from '#angular/core';
import { Subscription } from 'rxjs/Subscription';
#Component({
moduleId: module.id,
selector: 'display',
templateUrl: 'display.component.html'
})
export class DisplayComponent {
subscription: Subscription;
constructor(public service: Service) {}
ngOnInit() {
// the observable is undefined :(
this.subscription = this.service.observable$.subscribe(data => {
console.log(data);
},
error => {
// this never gets reached :(
})
}
ngOnDestroy() {
// prevent memory leak when component is destroyed
this.subscription.unsubscribe();
}
}
This design works ok except the error handling does not work for the DisplayComponent. Also the map function in the HttpService doesn't seem quite right this.subject.next(res.json()
Also it doesn't seem quite right that the GetComponent has to "subscribe" like this this.service.get(this.url).subscribe();
What is the proper way to design this sort of thing? How can I get the DisplayComponent to observe errors thrown by the HttpComponent

Your code has several problems where Service.$observable is modified several times:
observable$ = this.subject.asObservable();
this.observable$ = this.http.get('myapi.com') .... // remove this line
The right way to do this:
Get Component calls Service to get data. After data is loaded, Get Component emits a Data Ready event with the data. Display Component, and other components that use this data, listen to DataReady event and update data when the event is emitted.
Code to explain my answer:
#Injectable()
export class GetService {
/// subject
subject = new Subject<string[]>()
/// the observable
observable = this.subject.asObservable()
constructor(private $http: Http) {
}
/// get data
getData() {
// not override observable here
this.$http.get("api.com")
.map(response => response.json()) // map the data
.subscribe((data: string[]) => this.subject.next(data), // emit data event
error => this.subject.error(error)) // emit error event
}
}

Related

Problem passing data from one component to another component using services in Angular 8

Am working on a Single Page Application CRUD based API whereby am passing
data from the frontend to the backend using JWT which works fine. On the backend am saving the data in a database and later fetching all the data and return as a response to the frontend which works fine.
On the frontend, I fetch the data in the response and pass to another component through a Service. Basically I pass the data from create component to show component via a service called Shared service. The data am passing (response from the backend is an array of Javascript objects).
the problem is that the data is not reaching the show component, since I want after it reaches the show component I want to loop through the array of objects and show them on the view (show.component.html) as a table
Please assist?
Create Component
import { Component, OnInit } from '#angular/core';
import { Router } from '#angular/router';
import { SharedService } from 'src/app/Services/shared.service';
#Component({
selector: 'app-create',
templateUrl: './create.component.html',
styleUrls: ['./create.component.css']
})
export class CreateComponent implements OnInit {
public form = {
sponsorFirstName: null,
sponsorSurName: null,
sponsorEmail: null,
sponsorPhone: null,
nationality: null,
childFirstName: null,
childAge: null,
childSurName: null,
childGender: null
};
public error = null;
constructor(
private router: Router,
private Shared : SharedService) { }
onSubmit(){
this.Auth.submitFormData(this.form).subscribe(
data => this.handleResponse(data),
error => this.handleError(error)
);
}
handleResponse(data){
//console.log(data.data);
//pass the data to the shared service
this.Shared.createData(data.data);
//redirect to show-data component whereby I show the data in a table
this.router.navigateByUrl('/show-data');
}
handleError(error){
this.error = error.error.errors;
}
ngOnInit() {
}
}
Shared service
import { Injectable } from '#angular/core';
import { BehaviorSubject } from 'rxjs';
import { TokenService } from './token.service';
#Injectable({
providedIn: 'root'
})
export class SharedService {
private userData = new BehaviorSubject;
checkUser$ = this.userData.asObservable();
createData(data:any){
this.userData.next(data);
}
constructor(private Token : TokenService) { }
}
Show.component.ts file
import { Component, OnInit } from '#angular/core';
import { SharedService } from 'src/app/Services/shared.service';
#Component({
selector: 'app-show',
templateUrl: './show.component.html',
styleUrls: ['./show.component.css']
})
export class ShowComponent implements OnInit {
public userData;
constructor(
private Shared : SharedService,
) { }
ngOnInit() {
this.Shared.checkUser$.subscribe(message => this.userData = message);
}
}
Seems like a race condition between the service and your component, you should try and instantiate userData with a ReplaySubject(1) like so:
private userData = new ReplaySubject(1);
This way your subject will have a buffer and even if you late subscribe to it you'll have the value.
ReplaySubject
A Subject that "replays" or emits old values to new
subscribers

Angular 6 - Display data from database with *ngFor

I'm trying to display datas from a database. I've found examples on the internet but it's not working at all
Error from Eclipse:
java.lang.IllegalArgumentException: id to load is required for loading
My html file:
<ul>
<li *ngFor="let questionnaire of questionnaires | async">{{questionnaire.name}}</li>
</ul>
My typeScript file:
import { Injectable } from '#angular/core';
import { Component, OnInit, Input } from '#angular/core';
import {QuestionnaireService} from '../services/questionnaire.service';
#Injectable()
#Component({
selector: 'app-qcm',
templateUrl: './qcm.component.html',
styleUrls: ['./qcm.component.sass']
})
export class QcmComponent implements OnInit {
#Input()
questionnaires :any = [];
constructor(private questionnaireService: QuestionnaireService) { }
ngOnInit() {
this.questionnaires = this.questionnaireService.getQuestionnaire(1);
}
}
My service:
import {environment} from '../../environments/environment';
import {Injectable} from '#angular/core';
import {HttpClient} from '#angular/common/http';
#Injectable({providedIn: 'root'})
export class QuestionnaireService {
user: any;
constructor(private http: HttpClient) {}
getQuestionnaire(id: number) {
return this.http.get<any>(`${environment.apiUrl}getQuestionnaire`);
}
}
Here is my webservice methode (spring hibernate)
#GET
#Path("/getQuestionnaire")
public Questionnaire getQuestionnaire(#QueryParam("id") Long id) throws Exception {
return FacadeBackOffice.getInstance().getQuestionnaireService().getQuestionnaire(id);
}
TODO list:
Check if the api call goes to the right URL.
Howto: Check the request and response on network tab.
questionnaires in your component is an Observable and not an array.
Why: HttpClient get returns an Observable, so you have two choices:
use the async pipe (you are doing it the right way)
subscribe to the observable to send the request and in the anonymous function passed to the subscribe assign questionnaires class variable with the response (or subset).
Improvement:
Makes no sense to populate questionnaires class variable two ways (Input of the component and result of a http get request). Leave only one option here.
you might be facing asynch data call issue.
Please try subscribing to your service and then storing the data to your array. e.g.
ngOnInit() {
this.questionnaireService.getQuestionnaire()
.subscribe(
(data) => {
this.questionnaires= data;
});
}
Try taking the async out of the html, and then putting a .subscribe() on the the service instead. Also, remove the #Input(), it looks like it is unneeded since your component's ngOnInit will be populating the array.
I added a console.log() so you know what is in your service call in case you might need something like this.questionarries = response.body.
html
<ul>
<li *ngFor="let questionnaire of questionnaires">{{questionnaire.name}}</li>
</ul>
component
import { Injectable } from '#angular/core';
import { Component, OnInit, Input } from '#angular/core';
import {QuestionnaireService} from '../services/questionnaire.service';
#Injectable()
#Component({
selector: 'app-qcm',
templateUrl: './qcm.component.html',
styleUrls: ['./qcm.component.sass']
})
export class QcmComponent implements OnInit {
questionnaires :any = [];
constructor(private questionnaireService: QuestionnaireService) { }
ngOnInit() {
this.questionnaireService.getQuestionnaire().subscribe(
response => {
this.questionnaires = response;
console.log(this.questionnaries);
}
);
}
}

HTTP PUT Request Returns Empty From Angular 2 Service

I'm calling data from a service. I have to pass in an object with properties to retrieve the data. From what I can tell, I'm passing it in correctly and not receiving any errors in console, but the object that I assign to the data is empty. I'm including my Service and Component where I'm outputting the data.
I call the exact same endpoint from an Angular1 App with the same "payload" values and get my data object returned. I'm sure I'm missing something simple. Still getting used to Angular 2.
SERVICE
import { Injectable } from '#angular/core';
import { Http, Response } from '#angular/http';
import { Observable } from 'rxjs/Rx';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
#Injectable ()
export class DataService {
private qrDataUrl = 'http://my.data.url/endpoint';
private payload = {
DeliveryTypeId: 0,
PickupLocationid: 0,
PaymentTypeId: 0,
Items: ['XXXX'],
ApplicationToken:'123MYTOKEN456',
DateOfBirth: "1961-01-01T00:00:00.000Z"
};
constructor(private http: Http){ }
getQr():Observable<any>{
return this.http.put(this.qrDataUrl, this.payload)
.map((res:Response) => res.json());
}
}
COMPONENT
import { Component, OnInit } from '#angular/core';
import { DataService } from '../shared/dataService';
#Component({
selector: 'my-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'],
providers: [DataService]
})
export class HomeComponent implements OnInit {
qrData = { };
constructor(private dataService: DataService) {
// Do stuff
}
getData(){
this.dataService.getQr().subscribe(data => this.qrData = data);
console.log(this.qrData); //This logs an empty object
}
ngOnInit() {
console.log('Hello Home');
this.getData();
}
}
Asynchronous operation doesn't work synchronous way. You have to wait until the ajax gets completed. You can only see a data inside subscription of your getQr function once the service call resolved.
getData(){
this.dataService.getQr().subscribe(data => {
this.qrData = data;
console.log(this.qrData); //This logs an empty object
});
}

angular 2 observable.subscribe is not executed until being called a second time

I am trying to have this component get string data from a service that returns an observable
import { Component, OnInit, OnDestroy } from '#angular/core';
import { TabFourService } from "./tabfour.service";
import { Subscription } from "rxjs";
#Component({
selector: 'tab-four',
template: `
{{title}}
`
})
export class TabFourComponent implements OnInit, OnDestroy{
title: string = "This is Tab four";
subscription: Subscription;
constructor(private tabfourService: TabFourService){}
ngOnInit(){
console.log("now in init");
this.getItems();
this.getItems();
}
getItems(){
console.log("now in get items");
this.subscription = this.tabfourService.getItems()
.subscribe(data => console.log("testing observable"));
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
Here is a simple service:
import { Injectable } from '#angular/core';
import { Subject } from 'rxjs/Subject';
import { Observable } from "rxjs";
#Injectable()
export class TabFourService {
items: string;
private itemSource = new Subject<string>();
constructor(){}
itemSource$ = this.itemSource.asObservable();
getItems(): Observable<string> {
this.itemSource.next("aaa");
return this.itemSource$;
}
}
I list the service as a provider in #NgModule() so that it can be shared by all component instances. There is also a router for TabFourComponent so every time I navigate to it I should see "testing observable" in the console.
But it didn't show until I called getItems() twice.
I wonder why it didn't get triggered at the first time.
tabfour.component.ts:20 now in init
tabfour.component.ts:26 now in get items
tabfour.component.ts:26 now in get items
tabfour.component.ts:28 testing observable
Edit:
While in another case where services provide data from http.get
// in service
getLibraryFromDatabase(): Observable<any[]> {
return this.http.get(<some url>)
.map(data => data.json())
.catch(this.handleError);
}
Component doesn't need to call service's method again after subscription.
// in component
ngOnInit() {
this.subscription = this.libraryService.getLibraryFromDatabase()
.subscribe(libs => this.createLibs(libs));
}
If the subscribed method is not called until the second getItems() execution, it's because the event is triggered BEFORE the subscription is taken in charge. Register the subscription in the ngOnInit method, (that's the best place to do this kind of things) and then call the method that triggers the event that you have subscribed to:
ngOnInit() {
this.subscription = this.tabfourService.getItems()
.subscribe(data => console.log("testing observable"));
// The subscription has been taken in charge, now call the service's method
this.tabFourService.getItems();
}

How do I ensure the data has arrived before the template tries to render it?

I made an app with angular-cli, Immutable and Redux. I followed instructions from this article which does not describe how to get data. My app needs to initialize the Redux store with data from an asynchronous http call.
I have a component for listing the data which has a template. This component gets its data from the store which depends on a service which makes the http call. The http call works but the app throws an exception that indicates the listing component is trying to get the data before it has arrived.
My repository is here
A demo of the app is here
Error message:
main.js:21 TypeError: Cannot read property 'getState' of undefined
at a.get [as objections] (https://dancancro.github.io/bernierebuttals/main.js:18:22268)
at a._View_a0.detectChangesInternal (a.template.js:189:37)
at a.detectChanges (https://dancancro.github.io/bernierebuttals/main.js:32:13138)
at a.detectViewChildrenChanges (https://dancancro.github.io/bernierebuttals/main.js:32:13774)
at a._View_a_Host0.detectChangesInternal (a.template.js:34:8)
at a.detectChanges (https://dancancro.github.io/bernierebuttals/main.js:32:13138)
at a.detectContentChildrenChanges (https://dancancro.github.io/bernierebuttals/main.js:32:13588)
at a.detectChangesInternal (https://dancancro.github.io/bernierebuttals/main.js:32:13345)
at a.detectChanges (https://dancancro.github.io/bernierebuttals/main.js:32:13138)
at a.detectViewChildrenChanges (https://dancancro.github.io/bernierebuttals/main.js:32:13774)
Here are some relevant parts of the code: (I'm working on this. The repository contains the current code)
list.component.html
...
<ul id="objection-list" [sortablejs]="store.objections" [sortablejsOptions]="options" (update)="setTouched()">
<li *ngFor="let objection of store.objections">
<list-objection
[objection]="objection"
[editable]="editable"
(onEdit)="setTouched()"
(onReordered)="setReordered(objection)"
></list-objection>
</li>
</ul>
...
list.component.ts
import { Component, OnInit, ContentChildren, QueryList } from '#angular/core';
import { Router, ActivatedRoute } from '#angular/router';
import { SortablejsOptions, SORTABLEJS_DIRECTIVES } from 'angular-sortablejs';
import Immutable = require('immutable');
import { ObjectionComponent } from './objection/objection.component';
import { ObjectionModel } from '../objection';
import { ObjectionStore } from '../objection-store';
import { DataService } from '../data.service';
import { addObjection } from '../actions';
#Component({
moduleId: module.id,
selector: 'app-list',
templateUrl: 'list.component.html',
styleUrls: ['list.component.css'],
providers: [ObjectionStore, DataService],
directives: [ObjectionComponent, SORTABLEJS_DIRECTIVES]
})
export class ListComponent implements OnInit {
private sub: any;
editable: boolean = false;
touched: boolean = false;
expanded: boolean = false;
options: SortablejsOptions = {
disabled: false
};
objectionID: number;
constructor(
private store: ObjectionStore,
private route: ActivatedRoute) {}
...
}
objection-store.ts
import { Injectable } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import Immutable = require('immutable');
import { createStore } from 'redux';
import { ObjectionAction } from './actions';
import { reducer } from './reducer';
import { ObjectionModel } from './objection';
import { DataService } from './data.service';
#Injectable()
export class ObjectionStore {
private sub: any;
store: any;
constructor(
private dataService: DataService) {
this.store = createStore(reducer, Immutable.List<ObjectionModel>(objections.json()));
});
}
data.service.ts
import { Injectable } from '#angular/core';
import { Http, Response, Headers } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/toPromise';
import { ObjectionModel } from './objection';
import { Area } from './area';
let objectionsPromise;
#Injectable()
export class DataService {
result: Object;
combined: any;
error: Object;
getUrl: string = 'https://script.google.com/macros/s/AKfycbymzGKzgGkVo4kepy9zKIyDlxbnLbp-ivCvj8mVMClmWgr-V-g/exec?json=1';
postUrl: string = 'https://script.google.com/macros/s/AKfycbymzGKzgGkVo4kepy9zKIyDlxbnLbp-ivCvj8mVMClmWgr-V-g/exec';
static getObjection(objections: any[], id: number): ObjectionModel {
return objections.filter(function(objection) {
return objection.id === id
})[0];
}
constructor(private http: Http) {
objectionsPromise = this.http.get(this.getUrl).toPromise();
}
Basic Answer
To answer the immediate question, getting the objections from the server and putting them in a template can be simply done like this:
Use async pipe to bind an observable directly into the template. Async pipe 'unboxes' observables (promises too) and updates your template when they change.
<ul id="objection-list"
[sortablejs]="store.objections"
[sortablejsOptions]="options"
(update)="setTouched()">
<li *ngFor="let objection of objections | async">
<list-objection
[objection]="objection"
[editable]="editable"
(onEdit)="setTouched()"
(onReordered)="setReordered(objection)">
</list-objection>
</li>
</ul>
Use DataService to initialize this observable when your component is initialized:
#Component({
moduleId: module.id,
selector: 'app-list',
templateUrl: 'list.component.html',
styleUrls: ['list.component.css'],
providers: [ObjectionStore, DataService],
directives: [ObjectionComponent, SORTABLEJS_DIRECTIVES]
})
export class ListComponent implements OnInit {
// ...
private objections: Observable<ObjectionModel[]>;
constructor(
private dataService: DataService,
private route: ActivatedRoute) {}
ngOnInit() {
this.objections = this.dataService.getObjections();
}
// ...
Fix up your data service:
import { Injectable } from '#angular/core';
import { Http } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import { ObjectionModel } from './objection';
#Injectable()
export class DataService {
result: Object;
combined: any;
error: Object;
getUrl: string = 'https://script.google.com/macros/s/AKfycbymzGKzgGkVo4kepy9zKIyDlxbnLbp-ivCvj8mVMClmWgr-V-g/exec?json=1';
postUrl: string = 'https://script.google.com/macros/s/AKfycbymzGKzgGkVo4kepy9zKIyDlxbnLbp-ivCvj8mVMClmWgr-V-g/exec';
getObjections(private http: Http): Observable<ObjectionModel[]> {
return this.http.get(this.getUrl) // returns an observable of the response
.map(response => response.json()); // transforms it into an observable of ObjectionModels
}
}
Notes on Redux
Note that this has all been done without Redux.
In general, I like Redux and I use it a lot. However in your example you seem to be doing a couple of unorthodox things:
You are creating a store in your ObservableStore service - this suggests to me that you are planning on having several stores in your app. One of the main principles of Redux is global immutable state, meaning that there is normally only one Redux store in an application.
You seem to by trying to fetch the initial data set from the server and then creating your store when the response has come back. It's not generally a good idea to couple the store creation to an HTTP request like this. Instead I recommend creating an empty store when you initialize your app, and then updating it via reducer when the HTTP request comes back.
You can do raw Redux in Angular 2, but you may find it a little frustrating getting it to work with Angular's observable-heavy APIs. Fortunately people (including me) have done this work for you in the form of Observable-oriented redux libraries like ng2-redux and ngrx/store
If you were to use ng2-redux, things would look more like this:
Top-level app component: build your store and initialize it:
import { NgRedux } from 'ng2-redux';
import { rootReducer } from './reducers';
#Component({ /* ... */ })
class App {
constructor(private ngRedux: NgRedux<any>) {
this.ngRedux.configureStore(rootReducer, {});
}
}
List component: bind your template to a selector from the store's current data. Also trigger a data fetch on initialization.
import { NgRedux, select } from 'ng2-redux';
#Component({
moduleId: module.id,
selector: 'app-list',
templateUrl: 'list.component.html',
styleUrls: ['list.component.css'],
providers: [DataService],
directives: [ObjectionComponent, SORTABLEJS_DIRECTIVES]
})
export class ListComponent implements OnInit {
// ...
// Magic selector from ng2-redux that makes an observable out
// of the 'objections' property of your store.
#select('objections') objections: Observable<ObjectionModel[]>;
constructor(
private ngRedux: NgRedux<any>,
private dataService: DataService) {}
ngOnInit() {
this.subscription = this.dataService.getObjections()
.subscribe(objections => this.ngRedux.dispatch({
type: FETCH_OBJECTIONS_OK,
payload: objections
},
error => this.ngRedux.dispatch({
type: FETCH_OBJECTIONS_ERROR,
error: error
});
)
}
}
OK... so how does the data actually get into the store? Via the reducer. Remember in redux store state can only be changed from a reducer.
export function objectionReducer(state = [], action) {
switch(action.type) {
case FETCH_OBJECTIONS_OK: return [ ...action.payload ];
case ADD_OBJECTION: return [ ...state, action.payload ];
// etc.
}
return state;
}
We can also track errors in a reducer too if we want, how you want to structure this is up to you.
export function errorReducer(state = {}, action) {
switch(action.type) {
case FETCH_OBJECTIONS_ERROR: return { objectionFetch: action.error }
}
}
Since we have one store, we modularize the reducers instead and compose them together:
import { combineReducers } from 'redux';
import { objectionReducer } from './objection.reducer';
import { errorReducer } from './error.reducer';
export const rootReducer = combineReducers({
objections: objectionReducer,
error: errorReducer
});
More to Learn/Disclosure
Disclosure: I am one of the authors of Ng2-Redux. However Ngrx/Store is also a great alternative for doing redux with Ng2, and while the implementation is different using it is very similar to what I have described above.
I and my colleagues also maintain a number of training resources on Angular2 and on Redux, which I will provide below:
A good Angular2-specific intro to redux: http://angular-2-training-book.rangle.io/handout/redux/
An intro to Observables: http://angular-2-training-book.rangle.io/handout/observables/
Ng2-Redux documentation: https://github.com/angular-redux/ng2-redux/blob/master/README.md
Your data service is wrong. you are missing the point of the promise/observable.
you should read angular documentation about http client. at least read this part:
https://angular.io/docs/ts/latest/guide/server-communication.html#!#promises
No http call in the constructor!
this is more like it:
getHeroes (): Promise<Hero[]> {
return this.http.get(this.heroesUrl)
.toPromise()
.catch(this.handleError);
}
After you get use to that, I really recommend you read some about Observables. much cleaner and advanced.

Resources