Joining Flattened Data - javascript

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`).

Related

How do you filter an Observable with form input?

I have a component with this "countries$" variable:
countries$!: Observable<Country[]>;
that I'm populating with this data in an "ngOnInit" like this:
ngOnInit(){
this.countries$ = this.apiService.getAllCountries();
}
and I'm accessing this variable/Observable in the html template like this:
<div>
<app-country-card *ngFor="let country of countries$ | async" [country]="country"></app-country-card>
</div>
I want to include a search bar that filters the countries down to whatever is typed in.
I thought I could use the filter function inside a pipe like this:
searchFilterCountries(searchTerm: string){
this.countries$.pipe(filter((country: any) => country.name.common.toLowerCase().includes(searchTerm.toLowerCase())))
}
and put the input in the html template like this:
<input type="text" class="form-control" (input)="searchFilterCountries($event.target.value)"/>
so that the filter function would fire every time theres an input, narrowing down the list of countries on display.
This doesn't work however. I'm getting the typescript error:
Object is possibly 'null'.ngtsc(2531)
Property 'value' does not exist on type 'EventTarget'.ngtsc(2339)
Then I found a "sample" of a working filtered list here on Material UI
https://material.angular.io/components/autocomplete/examples (The FILTER one)
I attempted to implement this and came up with this code:
export class HomeComponent {
countries$!: Observable<Country[]>;
myControl = new FormControl('');
constructor(private apiService: ApiService) { }
ngOnInit(){
this.countries$ = this.apiService.getAllCountries();
}
private _filter(value: string): Observable<Country[]> {
const filterValue = value.toLowerCase();
return this.countries$.pipe(filter(option => option.name.common.toLowerCase().includes(filterValue))) <----ERROR #2
}
}
It doesn't work however. I think because the values are observables, not the data inside the observable.
I have squiggly lines showing a TS error under the under the "name" property in "option.name.common" saying:
option.name.common TS error
Property 'name' does not exist on type 'Country[]'
If I do this instead though:
option => option[0].name.common.toLowerCase().includes(filterValue)))
the error goes away, but I wouldn't be able to search all the values if I did that.
Am I on the right track here? Am I using the right operators? How do I fix the TS errors? I'm new to angular and don't know all the operators available. If I use mergeMap/switchMap will that solve my problem? If I do fix the typescript errors would it even work? Or is my approach wrong?
Can somebody help me get this working?
I would like to expand on your current code and suggest some changes like this:
export class HomeComponent {
allCountries: Country[] = [];
countries$!: Observable<Country[]>;
myControl = new FormControl('');
constructor(private apiService: ApiService) {}
ngOnInit() {
this.apiService
.getAllCountries()
.subscribe((countries) => (this.allCountries = countries));
this.countries$ = combineLatest({
searchTerm: this.myControl.valueChanges.pipe(startWith('')),
countries: this.apiService
.getAllCountries()
.pipe(tap((countries) => (this.allCountries = countries))),
}).pipe(map(({ searchTerm }) => this._filter(searchTerm)));
}
private _filter(value: string | null): Country[] {
if (value === null) {
return this.allCountries;
}
const filterValue = value?.toLowerCase();
return this.allCountries.filter((country) =>
country.name.common.toLowerCase().includes(filterValue)
);
}
}
So we're keeping the original country list in a separate variable, and we are using the form control's valueChange event to filter the countries that we need to display.
The template should look like this:
<input type="text" [formControl]="myControl" />
<div *ngFor="let country of countries$ | async">
<div>Name: {{ country.name.common }}</div>>
</div>
Example pipe
import { Pipe, PipeTransform } from '#angular/core';
import { Country } from './country';
#Pipe({
name: 'filterList',
})
export class FilterListPipe implements PipeTransform {
transform(countries: Country[]|null, searchText: string): Country[] {
if(!countries) return []
return countries.filter(country=>country.name.indexOf(searchText) != -1);
}
}
app.component.html
<form [formGroup]="controlsGroup">
<input type="text" formControlName="searchInput"/>
<div *ngFor="let country of countries | async | filterList:searchText">
<div>Name: {{country.name}}</div>
<div>Ranking: {{country.ranking}}</div>
<div>Metric: {{country.metric}}</div>
</div>
</form>
app.component.ts
import { Component } from '#angular/core';
import { FormBuilder, FormControl, FormGroup } from '#angular/forms';
import { Observable, of } from 'rxjs';
import { Country } from './country';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'piper-example-app';
searchText = ''
controlsGroup: FormGroup
constructor(public fb:FormBuilder){
this.controlsGroup = fb.group({
searchInput: new FormControl('')
})
this.controlsGroup.get('searchInput')?.valueChanges.subscribe(value => this.searchText=value)
}
countries: Observable<Country[]> = of([{
name: 'United States of America',
ranking: 1,
metric: 'burgers per capita'
},
{
name: 'China',
ranking: 9000,
metric: 'power level lower bound'
}])
}
Admittedly I'm doing a few things that are "dirty" here where filtering the incoming observable stream of arrays of countries might be a bit more efficient. Also note you'd need to still expand the filter function to check all the properties (can use for(prop in obj) type loop to iterate over all properties to see if any of them matches the searchText or adjust the criteria as see fit.
Bit more of a complete example showing the filter part with different types of properties being filtered slightly differently:
filter-list.pipe.ts (alternative)
import { Pipe, PipeTransform } from '#angular/core';
import { Country } from './country';
#Pipe({
name: 'filterList',
})
export class FilterListPipe implements PipeTransform {
transform(countries: Country[]|null, searchText: string): Country[] {
if(!countries) return []
return countries.filter(country => {
let foundMatch = false;
let property: keyof typeof country
for(property in country) {
if(typeof country[property] === 'string') {
if((country[property] as string).indexOf(searchText) != -1)
foundMatch = true
}else {
if((country[property] as number) == parseInt(searchText))
foundMatch = true
}
}
return foundMatch
});
}
}

How to join multiple documents in a Firestore?

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.

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

How to display only search result from firebase in angular?

I want to display only search result from firebase using angular version 8. I have data of customers stored in firebase and I want to search specific result by name.
import { Component, OnInit } from '#angular/core';
import { CustomerService } from '../shared/customer.service';
import { AngularFireDatabase } from 'angularfire2/database';
#Component({
selector: 'app-customer-list',
templateUrl: './customer-list.component.html',
styleUrls: ['./customer-list.component.css']
})
export class CustomerListComponent implements OnInit {
customerArray = [];
searchText: string = "";
findName: string;
constructor(private customerService: CustomerService) { }
ngOnInit() {
this.customerService.getCustomers().subscribe(
list => {
this.customerArray = list.map(item => {
return {
$key: item.key,
...item.payload.val()
};
});
});
}
filterCondition(customer) {
return
customer.fullName.toLowerCase().indexOf(this.searchText.toLowerCase()) != -1;
}
find(findName){
// query to check the enter name exist is firebase and display it
}
}
I expect only search data to be display but complete list of customers is displaying
You aren't ever actually using the filterCondition you have written, so of course all the customers are going to be displayed.
Its hard to tell if there aren't other problems, as you haven't specified an expected output or sample data in your question, but you at least need to change the callback you use when you subscribe to something more like this:
this.customerService.getCustomers().subscribe(
list => {
this.customerArray = list.filter(this.filterCondition).map(item => {
// contents omitted for berevity
});
});

Delete function error Angular 6 return undefined

I am trying to write a delete function to delete a movie from my object.
This is my code but when I click on a delete button I get DELETE: Error.
What do you think is the error in my code?
You can check out my code here ...
movie-model.ts
export class Movie {
Id: number;
Title: string;
Year: number;
Runtime: string;
Genre: string;
Director: string;
}
data.service.ts
import { Movie } from './model/movie.model';
import { HttpClient } from '#angular/common/http';
import { Injectable } from '#angular/core';
#Injectable({
providedIn: 'root'
})
export class DataService {
constructor(private http: HttpClient) { }
baseUrl: string = 'http://localhost:4200/';
getMovies() {
return fetch('https://www.omdbapi.com/?i=tt3896198&apikey=9fa6058b').then(function (resp) {
return resp.json()
});
}
createMovie(movie:Movie) {
return this.http.post(this.baseUrl, movie);
}
deleteMovie(movie:Movie){
return this.http.delete(this.baseUrl + movie.Id);
}
}
movie-list.component.ts
import { DataService } from './../data.service';
import { Movie } from './../model/movie.model';
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-movie-list',
templateUrl: './movie-list.component.html',
styleUrls: ['./movie-list.component.css']
})
export class MovieListComponent implements OnInit {
movies = [
{Id:1, Title: 'Avatar', Year: '2009'},
{Id:2, Title: 'Harry Potter', Year: '2001'},
{Id:3, Title: 'Spiderman 3', Year: '2007'}
];
constructor(private dataService:DataService){}
ngOnInit() {
this.getMovie().then(dt => {
this.movies.push(dt);
})
}
getMovie() {
return fetch('https://www.omdbapi.com/?i=tt3896198&apikey=9fa6058b').then(function (resp) {
return resp.json()
});
}
deleteMovie(movie: Movie): void {
this.dataService.deleteMovie(movie.Id)
.subscribe( data => {
this.movies = this.movies.filter(u => u !== movie);
})
};
}
This is the error I get ...
What can I do for the delete button to work and give me an alert and then delete itself from the object?
You try to acces an endpoint where actually your angular app is running: baseUrl: string = 'http://localhost:4200/'; This is the default port of your angular application on your local computer, and you try to call an delete endpoint of an external rest api I guess.
But the rest service does not run on your localhost on port 4200, thats why you get a 404 not found. I think you have to call delete on this endpoint https://www.omdbapi.com.
EDIT:
If you want delete a movie from your list, you have to delete the entry in your array. The easiest way would be if you change the id attribute to imdbID because the response type from omdbapi doesn't have an id attribute which means your id will always be undefined. Then when you want to delete an entry you could do it like this:
deleteMovie(imdbID: string): void {
this.movies = this.movies.filter(m => m.imdbID !== imdbID)
};
It's almost the same code that you have but without the delete call on the rest api. Because you don't want to delete the entry from the database but just on your angular app.
In the service file you have created method deleteMovie which accept a Movieobject
deleteMovie(movie:Movie){
return this.http.delete(this.baseUrl + movie.Id);
}
But in your Component movie-list.component.ts you are passing id in the delete method
import { DataService } from './../data.service';
import { Movie } from './../model/movie.model';
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-movie-list',
templateUrl: './movie-list.component.html',
styleUrls: ['./movie-list.component.css']
})
export class MovieListComponent implements OnInit {
movies = [
{Id:1, Title: 'Avatar', Year: '2009'},
{Id:2, Title: 'Harry Potter', Year: '2001'},
{Id:3, Title: 'Spiderman 3', Year: '2007'}
];
constructor(private dataService:DataService){}
ngOnInit() {
this.getMovie().then(dt => {
this.movies.push(dt);
})
}
getMovie() {
return fetch('https://www.omdbapi.com/?i=tt3896198&apikey=9fa6058b').then(function (resp) {
return resp.json()
});
}
deleteMovie(movie: Movie): void {
// this.dataService.deleteMovie(movie.Id) <- Your code error
// Pass movie object
this.dataService.deleteMovie(movie)
.subscribe( data => {
this.movies = this.movies.filter(u => u !== movie);
})
};
}

Categories

Resources