Observable rxjs filter - javascript

In my project, I wanted to create some sort of "Recommended Products" in each product page,
but having trouble with making my function filtering an observable.
I have tried using .pipe(filter()) in different ways, but to no use.
Basically the fucntion should filter products with the same type and id, and show them in the proper product page, but pretty much got stuck after subscribing all of my products(which is marked down below).
Much Appreciated!
import { Component, OnInit } from '#angular/core';
import { ProductService } from '../services/product.service';
import { ActivatedRoute, ParamMap, Router } from '#angular/router';
import Product from '../interfaces/product';
import { map, filter} from 'rxjs/operators';
import { Observable } from 'rxjs';
#Component({
selector: 'app-product',
templateUrl: './product.component.html',
styleUrls: ['./product.component.css']
})
export class ProductComponent implements OnInit {
recommandedProducts: Product[];
allProducts:Product[];
// allProducts:Observable< Product> = new Observable< Product>();
product: Product;
constructor(private productService: ProductService, private route: Router, private actRoute: ActivatedRoute) { }
ngOnInit() {
this.findRecommendedProducts(this.product)
};
//From ProductService:
// getProducts(){
// return this.http.get(`${this.uri}`);
// }
findRecommendedProducts(currectProduct: Product){
this.productService.getProducts().subscribe((data: Product[]) => {
this.allProducts = data;
console.log(this.allProducts)
this.recommandedProducts = this.allProducts.filter(otherProduct =>
otherProduct.type == currectProduct.type && otherProduct.id == currectProduct.id)
console.log(this.recommandedProducts);
});
};
}

A filter in rxjs is not the same as an Array.filter. In rxjs, a filter is used to emit values that pass the provided condition. So if you use a filter, based on the condition, the observable will either emit your data or return nothing.
Instead, what you need is pipe(map) along with an Array.filter. Also, as #jzzfs pointed out, your error shows currentProduct could be undefined, so you can pass a default value in your findRecommendedProducts function.
Try the below code.
findRecommendedProducts(currectProduct: Product = {} as Product) {
this.productService.getProducts()
.pipe(
map((products: Product[]) => products.filter(product => product.type == currectProduct.type && product.id == currectProduct.id))
)
.subscribe((data: Product[]) => {
this.recommandedProducts = data;
});
};
Now your subscribed data should directly return the recommendedProducts.

Looks like the currectProduct passed onto findRecommendedProducts is undefined, since the logged this.allProducts do contain values.
With that being said, notice that when you define product: Product, you've only defined its type but you have not initialized it. The default value is therefore undefined -- if I remember correctly.
So change product: Product to product: Product = {};, for instance or pass a value to it within the constructor or within ngInit.

Related

Lazy Loading data with a RxJS Subject. Last batch is not displayed, the subject never completes

