How to join multiple documents in a Firestore? - javascript

I have a Firestore DB with the following structure:
users
[uid]
name: 'User one'
artists
[uid]
style: 'Pop teste'
user_uid: [uid]
in my service I have
constructor(private afu: AngularFireAuth, private afs: AngularFirestore, private storage: AngularFireStorage) {
this.usersCollection = afs.collection<User>('users');
this.users = this.usersCollection.valueChanges();
}
getUsers() {
return this.users = this.usersCollection.snapshotChanges()
.pipe(map(changes => {
return changes.map(action => {
const data = action.payload.doc.data() as User;
return data
});
}));
}
How can join between users and artists ?

Using combineLatest is a great way. Since the user_uid doesn't exist on the user, I added the idField to the user as user_uid. View the code first then read below for an explanation.
import { Component, OnInit } from '#angular/core';
import { AngularFirestore } from '#angular/fire/firestore';
import { Observable, combineLatest } from 'rxjs';
interface User {
name: string;
user_uid: string;
}
interface Artist {
style: string;
user_uid: string;
}
interface Joined {
user_uid: string;
name: string;
style: string;
}
#Component({
selector: 'test',
templateUrl: './test.component.html',
styleUrls: ['./test.component.scss']
})
export class TestComponent implements OnInit {
users$: Observable<User[]>;
artists$: Observable<Artist[]>;
joined$: Observable<Joined[]>;
constructor(private afs: AngularFirestore){}
ngOnInit(){
this.users$ = this.afs.collection<User>('users').valueChanges({idField: 'user_uid'});
this.artists$ = this.afs.collection<Artist>('artists').valueChanges();
this.joined$ = combineLatest(this.users$, this.artists$, (users, artists) => {
const joinedAsMap: Map<string, Joined> = new Map(artists.map(oneArtist => [oneArtist.user_uid, { ...{name: null} , ...oneArtist}]));
users.forEach(one => joinedAsMap.set(one.user_uid , {...{name: one.name}, ...joinedAsMap.get(one.user_uid) } ));
const joined: Joined[] = Array.from(joinedAsMap.values());
return joined;
});
}
}
Make a joined interface
Get both observables
use combine latest
Build a map with uid as key and and artist as value. Set the name to null just so the types will work. Use the spread operator to merge some objects.
Loop through user and add in the user info to the value of each key
Build joined array from values of map
return the value
You can do this different ways but using es6 maps is a nice way to simplify some things. Also, didn't get a chance to test with a real database so you might need to verify. Also, this is all within the component for demonstration. You could do this in the service for sure.

Related

Why is my Angular Service not assigning a filtered result to my property?

I setup a service called 'bankService' which is being used by my 'user' component. The 'user' component is receiving the data from the service correctly but I am unable to assign a filtered result to my 'currentAccount' property. I am filtering by 'id' from the list of 'accounts' that is being returned from my service. Any help with an explanation would be appreciated!
Model
export interface Account {
id: number;
accountHolder: string;
checking: number;
savings: number;
}
Service
import { Injectable } from '#angular/core';
import { HttpClient, HttpHeaders } from '#angular/common/http';
import { elementAt, Observable } from 'rxjs';
import { Account } from '../models/Account';
import { Transaction } from '../models/Transaction';
#Injectable({
providedIn: 'root',
})
export class BankServiceService {
private apiUrl = 'http://localhost:5000/accounts';
constructor(private http: HttpClient) {}
getAccounts(): Observable<Account[]> {
return this.http.get<Account[]>(this.apiUrl);
}
}
Component
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import { Account } from 'src/app/models/Account';
import { BankServiceService } from 'src/app/services/bank-service.service';
#Component({
selector: 'app-user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.css'],
})
export class UserComponent implements OnInit {
currentAccount: Account[] = [];
accountId: number;
accountHolder: string;
checkingAmount: number;
savingsAmount: number;
constructor(
private route: ActivatedRoute,
private bankService: BankServiceService
) {}
ngOnInit(): void {
// gets the parameter for the url (the param is the account id)
this.accountId = this.route.snapshot.params['id'];
console.log('id: ', this.accountId);
// pulls in all accounts
this.bankService
.getAccounts()
.subscribe(
(accounts) =>
(this.currentAccount = accounts.filter(
(account) => account.id === this.accountId
))
);
console.log('current account: ', this.currentAccount);
}
}
If I'm not mistaking, your issue is this one:
the account id received from the backend is a number
the account id pulled from the url is a string
In the filter function you are using strict equality, and that's why no account id passes the condition in the filter callback
You can switch from strict equality to loose equality (==) or do something like this for the filter callback:
(account) => account.id.toString() === this.accountId

