How do you filter an Observable with form input? - javascript

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
});
}
}

Related

What makes the OrderBy custom pipe fail in in this Angular app?

I have been developing an e-commerce app with Angular 14 and Angular Material.
I am currently working on a form, among others, contains a elements populated with all the countries of the World that I get from restcountries.com.
In the countries.service.ts file I have:
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs';
import { HttpClient } from '#angular/common/http';
#Injectable({
providedIn: 'root'
})
export class CountriesService {
apiURL: string = 'https://restcountries.com/v3.1';
constructor (private http: HttpClient) { }
public getCountries(): Observable<any>{
return this.http.get<any>(`${this.apiURL}/all`);
}
}
I use the service in the form component:
import { CountriesService } from '../../services/countries.service';
export class FormComponent implements OnInit {
public countries!: any;
constructor (private countriesService: CountriesService) { }
ngOnInit(): void {
this.getCountries();
}
public getCountries() {
this.countriesService.getCountries().subscribe(response => {
this.countries = response;
});
}
}
<mat-form-field appearance="outline" floatLabel="always">
<mat-label>Country:</mat-label>
<mat-select formControlName="country" [(value)]="this.selectedCountry">
<mat-option value="">Select a country</mat-option>
<mat-option *ngFor="let country of countries | OrderBy: 'country.name.common'" [value]="country.cca2">
{{country.name.common}}
</mat-option>
</mat-select>
</mat-form-field>
The goal
I use this OrderBy custom pipe to order the countries by name (from A to Z), in the drop-down:
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({name: 'OrderBy'})
export class OrderByPipe implements PipeTransform {
transform(input: any, key: string) {
if (!input) return [];
return input.sort(function(itemA: any, itemB: any) {
if (itemA[key] > itemB[key]) {
return 1;
} else if (itemA[key] < itemB[key]) {
return -1;
} else {
return 0;
}
});
}
}
The problem
For a reason I have bean unable to understand, the ordering fails, the countries list stays unchanged.
EDIT
Alternatives like this also do not work:
public getCountries() {
this.countriesService.getCountries().subscribe(response => {
this.countries = response;
this.countries.sort(function(a: any, b: any) {
return a.name.common - b.name.common;
});
});
}
Questions
What am I doing wrong?
What is the easiest and most reliable way to achieve the desired result?
Your key in the pipe has the value 'country.name.common'
I doubt your country actually has a field 'country.name.common' but rather consists of multiple nested objects.
Thus itemA[key] returns undefined, same for itemB[key], so your ordering function returns 0 for every element. The list remains unchanged.

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
});
});

Unable to do searching using custom pipe created in angular 7

