I have 1 feature module (Fund module) that displays fund data in one of its components and an AppModule that displays Advisor data. Fund data and Advisor data have one to many relationship.
Fund component gets data from services defined in the AppModule.
Data are passed to the Fund module using BehaviorSubject as data might change if the user updates or modifies the data in AppModule.
The subscription of the data in not working correctly. The fund module only displays the value (10) that the BehaviorSubject is initialized with. It doesn't display the updated value (100), nor does the subscription work properly to display the values second time.
Here's the code:
Service in AppModule:
test = new BehaviorSubject<number>(10);
getTest(): Observable<number> {
return this.test.asObservable();
}
public updateLocalData(sleeves: Sleeve[]): void {
.... update logic
this.test.next(100);
}
FundDataComponent in Fund Module
changeDetection: ChangeDetectionStrategy.OnPush,
ngOnInit(): void {
this.test = this.service.getTest().subscribe((number) => {
this.number = number;
console.log(number); // get's called only once to display 10
});
}
Create a model - resource.ts:
import { BehaviorSubject, Observable } from 'rxjs';
import { refCount, publishReplay } from 'rxjs/operators';
export class StreamResource {
private loading = false;
private behaviorSubject: BehaviorSubject<any>;
public readonly obs: Observable<any>;
constructor(defaultValue?: any) {
this.behaviorSubject = new BehaviorSubject(defaultValue);
this.obs = this.behaviorSubject.asObservable().pipe(publishReplay(1), refCount());
}
request(method: any): void {
if (method && !this.loading) {
this.loading = true;
method().toPromise().then((data: any) => {
if (data) { this.update(data); }
this.loading = false;
});
}
}
getValue() {
return this.behaviorSubject.getValue();
}
update(data: any) {
this.behaviorSubject.next(data);
}
refresh() {
const data = this.getValue();
if (data) { this.update(data); }
}
}
Create a StreamService
import { StreamResource } from '../models/resource';
public test = new StreamResource(10);
...
getTest() {
this.test.request(() => this.testService.getTest());
}
How to use?
constructor(private streamService: StreamService) { }
ngOnInit() {
this.streamService.test.obs.subscribe((data: any) => {
if (!data) {
return this.streamService.getTest();
} else {
this.test = data;
}
});
Related
Not an expert at angular far from it. But I've been looking deeply and i cant figure out why my other components that call the function types, runs before the constructor. and to solve it where do i put the " echo " function? everything works likes a charm except for the fact that echo is called before types. what or how do i make echo come first to run before any other function. i cant hook it up to the promise because it takes data from another component. i ran a if statement to check if the global variable exist and obviously doesn't because of the order of processes.
import { Injectable, OnInit, OnDestroy } from '#angular/core';
import { HttpClient, HttpHeaders } from '#angular/common/http'
import { Observable, of } from "rxjs";
import { Router, ActivatedRoute } from '#angular/router';
import { Location } from '#angular/common/'
import { DataService } from './products.service';
import { BehaviorSubject, Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
#Injectable({ providedIn: "root" })
export class CartService implements OnInit, OnDestroy {
public data: any = { "productObjs": [] }
public array: any;
public datap: any;
private sub: Subscription;
//loop up the id if specexist remove thespec if empty remove id
constructor(public dataservice: DataService, private http: HttpClient) {
this.echo()
}
echo() {
let headers = new HttpHeaders();
headers.append('Content-Type', 'application/json');
let prom = new Promise((resolve, reject) => {
this.http.get('../assets/productCategories/products.json', { headers }).toPromise().then((data: any) => {
console.log(data)
var dat = this.datap
resolve(dat)
this.datap = data
return dat
}).then((dat) => { this.nextt(dat) });
})
return this.datap;
}
nextt(datap) {
console.log(datap)
this.datap = datap
}
// types is called from another component and runs before promise finishes
types(type) {
if (this.datap) {
console.log(this.datap)
let that = this
var func = type.func
var rtype = type.type
var arr;
switch (func) {
case "random":
return that.sortByRandom()
break;
case "category":
return that.sortByCategory(rtype)
break;
default: "specific"
return that.sortBySpecific(rtype)
}
console.log(this.array)
console.log(this.array)
console.log(this.array)
console.log(this.datap)
return this.array;
}
}
getArray() {
if (this.datap) {
console.log(this.array)
return this.array
}
}
sortBySpecific(specific) {
let that = this
console.log(that.datap)
var type = that.datap.product.filter(function(objj) {
return objj.type === specific;
})
return type
}
sortByCategory(category) {
let that = this
var type = this.datap.product.filter(function(objj) {
return objj.productCategory === category;
})
return type
}
sortByRandom() {
var cats = []
var picked = []
var sproducts;
let that = this
this.datap.productObjs.forEach((obj) => {
var randomnum2 = Math.floor(Math.random() * this.datap.productObjs.length)
cats.push(obj.category)
})
var randomnum = Math.floor(Math.random() * cats.length)
var selectedCats = this.datap.product.filter(function(objj) {
return objj.productCategory === cats[randomnum];
});
sproducts = selectedCats
var x = sproducts[Math.floor(Math.random() * sproducts.length)]
picked.push(x)
that.array = picked
return picked
}
addToCart(ps, pobj) {
var checkarray = this.data.productObjs.filter(function(obj) {
return obj.productSpec === ps;
});
console.log(checkarray)
if (checkarray.length <= 0) {
this.data.productObjs.push(pobj)
}
}
getItems() {
return this.data.productObjs
}
clearCart() {
this.data.productObjs = []
}
clearProduct(objspec) {
var newarray = this.data.productObjs.filter(function(obj) {
return obj.productSpec !== objspec;
});
this.data.productObjs = newarray;
}
changeInventory() {
//update pricing from inputs
}
checkout() {
this.http.post('http://localhost:4201/api', this.data).subscribe((res) => {
console.log(res)
var json = res
if (json['bool'] === "false") {
//cant check out
// this checks inventory also.
//pop up error problem with pricing.
}
if (json['bool'] === "true") {
//can check out
//neeeds to request paypal to send productinfo and once payment response is succeded send valid, and delete from database.
}
})
}
ngOnInit() {
}
ngOnDestroy() {
this.sub.unsubscribe();
console.log(this.sub)
console.log(this.datap)
}
}
Check this article for how to initialize global data:
https://www.cidean.com/blog/2019/initialize-data-before-angular-app-starts/
BTW, you should never call business logic in a constructor like:
this.echo()
Instead you should call it in the component it need the data, maybe ngOnInit in that component when it is needed.
It is usually recommended to use constructor only for dependency injection mainly. For other initialization consider using Angular life cycle hooks (Difference between constructor and ngOnInit).
Since you want to run your function echo() which gets called from child component you can call it from ngOnInit() in child component.
Then if you have anything that needs to be called from parent to child. You can call child from parent components ngAfterViewInit() method. (In your case types() function)
So, I probably don't understand Observables well. I have snippet like this, and would like to access todos stored in this service via function (from another component) defined in service. Unfortunately, I have no idea how to do this.
todos;
// fetching todos from api
fetchTodos() :Observable<Todo[]>{
return this.http.get<Todo[]>(api_url);
}
constructor(private http:HttpClient) {
this.fetchTodos()
.subscribe(data => this.todos = data)
}
To do it right solve your problem as follows.
SERVICE
import { BehaviorSubject, Observable } from 'rxjs';
/* make the plain object a subject you can subscribe to */
todos: BehaviorSubject<Todo[]> = new BehaviorSubject<Todo[]>([]);
constructor(private http:HttpClient) {
/* get the data and put it in the subject */
/* every subscriber to this event will be updated */
this.fetchTodos().subscribe(data => this.todos.next(data));
}
getTodos(): Observable<Todo[]> {
return this.todos.asObservable();
}
// fetching todos from api
private fetchTodos(): Observable<Todo[]> {
return this.http.get<Todo[]>(api_url);
}
COMPONENT
constructor(private service: Service) {}
ngOnInit(): void {
/* here you go. this subscription gives you the current content of todos[] */
/* whenever it gets updated */
this.service.getTodos().subscribe(data => {
console.log(data);
});
}
PLEASE NOTE
Subscriptions to an Observable should always be finished when you leave a component. The best way to reach this goal in your case is:
modified COMPONENT
import { Subscription } from 'rxjs';
private subscription: Subscription = new Subscription();
constructor(private service: Service) {}
ngOnInit(): void {
/* add each subscriber to the subscription */
this.subscription.add(
this.service.getTodos().subscribe(data => {
console.log(data);
});
);
}
ngOnDestroy(): void {
/* unsubscribe all subscribers at once */
this.subscription.unsubscribe();
}
I have a DataServive, that fetches content from an API:
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Observable } from 'rxjs';
import { map, catchError, retry } from 'rxjs/operators';
import { environment } from 'src/environments/environment';
#Injectable()
export class DataService {
this.request = {
count: 10
}
constructor(private http: HttpClient) { }
private handleError(error) {
console.log(error);
}
public getData(count): Observable<any> {
this.request.count = count;
return this.http.post<any>(environment.api + '/path', this.request).pipe(
map(response => {
return response;
}),
catchError(error => {
this.handleError(error);
return [];
})
);
}
}
This DataServie is consumned by a component like this:
ngOnInit() {
const subscriber = this.dataService.getData(this.count).subscribe((data) => { this.data = data; });
}
And it works fine.
However the user is able to change the variable this.count (how many items should be displayed) in the component. So I want to get new data from the server as soon as this value changes.
How can I achieve this?
Of course I could call destroy on this.subscriber and call ngOnInit() again, but that dosn't seem like the right way.
Easiest ways is just to unsubscribe:
subscriber: Subscription;
ngOnInit() {
this.makeSubscription(this.count);
}
makeSubscription(count) {
this.subscriber = this.dataService.getData(this.count).subscribe((data) => { this.data = data; });
}
functionInvokedWhenCountChanges(newCount) {
this.subscriber.unsubscribe();
makeSubscription(newCount);
}
But because count argument is just a one number it means HTTP always asks for data from 0 to x. In this case, you can better create another subject where you can store previous results (so you don't need to make useless HTTP requests) and use that subject as your data source. That needs some planning on your streams, but is definitely the preferred way.
When the user changes count, call getData(count) again with the updated count value. Need to see your html file, but having a button with (click)="getData(count)" may help.
I am writing an Angular Service to prove a Users permissions. In the constructor I want to get the current logged in user from an API. The current User which is created is used in other methods of this Service. This Methods are called from components to check which things can be shown and so on.
The problem is that the methods in the service are called faster than the current user is available.
Are there any possibilities solving this issue?
permission.service.ts
#Injectable()
export class PermissionService {
currUser = {
'id': "",
'permission': ""
};
apiService: AlfrescoApiService;
authService: AuthenticationService;
constructor(apiService: AlfrescoApiService, authService: AuthenticationService) {
this.apiService = apiService;
this.authService = authService;
this.init();
}
init() {
let userId: string = this.authService.getEcmUsername();
this.currUser.id = userId;
//API call
this.apiService.sitesApi.getSiteMember(SITENAME, userId).then(resp => {
this.currUser.permission = resp.entry.role;
})
}
isSiteManager(): boolean {
console.log(this.currUser.permission, this.currUser);
if(this.currUser.permission === "SiteManager"){
return true;
}else{
return false;
}
}
}
method call
export class AppLayoutComponent {
constructor(permissionService:PermissionService) {
permissionService.isSiteManager();
}
}
output in Google Chrome
{id: "admin", permission: ""}
id: "admin"permission: "SiteManager"
You should use promise in your getEcmUsername() handle this; after that you can code like this
`
this.authService.getEcmUsername().then((userID) => {
this.apiService.sitesApi.getSiteMember(SITENAME, userId).then(resp => {
this.currUser.permission = resp.entry.role;
})
});
`
In my opinion better solution is to use Observable here and rxjs. In service you can create Subject and subscribe it inside your compnent, to be sure data is already there. E.g.:
#Injectable()
export class PermissionService {
public Subject<bool> userFetched= new Subject<bool>();
currUser: IUser = {
'id': "",
'permission': ""
};
apiService: AlfrescoApiService;
authService: AuthenticationService;
constructor(apiService: AlfrescoApiService, authService: AuthenticationService) {
this.apiService = apiService;
this.authService = authService;
this.init();
}
init() {
let userId: string = this.authService.getEcmUsername();
this.currUser.id = userId;
//API call
this.apiService.sitesApi.getSiteMember(SITENAME, userId).subscribe((data:IUser)=>
{
this.user=data;
this.userFetched.next(true);
})
}
isSiteManager(): boolean {
console.log(this.currUser.permission, this.currUser);
if(this.currUser.permission === "SiteManager"){
return true;
}else{
return false;
}
}
}
After that in your component:
export class AppLayoutComponent {
constructor(permissionService:PermissionService) {
permissionService.userFetched.subscribe((data)=>{
permissionService.isSiteManager();
});
}
}
It's better approach. You need to consider if better is Subject or BehaviourSubject.
Thanks to everybody who answered. I have found a solution. I have changed the isSiteManager() method to a method which checks all four permissiontypes. This method is executed in the then() block and affects four variables for each permissiontype. These variables i can reach from other components.
Looks like this:
#Injectable()
export class PermissionService {
isSiteManager: boolean;
isSiteConsumer: boolean;
isSiteContributor: boolean;
isSiteCollaborator: boolean;
userId : string;
constructor(private apiService: AlfrescoApiService, private authService: AuthenticationService) {
this.init();
}
init() {
this.isSiteCollaborator = false;
this.isSiteConsumer = false;
this.isSiteContributor = false;
this.isSiteManager = false;
this.userId = localStorage.USER_PROFILE;
//proof permission of user
this.apiService.sitesApi.getSiteMember(SITENAME, this.userId).then(resp=>{
if(resp.entry.role === "SiteManager"){
this.isSiteManager = true;
}else if(resp.entry.role === "SiteConsumer"){
this.isSiteConsumer = true;
}else if(resp.entry.role === "SiteContributor"){
this.isSiteContributor = true;
}else{
this.isSiteCollaborator = true;
}
});
}
}
Now i can ask for the variables in other components like this:
export class AppLayoutComponent {
constructor(private permissionService : PermissionService) {
if(permissionService.isSiteManager){
console.log("You are Boss!");
}
}
}
You should call your service method synchrony. To do so, you have to map your response from the service:
your component code:
constructor() {
...
permissionService.isSiteManager().map(
response => {
isManager = response;
}
);
}
Something like this.
To call map operator import it before:
import 'rxjs/add/operator/map';
Continue to the Using angular material 2 table to display the result from backend based on user's current location
My purpose for this code is when user enter the site, it will try to ask user the current location. Once my front end get current lat/lon, it will pass to backend to get the nearest restaurant based on user's location, and using angular material table to display it. But when I testing on Chrome, I got weird behavior, the home page will not display the result immediately on the first time, try refresh, doesn't work, the only way make it works is switch another tab, and back to this one, it will display the result in angular material table.
Here is the code for home.component.ts
import { Component, OnInit } from '#angular/core';
import { Http, Response, URLSearchParams } from '#angular/http';
import { DataSource } from '#angular/cdk';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/startWith';
import 'rxjs/add/observable/merge';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/frompromise';
import { Restaurant } from '../restaurant/restaurant';
import { Category } from '../category/category';
import { RestaurantService } from '../restaurant/restaurant.service';
#Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
displayedColumns = ['Id', 'Name', 'Category', 'Address', 'City'];
dataSource: ExampleDataSource | null;
constructor(http: Http) {
//this.exampleDatabase = new ExampleHttpDatabase(http, this.location);
this.dataSource = new ExampleDataSource(http);
}
ngOnInit() {
this.dataSource.connect();
}
}
export class ExampleDataSource extends DataSource<Restaurant> {
private url = 'api/search/location';
private params = new URLSearchParams();
private lat;
private lon;
constructor(private http: Http) {
super();
}
/** Connect function called by the table to retrieve one stream containing the data to render. */
connect(): Observable<Restaurant[]> {
// var location;
// if (navigator.geolocation){
// var options = {timeout: 60000};
// location = navigator.geolocation.getCurrentPosition((position)=>{
// return position;
// },(err) =>{
// console.log("Error")
// }, options);
// }
// console.log("Locations: " + location);
var result = this.getCurrentLocation().then((res) =>
{
return res;
});
return Observable.fromPromise(result);
}
disconnect() { }
getPosition = () => {
var latitude, longitude;
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition((position) => {
resolve(position.coords);
}, (err) => {
reject(err);
});
})
}
async getCurrentLocation(): Promise<Restaurant[]> {
let coords = await this.getPosition();
this.lat = coords['latitude'];
this.lon = coords['longitude'];
this.params.set('lat', this.lat);
this.params.set('lon', this.lon);
var result = this.http.get(this.url, { search: this.params }).map(this.extractData);
return await result.toPromise();
}
extractData(result: Response): Restaurant[] {
return result.json().map(restaurant => {
return {
id: restaurant.id,
name: restaurant.restaurant_name,
category: restaurant.category.map(c => c.categoryName).join(','),
address: restaurant.address.address,
city: restaurant.address.city.cityName
}
});
}
}
I don't know what I did wrong.. can someone help me? For the full code, please see https://github.com/zhengye1/Eatr/tree/dev
Finally solved....
All I need to do just change ngOnInit on HomeComponent class to following
async ngOnInit() {
await this.dataSource.connect();
}
and it works.. don't know why...