I'm lazy loading data using a fake backend. The backend returns an array. Because I want to lazy load the data I'm buffering it every 100 records. The event that will trigger the call for getting more data is going to be a custom event, but for the time being I'm testing it with a button.
multiselect.service.ts
import { Injectable } from '#angular/core';
import { HttpClient, HttpHeaders, HttpResponse } from '#angular/common/http';
import { BehaviorSubject } from 'rxjs';
const httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
export interface ProductCategory {
id: number;
name: string;
}
#Injectable({
providedIn: 'root'
})
export class MultiSelectService {
constructor(private http: HttpClient) { }
private readonly categorySubject = new BehaviorSubject(undefined);
readonly categories$ = this.categorySubject.asObservable();
// URL to web api - mocked. Categories is the object present in the mock server file
private categoriesUrl = 'api/categories';
/** GET heroes from the server */
getCategories(): void {
this.http.get<Array<ProductCategory>>(this.categoriesUrl, httpOptions)
.subscribe( data => {
data.map( category => this.categorySubject.next(category));
this.categorySubject.subscribe( xyz => console.log(xyz));
});
}
}
multiselect.component.ts
import { Component, AfterViewInit } from '#angular/core';
import { zip, Observable, fromEvent } from 'rxjs';
import { MultiSelectService, ProductCategory } from './multiselect.service';
import { bufferCount, startWith, map, scan } from 'rxjs/operators';
#Component({
selector: 'multiselect',
templateUrl: './multiselect.component.html',
styleUrls: ['./multiselect.component.scss']
})
export class MultiselectComponent implements AfterViewInit {
SLICE_SIZE = 100;
constructor(private data: MultiSelectService) { }
ngAfterViewInit() {
const loadMore$ = fromEvent(document.getElementsByTagName('button')[0], 'click');
this.data.getCategories(); // loads the data
zip(
this.data.categories$.pipe(bufferCount(this.SLICE_SIZE)),
loadMore$.pipe(startWith(0)),
).pipe(
map(results => results[0]),
scan((acc, chunk) => [...acc, ...chunk], []),
).subscribe({
next: v => console.log(v),
complete: () => console.log('complete'),
});
}
}
multiselect.component.html
<button>Fetch more results</button>
1) The elements count is 429, and I'm showing them in batches of 100 elements. After four clicks (4 x 100) the last 29 are never shown. What am I missing to display the last 29 elements? I was able to check that the subject produced all the values.
2) Is there any better way of achieving the same functionality that I'm developing here? I'm mapping (it can be a forEach loop instead) the array due to the fact that after requesting the data I will only get one item (the array) with all the elements (429). That won't allow me to buffer the items if I want to do lazy loading.
3) I'm required to provide an initial value for the behaviorSubject, is there any way to avoid that?
Thanks in advance!
This seems like a very convoluted way of doing things
data.map( category => this.categorySubject.next(category));
This line returns a new array the same length as data containing undefined in each slot, if you want to create an observable from an array then you go
this.categories$ = from(data);
A BehaviorSubject with no initial value is a Subject.
All that aside you already have your data in an array, no need to create a new observable from it.
Have a BehaviorSubject with a number in it and each key press increment the number and use a combineLatest to get the amout of elements you want from the array.
combineLatest(categories$, page$).pipe(map(([categories, page]) => categories.filter((item, index) => index < page * pageSize)))
Is all you really need to do where you start page at 1 and increment it each button click.

Ionic & AngularFireDatabase list result - Property 'map' does not exist on type '{}'

I am trying to get data from Firebase by the list function and to use it in Json format (to load markers on my Google Map). But I'm facing an issue with the returned snapshots: Property 'map' does not exist on type '{}'
My imports:
import { Component, ViewChild, ElementRef } from '#angular/core';
import { NavController, IonicPage } from 'ionic-angular';
import { Geolocation } from '#ionic-native/geolocation';
import { AngularFireDatabase } from 'angularfire2/database';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
And the code snippet with the error:
markers: Observable<any[]>;
constructor(public navCtrl: NavController,
public geolocation: Geolocation,
public db: AngularFireDatabase) {
this.markers = this.db.list("empresa/", {preserveSnapshot: true})
.map(snapshots => {
return snapshots.map(snapshot => { // this second .map displays the error
return snapshot.val();
})
.subscribe((marcas)=> {
console.log(marcas);
});
});
}
Because of this, I think, the subscribe does not display anything on the console. I also tried to import in some different ways the rxjs, as in this link Property 'map' does not exist on type 'Observable<Response>', but without success.
Seems like a migration issue - since AF 5.0 list returns an AngularFireList<T> service, not FirebaseListObservable, you have to call an action on it to stream data into an observable collection, e.g.:
const subscription = this.db.list("empresa/").snapshotChanges()
.map(snapshots => {
return snapshots.map(snapshot => snapshot.payload.val());
})
.subscribe(values => {
console.log(values);
this.markers = values;
});
Note also that the subscribe is chained after the outer map, not the inner as in your example; and this.markers are being assigned results inside the subscription, so you have to change their type from Observable<any[]> to any[] (or whatever they are, just not observable).
See 3. Retrieving data as lists in the docs for details; there's an example at the end of the page on using the listed items inside an angular template with async pipe, where you don't need to subscribe, then you'd keep this.markers as Observable<any[]> and the rest would be:
this.markers = this.db.list("empresa/").snapshotChanges()
.map(snapshots => {
return snapshots.map(snapshot => snapshot.payload.val());
});
This is preferred as the async pipe manages the subscription for you (because if you are subscribing manually you should also unsubscribe).