convert returned Observables to custom class array in angular

Hello folks I will keep my question very simpler by showing code
I am using Json placeholder site for the fake rest Api
I have a user class Object
I want to convert returned Observable to the
custom class object array.
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Observable } from 'rxjs';
import { Users } from './users.model';
#Injectable({
providedIn: 'root'
})
export class UsersService {
private url = "https://jsonplaceholder.typicode.com";
constructor(private http:HttpClient) {
console.log(this.getUsers());
}
getUsers():Observable<Users[]>{
return this.http.get<Users[]>(`${this.url}/posts`);
}
}
The above is my service
export class Users {
email: string;
id: number;
name: string;
phone: string;
username: string;
}
above is my class I haven't included all properties
In my typescript file I have code like.
constructor(private _usersService:UsersService) {
}
ngOnInit(): void {
this._usersService.getUsers().subscribe(data=>this.users=data);
console.log(this.users);
}
Now the things I want is
how to convert returned observable in my custom class object?
I don't have all the fields so how is it possible to map only those fields which I want?
Hope my question is clear..!!
so this answer takes advantage of map() which is imported from rxjs.
before subscribing we are going to pipe a map() function into the observable stream and then map() each element from that array into a new object that fits our User interface
then we subscribe and the data we get then will be an array that fits our User interface
ngOnInit(): void {
this._usersService.getUsers()
.pipe(map(data => {
return data.map(item => {
const user: User = {
name: item.name,
email: item.email,
}
return user
})
}))
.subscribe(data=>this.users=data);
console.log(this.users);
}
You can do like below, in the User class have a constructor and return User while mapping
import { Component, VERSION, OnInit } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { map } from 'rxjs/operators';
export class User {
email: string;
id: number;
name: string;
phone: string;
username: string;
constructor( user: User ) {
Object.assign( this, user );
}
}
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit {
name = 'Angular ' + VERSION.major;
constructor(private http: HttpClient){}
ngOnInit() {
this.http.get<User[]>("https://jsonplaceholder.typicode.com/users")
.pipe(
map( data => {
return data.map( ( user ) => {
return new User( {
email: user['email'],
id: user['id'],
name: user['name'],
phone: user['phone'],
username: user['username'],
} );
} );
} ),
)
.subscribe( (users : User[]) => console.log(users) );
}
}
Working stackblitz

Format httpclient response for *ngFor?