How can i search in angular 7 using pipe (like filter in angular 1) ? Below is the code which i tried.But that returns only if exact match is there. But i need results which contains that word.
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({
name: 'search',
pure:true
})
export class SearchPipe implements PipeTransform {
transform(data: any,searchTxt:string): any {
if(!data || !searchTxt)
{
return data;
}
return data.filter(function(x) {
return x.name ==searchTxt}) ;
}`
}
i tried below code also but doesn't work
return data.filter(x=>x.name.toString().toLowerCase().indexof(searchTxt.toLowerCase()) !== -1)
This throws error: x.name.indexof is not a function
How can i do contains search using javascript\angular ?
You should be using indexOf instead of === or indexof(which I think is a typo in your code).
Plus you should not be using a pipe to filter values. Here's why Angular doesn't recommend using pipes to filter or sort values.
Angular doesn't offer such pipes because they perform poorly and prevent aggressive minification. Both filter and orderBy require parameters that reference object properties. Read more about that here.
That being said, you can basically write the logic to filter data, right inside your Component:
Here, give this a try:
import { Component } from "#angular/core";
import { HttpClient } from "#angular/common/http";
#Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
users = [];
filteredUsers = [];
constructor(private http: HttpClient) {}
ngOnInit() {
this.http
.get("https://jsonplaceholder.typicode.com/users")
.subscribe((users: any[]) => {
this.users = users;
this.filteredUsers = [...this.users];
});
}
onSearchTextChange(searchString) {
this.filteredUsers = [
...this.users.filter(user => user.name.indexOf(searchString) > -1)
];
}
}
Here's a Working CodeSandbox Sample for your ref.

Casting firestore observables to custom objects

I'm new to angular and firestore and trying to figure out how to cast the data received from firebase directly to models. What is the best approach here?
Currently I get the data, but it looks like it's not casted into a Blimp object. When I try to call getImageUrl() on it in the view, I get the following error message.
ERROR TypeError: _v.context.$implicit.getImageUrl is not a function
So my question: What is the best and cleanest way to cast these observables to the correct local model? I was expecting the tags to cast it by default.
Current code
Custom model class
export class Blimp {
created_at: Date;
file_location: string;
id: string;
constructor() {
console.log('OBJ');
}
getImageUrl() {
return "https://*.com" + this.file_location;
}
}
Service class
import { Injectable } from '#angular/core';
import {Blimp} from '../models/blimp';
import { AngularFirestore } from '#angular/fire/firestore';
import {AngularFireStorage, AngularFireUploadTask} from '#angular/fire/storage';
import {Observable} from 'rxjs';
import {finalize} from 'rxjs/operators';
#Injectable({
providedIn: 'root'
})
export class BlimpService {
blimps: Observable<Blimp[]>;
constructor(private fireStore: AngularFirestore, private fireDisk: AngularFireStorage) { }
getBlimps() {
this.blimps = this.fireStore.collection<Blimp>('blimps').valueChanges();
return this.blimps;
}
}
Display component
import { Component, OnInit } from '#angular/core';
import {BlimpService} from '../../services/blimp.service';
import {Observable} from 'rxjs';
import {Blimp} from '../../models/blimp';
#Component({
selector: 'app-blimp-viewer',
templateUrl: './blimp-viewer.component.html',
styleUrls: ['./blimp-viewer.component.scss'],
})
export class BlimpViewerComponent implements OnInit {
blimps: Observable<Blimp[]>;
constructor(private blimpService: BlimpService) { }
ngOnInit() {
this.blimps = this.blimpService.getBlimps();
}
}
View
<ul>
<li *ngFor="let blimp of blimps | async">
{{ blimp | json}}
<img [src]="blimp.getImageUrl()" />
</li>
</ul>
Update #1
Changed the code to
I now have changed your example to: getBlimps() {
this.blimps = this.fireStore.collection<Blimp>('blimps')
.valueChanges()
pipe(map(b => {
let blimp = new Blimp();
blimp.created_at = b.created_at;
blimp.file_location = b.file_location;
blimp.id = b.id;
return blimp;
}));
return this.blimps;
}
This still complains in the view about the getImageUrl() not being found on the object.
# Solution
Looks like I forget a . (dot) in the last code
This code works:
this.blimps = this.fireStore.collection<Blimp>('blimps')
.valueChanges()
.pipe(map(collection => {
return collection.map(b => {
let blimp = new Blimp();
blimp.created_at = b.created_at;
blimp.file_location = b.file_location;
blimp.id = b.id;
return blimp;
});
}));
return this.blimps;
Concept :
You don't cast an observable to an object model. An observable is a stream which has a lifecycle.
An observable emits value to its subscribers, you need to subscribe to your observable to be notified when it emits value. You also need to close the subscription or the subscription will last until your observable complete causing memory leaks.
I can see you're using | asyncin your html template, it's a subscription handled by angular that auto-unsubscribe when needed.
Get data :
You need to map the data you received to a Blimp object, you can use map operator.
blimps$: Observable<Blimp[]>; // naming convention, suffix your observable with $
blimps$ = this.fireStore.collection<Blimp>('blimps')
.valueChanges()
.pipe(map(collection => {
return collection.map(b => {
let blimp = new Blimp();
blimp.created_at = b.created_at;
blimp.file_location = b.file_location;
blimp.id = b.id;
console.log(blimp);
console.log(b);
return blimp;
});
}));
return this.blimps;
As we changed blimps to blimps$, change your html template :
*ngFor="let blimp of blimps$ | async"
EDIT :
You can use your class constructor to initialize your object :
export class Blimp {
created_at?: Date;
file_location?: string;
id?: string;
constructor(blimp: Blimp = {}) {
this.created_at = blimp.created_at;
this.file_location = blimp.file_location;
this.id = blimp.id;
}
getImageUrl() {
return `https://*.com${this.file_location}`; // use string interpolation here
}
blimps$ = this.fireStore.collection<Blimp>('blimps')
.valueChanges()
.pipe(map(collection => {
return collection.map(b => new Blimp(b));
}));