Angular 2 - http.get never being call

Im learning Angular 4 and have run into a problem that I cannot seem to find a solution to. Here is the context:
I have a simple app that displays info about US Presidents.
The backend is a rest API provided by webapi...this works fine.
The front end is an Angular app.
Ive distilled the problem down to 3 components, 1 data service and 1 model.
Here is the model:
export class President {
constructor(
public id: number,
public presidentName: string,
public presidentNumber: number,
public yearsServed: string,
public partyAffiliation: string,
public spouse: string) {}
}
The 3 components are
1. SearchComponent
2. HomeComponent
3. PresidentComponent
When the app bootstraps, it loads the ApplicationComponent - it is the root component:
import { Component, ViewEncapsulation } from '#angular/core';
#Component({
selector: 'my-app',
template: `
<search-component></search-component>
<home-component></home-component>
`
})
export class ApplicationComponent {}
PresidentComponent is a child component of HomeComponent. When home component loads, it makes an http call to the api to get a list of presidents and renders 1 presidentComponent for each row returned. This works fine.
What Im trying to do is implement a search feature where the dataService exposes an EventEmitter and provides the search method as shown here:
import { Injectable, EventEmitter, Output } from '#angular/core'
import { President } from '../models/President'
import { Http } from '#angular/http';
import 'rxjs/add/operator/toPromise';
import 'rxjs/add/operator/map';
import { Observable } from 'rxjs/Observable';
#Injectable()
export class DataService {
constructor(private http: Http) {
}
searchEvent: EventEmitter<any> = new EventEmitter();
// simple property for the url to the api
get presidentUrl {
return "http://localhost:51330/api/presidents";
}
search(params: any): Observable<President[]> {
let encParams = encodeParams(params);
console.log(encParams);
return this.http
.get(this.presidentUrl, {search: encParams})
.map(response => response.json());
}
getParties(): String[] {
return ['Republican', 'Democrat', 'Federalist', 'Whig', 'Democratic-Republican', 'None'];
}
getPresidents(): Observable<President[]> {
return this.http.get(this.presidentUrl)
.map(response => response.json());
}
}
/**
* Encodes the object into a valid query string.
* this function is from the book Angular2 Development with TypeScript
*/
function encodeParams(params: any): URLSearchParams {
return Object.keys(params)
.filter(key => params[key])
.reduce((accum: URLSearchParams, key: string) => {
accum.append(key, params[key]);
return accum;
}, new URLSearchParams());
}
The Search Component houses the search form and when the search button is clicked, it executes the onSearch() function and calls emit on the data service:
onSearch(){
if(this.formModel.valid){
console.log('emitting event from search.ts');
this.dataService.searchEvent.emit(this.formModel.value);
}
}
Then, in the HomeComponent, I want to subscribe to this event and execute a search via the dataservice when it fires:
ngOnInit(): void {
//when component loads, get list of presidents
this.dataService.getPresidents()
.subscribe(
presidents => {
console.log('sub');
this.presidents = presidents;
},
error => console.error(error)
)
//when search event is fired, do a search
this.dataService.searchEvent
.subscribe(
params => {
console.log('in home.ts subscribe ' + JSON.stringify(params));
this.result = this.dataService.search(params);
},
err => console.log("cant get presidents. error code: %s, URL: %s"),
() => console.log('done')
);
}
When I run this in the browser, everything works except the http call is never executed. If I subscribe() to the http.get call in the dataservice itself, it executes but why should I have to do that when I have a subscription being setup on the HomeComponent?
I want to handle the Observable in the HomeComponent and update the list of presidents that is being displayed in the UI based on the search result.
Any advice is greatly appreciated.
The entire idea of using EventEmitter in the service is not right. The EventEmitter should be used with #Output properties to send data from the child component to its parent.
Even though the EventEmitter is a subclass of the Subject, you shouldn't be using it in services. So inject the service into your component, subscribe to its observable in the component, and emit an event using EventEmitter to the parent component if need be.
In the code this.result = this.dataService.search(params);, result is an observable. You have not made a subscription.
In that case you should have used the async pipe to display the data.
Why not use Subject from rxjs. Here is what i am proposing:
DataService:
import { Observable, Subject } from "rxjs";
import 'rxjs/add/operator/catch';
#Injectable()
export class DataService {
private _dataSubject = new Subject();
constructor(private http: Http) {
this.http.get(this.presidentUrl)
.map(response => this._dataSubject.next(response.json()))
.catch(err => this._dataSubject.error(err));
);
}
// simple property for the url to the api
get presidentUrl {
return "http://localhost:51330/api/presidents";
}
search(params: any){
let encParams = encodeParams(params);
console.log(encParams);
this.http
.get(this.presidentUrl, {search: encParams})
.map(response => this._dataSubject.next(response.json()))
.catch(err => this._dataSubject.error(err));
}
getParties(): String[] {
return ['Republican', 'Democrat', 'Federalist', 'Whig', 'Democratic-Republican', 'None'];
}
getPresidents(): Observable<President[]> {
return this._dataSubject;
}
SearchComponent:
onSearch(){
if(this.formModel.valid){
console.log('emitting event from search.ts');
this.dataService.search(this.formModel.value);
}
}
With these modifications you should be able to have only 1 subscriber in homeCompoent and then get new data emitted every time onSearch() is called.

