I am writing an Angular9 app with a use case where I am buffering data across 3 pages (with forms taking data input).
Then, on the 4th page, I display a summary of the details gathered across the 3 pages as read only text.
I have implemented the data buffering using the following article:
https://www.infragistics.com/community/blogs/b/infragistics/posts/simplest-way-to-share-data-between-two-unrelated-components-in-angular
Which uses the BehaviorSubject
Below is my data service logic:
import { Injectable } from '#angular/core';
import { HttpClient, HttpHeaders } from '#angular/common/http';
import { Observable, BehaviorSubject, throwError } from 'rxjs';
import { retry, catchError } from 'rxjs/operators';
import { UserResponse } from './user-response';
#Injectable({
providedIn: 'root'
})
export class UserService {
//Details Page 1
public salutation: BehaviorSubject<string>;
public firstName: BehaviorSubject<string>;
// Base url
baseurl = 'https://localhost:8080/rest';
constructor(private http: HttpClient) { }
// Http Headers
httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json'
})
}
// POST
createUser(data): Observable<UserResponse> {
alert('user - details = '+ data);
return this.http.post<UserResponse>(this.baseurl + '/user', JSON.stringify(data), this.httpOptions)
.pipe(
retry(1),
catchError(this.errorHandler)
)
}
// Error handling
errorHandler(error) {
let errorMessage = '';
if(error.error instanceof ErrorEvent) {
// Get client-side error
errorMessage = error.error.message;
} else {
// Get server-side error
errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
}
console.log(errorMessage);
return throwError(errorMessage);
}
}
Below is my first view component:
import { Component, OnInit } from '#angular/core';
import { NgForm } from '#angular/forms';
import { UserService } from '../service/user.service';
import { BehaviorSubject } from 'rxjs';
#Component({
selector: 'app-details1',
templateUrl: './details1.component.html',
styleUrls: ['./details1.component.css']
})
export class Details1Component implements OnInit {
salutation: string;
firstName: string;
constructor(private userService: UserService) { }
ngOnInit(): void {
}
goToDetails2(form : NgForm) {
this.userService.salutation = new BehaviorSubject(form.value.salutation);
this.userService.firstName = new BehaviorSubject(form.value.firstName);
}
}
Below is a snippet of my final/summary view component
import { Component, OnInit} from '#angular/core';
import { Router } from '#angular/router';
import { UserService } from '../service/user.service';
#Component({
selector: 'app-summary',
templateUrl: './summary.component.html',
styleUrls: ['./summary.component.css']
})
export class SummaryComponent implements OnInit {
salutation: string;
firstName: string;
constructor(
private router: Router,
public userService: UserService
) {
}
ngOnInit(): void {
this.userService.salutation.subscribe(c => { this.salutation = c; });
this.userService.firstName.subscribe(c => { this.firstName = c; });
}
}
On the final page my html excerpts is as follows:
<form #detailsForm4="ngForm">
<div class="govuk-form-group">
<table class="govuk-table">
<thead class="govuk-table__head">
<tr class="govuk-table__row"></tr>
</thead>
<tbody class="govuk-table__body" align="left">
<tr class="govuk-table__row">
<th scope="row" class="govuk-table__header">Salutation</th>
<td class="govuk-table__cell"> {{salutation}} </td>
<td class="govuk-table__cell"> </td>
</tr>
<tr class="govuk-table__row">
<th scope="row" class="govuk-table__header">First name</th>
<td class="govuk-table__cell"> {{firstName}} </td>
<td class="govuk-table__cell"> </td>
</tr>
The data is displayed on the summary page as captured on the forms in pages 1-3,
BUT the data is lost on page refresh or when my machine hibernates etc
I have read about local and session storage persistence and used it since the angularjs 1.x days.
Ages ago, I was actually surprised when the data disappeared on my summary screen!
I guess that an ideal solution will involve storing the data in client side storage when the user navigates away from the relevant form, then retrieving the data on the summary page.
I guess, when the summary page is refreshed or displayed for the first time,
I will check storage for the data and display if it exist.
If storage is empty, then I will be using data from previous page at runtime.
Please, I need help with a best practice and stable solution, which is cross browser friendly and intelligent for a commercial application.
Also, an approach that fits into Angular9 BehaviorSubject - rxjs stack OR Any recent features, because I know Angular has evolved since AngularJs 1.x ?
The summary details is only changed when the user navigates back to any of the pages 1-3 to change the form details.
I will appreciate any code snippet or reference to keep the data on the summary page.
Thanks a lot.
I will propose you to use localStorage within your SummaryComponent within ngOnDestroy lifecycle hook. It will always set your data within localStorage before component will be destroyed due to refresh. Then when you are trying to retrieve data from the service, if it will be empty try to get it from localStorage.
export class SummaryComponent implements OnInit, OnDestroy {
salutation: string;
firstName: string;
constructor(
private router: Router,
public userService: UserService
) {
}
ngOnInit(): void {
this.userService.salutation.subscribe(c => { this.salutation = c || localStorage.get('salutation'); });
this.userService.firstName.subscribe(c => { this.firstName = c || localStorage.get('firstName'); });
}
ngOnDestroy(): void {
localStorage.setItem('salutation', this.salutation));
localStorage.setItem('firstName', this.firstName));
}
}
Similar implementation you can do in the component where this values are set and you expect that user can refresh the page before go to the next page.
EDIT
Sorry, you are right that ngOnDestory will not trigger on page refresh to do that you will need to handle window:beforeunload.
#HostListener('window:beforeunload', ['$event'])
beforeunload($event: Event): void {
// set localStorage here
}
Going forward with your solution, the best option to set items to the localStorage is the place where you set values to the service. It's probably goToDetails2 method.
goToDetails2(form : NgForm) {
this.userService.salutation = new BehaviorSubject(form.value.salutation);
this.userService.firstName = new BehaviorSubject(form.value.firstName);
localStorage.setItem('salutation', form.value.salutation));
localStorage.setItem('firstName', form.value.firstName));
}
EDIT-2 to your problem described in the comment
I will propose you to initialize your BehaviorSubject directly inside of the service.
#Injectable({
providedIn: 'root'
})
export class UserService {
//Details Page 1
public salutation: BehaviorSubject<string> = new BehaviorSubject('');
public firstName: BehaviorSubject<string> = new BehaviorSubject('');
And then within your Details1Component set values on them using next
goToDetails2(form : NgForm) {
this.userService.salutation.next(form.value.salutation);
this.userService.firstName.next(form.value.firstName);
localStorage.setItem('salutation', form.value.salutation));
localStorage.setItem('firstName', form.value.firstName));
}
Related
I'm new to typescript and angular and I was trying to fetch some data from firebase using angularfire2 and assign it to variables to use in some other functions later. I'm only familiar with javascript dot notation where I access members of the object using dot notation seems like it doesn't work with angular can somebody please help me with extracting data from the model to variables, please
I'm still having a hard time understanding Observable and subscribes too.
code
model
export class Reacts {
sad?: number;
happy?: number;
neutral?: number;
}
service
import { Injectable } from "#angular/core";
import {
AngularFirestore,
AngularFirestoreCollection,
AngularFirestoreDocument
} from "angularfire2/firestore";
import { Reacts } from "../models/reacts";
import { Observable } from "rxjs";
#Injectable({
providedIn: "root"
})
export class ReactService {
mapCollection: AngularFirestoreCollection<Reacts>;
reacts: Observable<Reacts[]>;
constructor(public afs: AngularFirestoreDocument) {
this.reacts = this.afs.collection("reacts").valueChanges();
}
getItems() {
return this.reacts;
}
}
component
import { Component, OnInit } from "#angular/core";
import { Reacts } from 'src/app/models/reacts';
import { ReactService } from 'src/app/services/react.service';
#Component({
selector: "app-reacts",
templateUrl: "./reacts.component.html",
styleUrls: ["./reacts.component.css"]
})
export class ReactsComponent implements OnInit {
react: Reacts[];
happy: number;
sad: number;
neutral:number;
constructor(private reactsService: ReactService ) {}
ngOnInit(): void {
this.reactsService.getItems().subscribe(reacts => {
this.react = reacts;
console.log(reacts); //this works print an array object of data from database
this.happy= reacts.happy// what i'm trying to achieve
});
}
}
Ok, I'll break it down for you. You are trying to access .happy but it is actually an array of React[]
ngOnInit(): void {
this.reactsService.getItems().subscribe((reacts:Reacts[]) => { // Note I have defined its model type
this.react = reacts;
console.log(reacts); //this works print an array object of data from database
//this.happy= reacts.happy // Now VS code will show you error itself
this.happy = reacts[0].happy;
});
}
The power of typscript comes as it is strongly typed language. If you'll make changes as below in service, the VS Code will itself explain you the error:
export class ReactService {
mapCollection: AngularFirestoreCollection<Reacts>;
reacts: Observable<Reacts[]>;
constructor(public afs: AngularFirestoreDocument) {
this.reacts = this.afs.collection("reacts").valueChanges();
}
getItems(): Observable<Reacts[]> { // added return type
return this.reacts;
}
}
Once I provide return type of getItems() , you dont even have to define type in .subscribe((reacts:Reacts[]) as I have done in your component.
I am using AngularJS for web app and in that I am trying to read data from APIs. Thus i have made few Models in accordance with the API's result set. Among many Models, Lets talk about a single Model TYPE
//This is the JSON API is returning
{
"records":[
{
"ID":"1",
"TYPE":"mythological"
}
],
"pagination":{
"count":"1",
"page":1,
"limit":10,
"totalpages":1
}
}
Now I have made the following Model for TYPE
//type.ts
export class Type {
"ID":string;
"TYPE":string;
}
After fetching the data from API i am successfully storing it and running through my code using following TYPE component ts.
//gallery.component.ts
import { Component, OnInit } from '#angular/core';
import { DataService } from 'src/app/services/data.service';
import { Type } from 'src/app/models/type';
#Component({
selector: 'app-gallery',
templateUrl: './gallery.component.html',
styleUrls: ['./gallery.component.scss']
})
export class GalleryComponent implements OnInit {
types: Type;
constructor(private data: DataService) { }
ngOnInit() {
}
clickfunction(){
this.data.getData().subscribe(data=>{
this.types=data.records;
console.log(this.types);
});
}
}
ALso, i am fetching data from this service
//data.service.ts
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
#Injectable({
providedIn: 'root'
})
export class DataService {
dataUrl:string = 'http://localhost/api-slim/public/index.php/api/info/type';
constructor(private http: HttpClient) { }
getData() {
return this.http.get(this.dataUrl);
}
}
Although the application is running its obviously giving me the following error, which i need to radicate.
Date: 2019-09-26T19:42:06.903Z - Hash: a1b41d5889df87ba0aa3
5 unchanged chunks
Time: 780ms
ℹ 「wdm」: Compiled successfully.
ERROR in src/app/components/gallery/gallery.component.ts(23,21): error TS2339: Property 'records' does not exist on type 'Object'.
NOW
The pagination data that the API is providing is common in each of the API response, but as you can see none of my models are consuming it. What would be the best way to store and use that pagination in each of my model. I have tried to made a temporary demo class in gallery.component.ts as follows,
export class TEMP {
records: TYPE[];
pagination: [];
}
But it's ugly. Is there any efficient fix?
Your model class doesn't really reflect the API response.
A model is like a custom data structure that you can use like a data type like this:
export TEMP { //consider renaming this to something more meaningful
records: Array<Type>;
pagination: Pagination;
}
export class Type {
ID: string;
TYPE: string;
}
export Pagination{
count: string;
page: number;
limit: number;
totalpages: number;
}
I am building a site that allows one to search for a beer and it returns data about that beer. The user clicks the search button, and it runs the http request I have setup on a service. All displays fine. But what I am trying to do is move my search form from the displaying component, to be inside the navbar. How do I link the search form on the navbar to the viewing component?
here is the home.component where the search form currently sits(clicking search runs the "searchBeer" function below passing the beer name being searched for:
#Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
constructor(private beerSearchService:BeerSearchService) { }
beerName:string;
beers:{};
selectedBeer: {};
searchBeer(beerName){
this.beers=null;
this.beerSearchService.searchBeer(beerName)
.subscribe(data => console.log(this.beers=data));
this.selectedBeer=null;
}
onSelect(beer): void {
this.selectedBeer = beer;
console.log(beer);
}
ngOnInit() {
}
}
EDIT... Had wrong service before.....
beer-search.service:
import { Injectable } from '#angular/core';
import {HttpClient} from '#angular/common/http';
#Injectable({
providedIn: 'root'
})
export class BeerSearchService{
constructor(private http: HttpClient) { }
searchBeer(name){
let data= {
query: `{beerSearch(query:"`+name+`"){items{id, name, style {description}, description, overallScore,
imageUrl, abv, brewer {name, facebook, web}}}}`,
variables:"{}",
operationName:null
}
return this.http.post('https://api.r8.beer/v1/api/graphql/', data, {
headers:{
'x-api-key': '<API-KEY>'}
});
}
}
If I move the search bar to the navbar component, how do I call this searchBeer function?
You store the results of API call to BehaviorSubject in the service, from navbar call the method to get beers from API and in component instead of subscribing to API result, subscribe to Observable (from BehaviorSubject of BeerS - your data):
BeerSearchService
export class BeerSearchService {
private _beers = new BehaviorSubject<Beer[]>(null);
constructor(private http: HttpClient) { }
searchBeer(beerSearch?: string) {
// do something with beerSearch parameter
let data = {
query: ` {topBeers{items{id, name, style {description}, description,
overallScore, imageUrl, abv, brewer {name, facebook, web}}}}`,
variables:"{}",
operationName:null
};
this.http.post('https://api.r8.beer/v1/api/graphql/', data, {
headers: {'x-api-key': '<api-key>'}
}).subscribe(data => {
this._beers.next(data);
});
}
get beers$() {
return this._beers.asObservable();
}
}
navbar.ts
export class NavbarComponent implements OnInit {
constructor(private beerSearchService: BeerSearchService) {}
searchBeer(beerName) {
this.beerSearchService.searchBeer(beerName);
}
}
Component.ts
export class HomeComponent implements OnDestroy {
beers:{};
sub: Subscription;
constructor(private beerSearchService: BeerSearchService) {
this.sub = this.beerSearchService.beers$.subscribe(beers => {
this.beers = beers;
});
}
ngOnDestroy() {
this.sub.unsubscribe();
}
}
I have a route which needs some data from my Firebase db before the route is loaded. It feels like the Route is not calling subscribe so the request is never being fired off. Am I missing a step?
(Angular 5)
My router:
{
path: 'class/:idName',
component: ClassComponent,
resolve: {
classData: ClassResolver
}
},
My Resolver:
#Injectable()
export class ClassResolver implements Resolve<any> {
constructor(
private db: AngularFireDatabase
) {}
resolve(route: ActivatedRouteSnapshot): Observable<any> | Promise<any> | any {
// return 'some data'; //This worked fine
return this.db
.list('/')
.valueChanges() // Returns Observable, I confirmed this.
//.subscribe(); // This returns a Subscriber object if I call it and I never get any data
}
// I tried this and it didnt work either
//const list = this.db
// .list('/')
// .valueChanges();
//console.log('list', list); // Is a Observable
//list.subscribe(data => {
// console.log('data', data); // returned data
// return data;
//});
//return list; // never gets to the component
}
My Component:
public idName: string;
// Other vars
constructor(
private fb: FormBuilder,
private route: ActivatedRoute,
private db: AngularFireDatabase
) {
// Form stuff
}
ngOnInit() {
// Never makes it here
this.idName = this.route.snapshot.params.idName;
const myclass = this.route.snapshot.data.classData;
console.log('myclass', myclass);
}
I never makes it to the component. It waits for the component to load, which it never does. If I add the subscribe and console.out the data it returns quite quickly with the correct data, so its not the service.
After calling .subscribe() in my Resolver that now returns a Subscriber object. Because my return signature allows for any its returning this Subscriber as if it was the data. This seems obvious now.
My question now becomes why isn't it resolving my Observable?
Your resolve function is returning an Observable that never completes. The Observable is indeed firing (and this can be verified by adding a tap to its pipeline with some console-logging)—but the resolve phase won't end (and therefore your component won't load) until the Observable completes. (The docs are not great at highlighting this.)
Obviously you don't want your Observable to complete either, because then you wouldn't get further data updates.
The simplest “fix” is to wrap your Observable in a Promise:
async resolve(route: ActivatedRouteSnapshot): Promise<Observable<any>> {
return this.db.list('/').valueChanges();
}
but this won't guarantee that Firebase has emitted its initial response, which I feel is what you're trying to ensure before the route loads.
The only approach I can see that would:
ensure that the component doesn't load until Firebase has returned data at least once; and
prevent two different Firebase reads (one by the resolver and then one by the component) for one effective operation
is to wrap your Firebase Observable in a service:
import { Injectable, OnDestroy, OnInit } from '#angular/core';
import { AngularFireDatabase } from '#angular/fire/database';
import { Subscription } from 'rxjs';
import { shareReplay } from 'rxjs/operators';
#Injectable({
providedIn: 'root',
})
export class DataService implements OnInit, OnDestroy {
constructor(private readonly db: AngularFireDatabase) {}
/**
* Observable to the data.
* shareReplay so that multiple listeners don't trigger multiple reads.
*/
public readonly data$ = this.db
.list('/')
.valueChanges()
.pipe(shareReplay({ bufferSize: 1, refCount: true }));
/**
* To trigger the first read as soon as the service is initialised,
* and to keep the subscription active for the life of the service
* (so that as components come and go, multiple reads aren't triggered).
*/
private subscription?: Subscription;
ngOnInit(): void {
this.subscription = this.data$.subscribe();
}
ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
}
and then your resolver would look like this:
async resolve(route: ActivatedRouteSnapshot): Promise<Observable<any>> {
// ensure at least one emission has occurred
await this.dataService.data$.pipe(take(1)).toPromise();
// ...then permit the route to load
return this.dataService.data$;
}
By wrapping your Firebase Observable in a service, you get OnInit and OnDestroy lifecycle hooks, which you can use to ensure that the observable "lives on" between component loads (and prevent multiple Firebase reads where one would suffice). Because the data is then hanging around, subsequent loads of the data would also be quicker. Lastly, this still enables you to use a resolver to ensure that the data will be instantly available before proceeding to load the component.
Your code looks to be correct. Have you been passing a parameter to your class route? It wont resolve without a parameter, that might be why you are not reaching your ngOnInit function. I would suggest console logging your route snapshots as well to make sure you are grabbing the right objects. I'll also post a resolve example that I got working:
Component.ts
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import { Observable } from 'rxjs/Observable';
#Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
public data: Observable<any>;
constructor(private router: ActivatedRoute) { }
ngOnInit() {
this.data = this.router.snapshot.data.test;
}
}
Routing.ts
{ path: 'home/:id', component: HomeComponent, resolve: { test: ResolverService } },
ResolverService
import { Injectable } from '#angular/core';
import { Resolve } from '#angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
#Injectable()
export class ResolverService implements Resolve<Observable<any>> {
constructor() { }
public resolve(route: ActivateRouteSnapShot): Observable<any> {
return Observable.of({test: 'Test Observable'});
}
}
HTML
{{this.data.test}}
You just need to add a take(1) operator to the Observable the resolver returns so that it completes.
resolve(route: ActivatedRouteSnapshot): Observable<any> {
return this.db.list('/').valueChanges()
.pipe(take(1)); // <-- The Magic
}
#AlexPeters was on the right track, but you don't have to go so far as to return a promise. Just force the completion with take(1). Alex is also spot-on that the docs are not very clear on this. I just spent an couple hours debugging this same issue.
EDIT: Comment by OP:
"Sorry , but I think I had had slight typo in enviroment/environment, sorry for wasting your time ,it seems to work now"
I have having trouble passing data from app components to child component in angular 2 . I recently started toying with angular 2 and trying to understand how it works. I tried to used the concept shown in this tutorial to do pass data to child component
https://angular.io/docs/ts/latest/tutorial/toh-pt3.html
But I think I am missing something
Here is my project: App component:
import { Component, ViewChild } from '#angular/core';
import { WorkflowService } from './components/workflow_display/workflow.service';
import { WorkflowDisplayComponent } from './components/workflow_display/workflow-display.component';
import { PropertyService } from './shared/property.service';
import '../../public/css/styles.css';
#Component({
selector: 'my-app',
template: require('./app.component.html')
})
export class AppComponent {
title = 'Hello World';
#ViewChild("taskDisplay") workflowDisplay: WorkflowDisplayComponent;
myEnvironment: String; //the variable I am trying to bind from
errorMessage: String;
workbenchBaseUrl : String = 'workbenchBaseUrl';
public selectedNavID : String = 'workspace_control_workStreamView';
public isWorkOrdersCollapsed = false;
public isWorkStreamsCollapsed = false;
constructor(private _propertyService : PropertyService){
}
ngOnInit(): void {
this._propertyService.getValue(this.workbenchBaseUrl)
.subscribe(environment => this.myEnvironment = environment,
error => this.errorMessage = <any>error);
}
}
app.component.html
<div>
<div>
<div>
<!--some html-->
<main class="col-sm-9 offset-sm-3 col-md-10 offset-md-2 pt-3 mh-100">
<workflow-display [environment] ="myEnvironment" #taskDisplay></workflow-display>
</main>
</div>
</div>
</div>
WorkDisplay component
import { Component, Input} from '#angular/core';
import { OnInit } from '#angular/core';
import { IGrcTask } from './grc-task';
import { WorkflowService } from './workflow.service';
import { PropertyService } from '../../shared/property.service';
#Component({
selector: 'workflow-display',
template: require('./workflow-display.component.html')
})
export class WorkflowDisplayComponent implements OnInit {
taskMode: string = 'workstream'; // 'workorder' or 'workstream' to currently identify the columns to display
taskQuery: string = 'process=workstream&taskStatus=RUNNING'; // the query parameters to pass to the tasks web service
workbenchUrl: string = 'http://localhost:8081'; // workbench URL
workbenchTaskPage: string = 'wsIndex'; // workbench page to use to open tasks
infoMessage: string;
errorMessage: string;
tasks: IGrcTask[];
currentTask: IGrcTask;
#Input()
environment: String; //the variable I am trying to bind to
workbenchBaseUrl : String = 'workbenchBaseUrl';
constructor() {
}
//called when user clicks a row
openTask(event: any, task: any) {
// this.environment is still undefined
window.open(this.environment + this.workbenchTaskPage + "?taskId=" + task.taskId + "&activitiWorkflow=true");
}
}
WorkDisplay.component.html
<--!some html-->
<tbody *ngIf='(taskMode == "workorder") && tasks && tasks.length'>
<ng-container *ngFor='let task of tasks; let i=index'>
<tr (click)="setCurrentTask($event, task)" (dblclick)="openTask($event, task)"
<--!some html-->
Property.service.ts
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/do';
import 'rxjs/add/operator/map';
/**
* Service return Property value/values from the project property file
*
*/
#Injectable()
export class PropertyService {
//ReST Url for the PopertyService on the back end
private _url = '/grcworkflow/resources/grcWorkflow/environment/';
constructor(private _http: Http) {}
/**
* Method return an Observable<String -> Value> for any property
* Method make an http get call to the server to fetch the property
* #Param key for the property in the property file
*/
getValue(key: String): Observable<String> {
return this._http.get(this._url+key)
.map((response: Response) => <String> response.text())
.do(data => console.log('All: ' + data))
.catch(this.handleError);
}
private handleError(error: Response) {
return Observable.throw(error.json().error || 'Server error');
}
}
NOTE I have removed some function definitions and variable from the components which might be irrelevant.
I am trying to bind myEnviroment value of the app.component enviroment value. myEnviroment get set when proerty service returns a string. Although enviroment value still stays undefined .
I am looking for one way binding i.e when myEnvironment(parent) changes environment(child) should change too. But this doesn't seem to happen. Please help out here