Data not passing through with route in Angular 6

I have a application with a table of cars:
This is my code:
Carcomponent.html
<tbody>
<tr *ngFor="let car of allCars; index as carId" \>
<td [routerLink]="['/cars', carId]">{{car.carId}}</td>
<td>{{car.brand}}</td>
<td>{{car.model}}</td>
<td>{{car.color}}</td>
<td>{{car.topSpeed }}</td>
</tr>
</tbody>
I have register the route like this:
{ path: 'cars/:carId', component: CardetailsComponent }
And this is my CarDetails.ts file:
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import { CarVM } from '../viewmodels/car-vm';
import { CarService } from '../services/car.service';
#Component({
selector: 'app-cardetails',
templateUrl: './cardetails.component.html',
styleUrls: ['./cardetails.component.css']
})
export class CardetailsComponent implements OnInit {
car: any;
carList: any;
constructor(private route: ActivatedRoute, private carservice: CarService) { }
ngOnInit() {
this.route.paramMap.subscribe(params => {
this.car = params.get('carId');
});
}
getCarList() {
this.carList = new CarVM();
this.carservice.getCarById(this.carList.carId).subscribe((res: any) => {
this.carList = res.data;
console.log(this.carList)
})
}
}
And on my Cardetails.html I want to show the selected car like this:
<h2>Car Details</h2>
<div *ngIf="car">
<h3>{{ car.brand }}</h3>
<h4>{{ car.model }}</h4>
<p>{{ car.color }}</p>
</div>
The routing is working fine and fetching the cars is working. Now I want to select one car and see the brand, model, color on the next page. I use a viewmodel for this:
export class CarVM {
CarId: number;
Brand: string;
Model: string;
Color: string;
TopSpeed: number;
}
How can I see the selected car on the next page?
I have followed this tutorial:
https://angular.io/start/routing
Ok, you seem to be bit confused. In cardetails component you want to process carId from route parameters and use it to get car details. You can either get them from server, or have the service return already loaded details of all cars.
Let's say we are trying to make it happen getting the first way, it might look like this:
import { map, switchMap } from 'rxjs/operators';
ngOnInit() {
this.getCar();
}
private getCar(): void {
this.route.paramMap.pipe(
map(params => params.get('carId')),
switchMap(carId => {
return this.carservice.getCarById(carId);
})
).subscribe(
res => {
this.car = res;
console.log('#My car:', this.car);
}
);
}
First, you'll get the carId from route.paramMap, map it using rxjs map, then use switchMap to call you carservice.getCarById(carId) and have it return Observable to which you can subscribe. This should do the trick. Don't forget to properly map it/create CarVM object from it.
The problem is, you don't have CarVM object properly on CardetailsComponent. You are only getting carId into CarVM here: this.car = CarVM[+params.get('carId')];
First you need to create CarVM properly with your class variables. And the you can call your index.
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import { CarVM } from '../viewmodels/car-vm';
#Component({
selector: 'app-cardetails',
templateUrl: './cardetails.component.html',
styleUrls: ['./cardetails.component.css']
})
export class CardetailsComponent implements OnInit {
car: any;
carList: any;
constructor(private route: ActivatedRoute) { }
ngOnInit() {
this.route.paramMap.subscribe(params => {
this.car = params.get('carId');
});
}
getCarList(){
this.carList = new CarVM();
//call your service here to fill your carList variable and once you get car list, you will be able to access variable using with your index (this.car).
}
}

Categories

Resources