Hi I was wondering if anyone could help me solve a small problem.
I am received data from my rest api which is returned as an array with objects inside.
Once I get it to my service I try to transform the data and push it to a subject so that it can inform my component that the data is here or updated.
When i console.log the data I get
0:{code: "AUH", name: "Abu Dhabi"}
1:{code: "ALY", name: "Alexandria"}
2:{code: "LTS", name: "Altus"}
3:{code: "ANK", name: "Ankara"}
4:{code: "AIY", name: "Atlantic City"}
5:{code: "BAK", name: "Baku"}
6:{code: "BKK", name: "Bangkok"}
7:{code: "EAP", name: "Basel"}
8:{code: "BJS", name: "Beijing"}
So when I try and use my *ngFor I get [object]p[Object]
How can I format this to work with *ngFor?
city-list.component.html
import { CityService } from "./services/city-list.service";
import { Component, OnInit, OnDestroy } from "#angular/core";
import { City } from "../cities/models/city";
import { Subscription } from "rxjs";
#Component({
selector: "<app-cities></app-cities>",
templateUrl: "./city-list.component.html"
})
export class CityListComponent implements OnInit, OnDestroy {
cities: City[];
private citiesSub: Subscription; // so as to unsubscribe if page changes/ memory leak
constructor(public cityService: CityService) {}
ngOnInit() {
this.cityService.getCities();
this.citiesSub = this.cityService
.getCityUpdateListener()
.subscribe((cities) => {
this.cities = cities;
});
// 1st value: when data emit 2nd value: error emit, 3rd value function for when no more data is available
}
ngOnDestroy() {
this.citiesSub.unsubscribe();
}
}
// subject is an observable but you can call next on them to emit a change when you want
"service"
import { Subject } from 'rxjs';
import {Injectable} from '#angular/core';
import {HttpClient} from '#angular/common/http';
import { map } from "rxjs/operators";
import {City} from '../models/city';
#Injectable()
export class CityService {
cities: City[] = [];
private updatedCities = new Subject<City[]>();
constructor(private http: HttpClient) {}
getCities() {
this.http.get<{message: string; cities: City[]}>('http://localhost:3000/cities')
.pipe(
map((cityData)=>{
return cityData.cities.map(city=>{
return{
code: city.code,
name: city.name
};
});
})
)
.subscribe((transCity) => {
this.cities = transCity;
console.log(this.cities);
this.updatedCities.next([...this.cities]);
});
}
getCityUpdateListener() {
return this.updatedCities.asObservable();
}
}
You can just use the json pipe:
<div *ngFor="let item of response">{{ item | json }}</div>
If you want to display it in "pretty" instead of as json, you need to access the individual fields of the item and format it in the desired way.
try as below , first get keys form reponse object you are receiving from http call and then go through each key in html , might resole your issue
in ts file
//response is data you received after making http call, list of cities in your case
keys = Object.keys(response);
in html file
<div *ngFor="let key of keys">
{{response[key].code }} {{response[key].name }}
</div>
this should work based on response you are getting from server
It looks like the issue here is that you're not actually returning an array of City, instead you're returning a dictionary or Map<City>. You'll probably want to iterate over your response and map it to the correct type.
this.citiesSub = this.cityService
.getCityUpdateListener()
.subscribe((cityMap) => {
this.cities = [ ...cityMap.values() ]
});
Asuming you are using httpClient(new released in angular5) then there is no need of the map() and pipe() functions, results are mapped to json by default you just have to subscribe to the service
this is how it would look your new service class
import { Subject } from 'rxjs';
import {Injectable} from '#angular/core';
import {HttpClient} from '#angular/common/http';
import { map } from "rxjs/operators";
import {City} from '../models/city';
#Injectable()
export class CityService {
cities: City[] = [];
private updatedCities = new Subject<City[]>();
constructor(private http: HttpClient) {}
getCities() {
return this.http.get<City[]>('http://localhost:3000/cities')//http.get<any> also work but for type safety i am asuming City[] array have the same structure.
}
getCityUpdateListener() {
return this.updatedCities.asObservable();
}
}
Then in your component you would have to subscrive to that service and use it
constructor(public cityService: CityService) {
this.cityService.getCities().subscribe(cities => {
this.cities = cities;
console.log(cities);
}, error=> {console.log(error)});//handling errors
}
ngOnInit() { } // just moved the service call to the constructor of the component
I hope this solve your problem,
Thanks

Cannot find namespace error for model in Angular2/TypeScript

The FeaturedCategories model
export class FeaturedCategories {
categories: Array<{ id: number, title: string, graphic: string, categorycards: Array<{}> }>;
}
Also tried this:
export class FeaturedCategories {
id: number;
title: string;
graphic: string;
categorycards: Object[];
}
The Component
import { Component, ChangeDetectionStrategy, ViewEncapsulation } from '#angular/core';
import { ApiService } from '../shared/services/api.service';
import { FeaturedCategories } from '../shared/models/home/featuredcategories';
#Component({
changeDetection: ChangeDetectionStrategy.Default,
encapsulation: ViewEncapsulation.Emulated,
selector: 'home',
styleUrls: [ './home.component.css' ],
templateUrl: './home.component.html'
})
export class HomeComponent {
testFeaturedCategories: Array<FeaturedCategories>;
constructor(private api: ApiService) {
// we need the data synchronously for the client to set the server response
// we create another method so we have more control for testing
this.universalInit();
}
universalInit() {
console.log('universalInit...')
this.api.getFeatured()
.subscribe(categories => {
console.log('categories', categories);
this.testFeaturedCategories = categories
});
}
}
This will work: testFeaturedCategories: Array<{}>;
However I'm trying to use TypeScript to let my App know what type of model to expect.
This causes the error above:
testFeaturedCategories: FeaturedCategories.categories;
And if I just try this: testFeaturedCategories: FeaturedCategories;
I get a type [{}] is not assignable error.
UPDATE
So I noticed that when I commented out all the keys in my FeaturedCategories model finally the error goes away and
featuredCategories: FeaturedCategories[]; will work.
However now this is just an empty object without keys to expect :(
export class FeaturedCategories {
// id: number;
// title: string;
// graphic: string;
// categorycards: Object[];
}
this is working fine for me.
export class MyComponent {
categories: FeaturedCategories[] = [{
id: 1,
title: "",
graphic: "",
categorycards: [{}]
}];
}
export class FeaturedCategories{
id: number;
title: string;
graphic: string;
categorycards: Object[];
}
My problem was trying to type my Array, instead of just using the Typed objects that exist in the larger Array.
Also had a problem in my service, originally I had this:
/**
* Get featured categories data for homepage
* /wiki
*/
getFeatured(): Observable<[{}]> {
return this.http.get(`${this.getFeaturedUrl}/home`)
// .do(res => console.log('getFeatured res', res.json()))
.map(res => res.json())
.catch(this.handleError);
}
I did not need or could even use a type for my larger Categories array, what I needed was a smaller type for the exact Objects that exist in that larger Array:
export class FeaturedCategory {
id?: number;
type: string;
title: string;
graphic?: string;
video?: string;
categorycards: Array<{}>;
}
So now with the correct Type of Objects inside my Array I added it to the service:
getFeatured(): Observable<[FeaturedCategory]> {
return this.http.get(`${this.getFeaturedUrl}/home`)
.map(res => res.json())
.catch(this.handleError);
}
Now back in my Component I imported the single Typed Object
import { FeaturedCategory } from '../shared/models/home/featuredcategory';
Then typed the variable:
featuredCategories: Array<FeaturedCategory>;
And finally in ngOnInit
ngOnInit() {
this.api.getFeatured()
.subscribe(categories => {
console.log('categories', categories);
this.featuredCategories = categories;
});
}
No more errors :)