Subscribing to an observable before it's been populated

I'm trying to create a user profile service for an Angular 4 project and struggling a little with how to properly initialize and update the observable Profile object. Currently, when the user authenticates (via Firebase), AuthService passes the user's auth info to UserProfileService via the latter's initialize() function. UserProfileService then looks up the user's profile (or creates one if none exists yet) and populates a public observable with the profile.
The problem I'm running into is with other parts of the application trying to subscribe to the profile observable before all this has happened. I'd originally been initializing the observable via ...
public profileObservable: UserProfile = null;
... which of course resulted in a "subscribe() does not exist on null" error, so I changed it to ...
public profileObservable: Observable<UserProfile> = Observable.of();
This at least doesn't throw any errors, but anything that subscribes to profileObservable before I've mapped the Firebase object to it never updates.
Complete code for user-profile.service.ts below. I'm still struggling to get my head around how some of this is meant to work, so hopefully someone can shed some light. Thanks!
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { FirebaseListObservable, FirebaseObjectObservable, AngularFireDatabase } from 'angularfire2/database';
import * as firebase from 'firebase/app';
export class UserProfile {
$exists: Function;
display_name: string;
created_at: Date;
}
#Injectable()
export class UserProfileService {
private basePath: string = '/user-profiles';
private profileRef: FirebaseObjectObservable<UserProfile>;
public profileObservable: Observable<UserProfile> = Observable.of();
constructor(private db: AngularFireDatabase) {
// This subscription will never return anything
this.profileObservable.subscribe(x => console.log(x));
}
initialize(auth) {
this.profileRef = this.db.object(`${this.basePath}/${auth.uid}`);
const subscription = this.profileRef.subscribe(profile => {
if (!profile.$exists()) {
this.profileRef.update({
display_name: auth.displayName || auth.email,
created_at: new Date().toString(),
});
} else subscription.unsubscribe();
});
this.profileObservable = this.profileRef.map(profile => profile);
// This subscription will return the profile once it's retrieved (and any updates)
this.profileObservable.subscribe(profile => console.log(profile));
}
};
You must not change observable references once you constructed them. The way I found to properly decouple subscribers from the datasource is to use an intermediate Subject, which is both an observer and an observable.
Your code would look something like this:
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
...
export class UserProfileService {
...
public profileObservable = new Subject<UserProfile>();
constructor(private db: AngularFireDatabase) {
// This subscription now works
this.profileObservable.subscribe(x => console.log(x));
}
initialize(auth) {
const profileRef = this.db.object(`${this.basePath}/${auth.uid}`);
...
profileRef.subscribe(this.profileObservable);
}
};