Joining Flattened Data

i'd like to join the data on init from my customers table into the projects list.
Model is like this:
projects
key
name: string
customer : customerKey
customers
key
name: string
Do you have an example, how i do this from angular2 component using angularfire2?
my controller looks like this:
import { Component, OnInit } from '#angular/core';
import { Project } from '../project';
import { Router } from '#angular/router';
import { FirebaseAuth } from 'angularfire2';
import { AngularFire, FirebaseListObservable, FirebaseObjectObservable } from 'angularfire2';
import { Observable } from 'rxjs';
#Component({
moduleId: module.id,
selector: 'app-projects',
templateUrl: 'projects.component.html',
styleUrls: ['projects.component.css']
})
export class ProjectsComponent implements OnInit {
projects: FirebaseListObservable<any[]>;
customers: FirebaseListObservable<any[]>;
projectName: string;
constructor(
private router: Router,
private af: AngularFire
) { };
ngOnInit() {
this.projects = this.af.database.list('projects');
}
add(projectName: string) {
this.af.database.list('projects')
.push({ name: projectName, id: '123' });
this.projectName = null;
}
}
Update
i've changed the type of this.projects to Observable from FirebaseListObservable
my on ngOnInit() method looks now like this:
ngOnInit() {
this.projects = this.af.database.list(`projects`)
.map(projects => {
projects.map(project => {
this.af.database.object('customer/' + project.customer + '/name')
.subscribe(customer => {
project.customer = customer;
})
return project;
})
return projects;
});
}
i can now access not the name property of customer from the template inside of
<li *ngFor="let project of projects | async">
project.customer.$value
Not exactly sure how your dataset looks like, so I'm just going to write a basic example. Assuming a structure something like this:
- projects
- key
- name: string
- customers
- customerKey: boolean
- customers
- key
- name: string
Example data
- projects
- projectId1
- name: "Cool project!",
- customers
- customerId1: true,
- customerId2: true
- projectId2
- name: "Another cool project!",
- customers
- customerId2: true,
- customerId3: true
- customers
- customerId1
- name: "John Smith"
- customerId2
- name: "John Doe"
- customerId3
- name: "John John"
So we're storing the customers' key in every projects' customers property.
Let's say we want to list every projects, but we also want to get the customers' real name as well, not just their id. Since firebase doesn't have joins we'll have to do this manually. Here's one way to do it:
this.projects = this.af.database.list(`projects`)
.map(projects => {
return projects.map(project => {
project.customers.map(customer => {
this.af.database.list(`customers`)
.subscribe(c => {
customer = c;
});
});
return project;
});
});
The inner .subscribe could be changed to a simple .map if you want to get the data asynchronously (in this case use the async pipe in the template`).

Categories

Resources