Angular 2 passing data to another component

I am new to Angular 2 and am still figuring things out.
I have two components:
1) List Component
This lists all the products in a store and does other functions
#Component({
selector :'home-list',
providers :[CartService,CartStatus]
})
#View({
templateUrl :'/app/views/list-product.partial.html'
})
export class HomeList{
title: string;
products : ProductInterface[];
cart;
private isFetching: boolean = false;
constructor(
private _router : Router,
private _dataService: DataService,
private _cartService: CartService,
private _cartStatus: CartStatus
){}
ngOnInit(){
this.title = 'Featured Products';
this.getfeaturedproducts();
}
getfeaturedproducts(){
this._dataService.getFeatured().subscribe(
products => {
this.products = products;
this.isFetching = true;
}
)
}
gotoDetail(slug:string) {
console.log(slug);
this._router.navigate(['ProductsDetail', {productslug:slug}]);
return false;
}
getCart(){
this._cartService.getCartContent().subscribe(
res => {
this.cart = res;
console.log(res.result)
}
);
}
addtoCart(id:number){
this._cartService.addToCart(id).subscribe(
res => console.log(res)
this._cartStatus.updateCart(id);
//want to pass the data to CartStatus Component
)
}
}
2) CartUpdate Component which shows no of items in cart
#Component({
selector : 'cart-status'
})
#View({
template :'{{cart}}'
})
export class CartStatus{
cart;
updateCart(id:number){
this.cart = id;
}
}
The problem is that I have not been able to pass the id or any value to the CartStatus view. When I console.log the id on updateCart it shows accurate value but does not reflect on the view of the CartStatus.
Am I doing something wrong here??
From your code, what I can figure out is CartStatus is a component so,
providers :[CartService,CartStatus]
should be,
providers : [CartService]
directives : [cardStatus]
Now, check this official docs for communication between components,
https://angular.io/docs/ts/latest/cookbook/component-communication.html
I am also creating a large scale shopping cart using MEAN stack with Angular 2
As for me adding items to a cart is not so easy for me to implement. User has items(products, qty, total price), User has orderedItems(items, totalQty, totalPrice), User - logged in, all session cart items are cleared and added to user items and so on. I implemented my cart with MongoStore (session with expiration date) so that when you refresh the page, the items are still stored in the cart. MongoStore is a long subject and I can not help you here. There is a tutorial in Utube(angular 1 by MAX from Udemy and that's how I learned it). Honestly, I do not know how to store a "class object" in a LocalStorage. For learning purposes of how to add items to a cart is to use array.
Lets' create a Cart class:
export Class {
public product: Product;
public qty: number) {}
}
Create a service: cart.service.ts
import { Injectable } from '#angular/core';
import { Cart } from './cart';
import { Product } from './product';
#Injectable()
export class CartService {
private carts: Carts[];
getCart() {
return this.carts;
}
addToCart(product: Product) {
for(let i=0; i<this.carts.length; i++) {
if(this.carts[i].product.productId == product.productId) {
this.carts[i].quantity = this.carts[i].quantity + 1;
return;
}
let cart = new Cart(product, 1);
this.carts.push(cart;
}
}
getCartTotal(){
let total = 0;
for(let i=0; i<carts.length; i++) {
total = total + this.carts[i].quantity;
}
return total;
}
emptyCart() {
this.carts = [];
}
}
In your cart component:
export class CartStatus implements OnInit {
carts: Cart[];
constructor(private: _cartService: CartService) {
ngOnInit(){
this.carts = this._cartService.getCart(); //no subsribe() here since is not an http request.
}
Please don't forget to add CartService to to your boot file. I hope this helps. I can also help you with deleteItem, updateItem, and getTotalCost. Thanks and Happy coding..

Categories

Resources