Angular creating filter with pipe and map - javascript

I am fairly new to angular and I am trying to create a filter for a value.
In my component - I have => myData$: Observable<MyInterface>
and the interface is as follows
export class FoundValues {
customerName: string;
startDate: string;
endDate: string;
includes(values: string) : boolean {
value = value.toLowerCase();
return this.valueIncludes(this.customerName, value);
}
private valueIncludes(includedValue, value){
if (value) {
const value = value.toLowerCase().includes(includedValue);
return result;
} else {
return false;
}
}
}
export interface MyInterface {
found_values : Array<FoundValues>;
}
In my component ngOnInit(), I am trying to create a logic filter but not getting it as it return a type FoundValues[] and it's complaining that it's not the expected Observable return type.
export class MyComponent implements OnInit{
myData$ = Observable<MyInterface>;
myControl = new FormControl();
ngOnInit(): void{
this.filterData =
this.myControl.valueChanges.pipe(map(value=>this._filter(value)));
}
private _filter(value:string): .....{
--- need logic here ----
}
}
How can I create the filter so that if I type a customer name in my form it shows only the matching customer name?

You can use the combineLatest RxJS operator for filtering through as shown in the following code snippet,
export class MyComponent implements OnInit {
myData$ = Observable < MyInterface > ;
mySearchQuery$: Observable < any > ;
searchString = new FormControl('');
filterData: Observable < any >
constructor() {
mySearchQuery$ = this.searchString.valueChanges.startsWith('');
}
ngOnInit(): void {
this.filterData = this.searchQuery$.combineLatest(this.myData$).map(([queryString, listOfCustomers]) => {
return listOfCustomers.filter(customer => customer.name.toLowerCase().indexOf(queryString.toLowerCase()) !== -1)
})
}
}
The combineLatest RxJS operator takes in the observables myData$ and mySearchQuery and returns the observable filterData containing the emitted values that match the searchString.

usual design in angular would be different
https://stackblitz.com/edit/angular7-rxjs-pgvqo5?file=src/app/app.component.ts
interface Entity {
name: string;
//...other properties
}
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
name = new FormControl('');
data$: Observable<Array<Entity>> = of([
{ name: 'Jhon' },
{ name: 'Jane' },
{ name: 'Apple' },
{ name: 'Cherry' },
]);
filtered$: Observable<Array<Entity>>;
ngOnInit() {
// this can be moved to a util lib/file
const startWithPipe = pipe(
map(
([query, data]: [query: string, data: Array<Entity>]): Array<Entity> =>
data.filter((entity) =>
query ? entity.name.toLowerCase().startsWith(query) : true
)
)
);
this.filtered$ = this.name.valueChanges.pipe(
startWith(''),
debounceTime<string>(300),
withLatestFrom(this.data$),
startWithPipe
);
}

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 make an injection optional in an angular component

I have a angular component which uses a form group and can be used to show a readonly view based on an input #Input() isEditModeActive: boolean;. The component works fine when the parent component has form .
#Component({
selector: 'bifrost-organization-financial',
templateUrl: './organization-financial.component.html',
})
export class OrganizationFinancialComponent implements OnInit {
readonly MAX_NAME_LENGTH: number = 50;
readonly MAX_IBAN_LENGTH: number = 255;
readonly MAX_OTHER_NUMBER_LENGTH: number = 50;
readonly MAX_BIC_LENGTH: number = 255;
currencies: Currency[] = currencies;
bankAccount: BankAccount;
bankAccountTypes: BankAccountType[] = bankAccountTypes;
organizationFinancialFormGroup: FormGroup = new FormGroup({});
#Input() errorCodes: ErrorCode[];
#Input() customer: Organization;
#Input() isEditModeActive: boolean;
constructor(public readonly controlContainer: ControlContainer) {}
ngOnInit(): void {
this.organizationFinancialFormGroup = this.controlContainer
.control as FormGroup;
if (this.customer.bankAccounts.length === 0) {
this.customer.bankAccounts = [new BankAccount()];
}
this.bankAccount = this.customer.bankAccounts[0];
this.organizationFinancialFormGroup.addControl(
'currency',
new FormControl(this.customer.currency, [])
);
this.organizationFinancialFormGroup.addControl(
'name',
new FormControl(this.bankAccount.name, [
Validators.maxLength(this.MAX_NAME_LENGTH),
])
);
this.organizationFinancialFormGroup.addControl(
'type',
new FormControl(this.bankAccount.type, [Validators.required])
);
let bankAccountNumber: String = '';
if (this.bankAccount.type === 'SEPA') {
bankAccountNumber = this.bankAccount.number;
}
this.organizationFinancialFormGroup.addControl(
'iban',
new FormControl(bankAccountNumber, [
Validators.maxLength(this.MAX_IBAN_LENGTH),
ibanValidator(),
])
);
bankAccountNumber = '';
if (this.bankAccount.type === 'OTHER') {
bankAccountNumber = this.bankAccount.number;
}
this.organizationFinancialFormGroup.addControl(
'otherBankAccountNumber',
new FormControl(bankAccountNumber, [
Validators.maxLength(this.MAX_OTHER_NUMBER_LENGTH),
Validators.pattern(/^[A-Za-z0-9\s]*$/),
])
);
this.organizationFinancialFormGroup.addControl(
'bic',
new FormControl(this.bankAccount.bic, [
Validators.maxLength(this.MAX_BIC_LENGTH),
])
);
}
}
I want to reuse the component only for the read only view , but in this case the parent component does not have any FormGroup, But the code breaks with the below error :
NullInjectorError: R3InjectorError(CustomersModule)[ControlContainer -> ControlContainer -> ControlContainer -> ControlContainer]:
NullInjectorError: No provider for ControlContainer!
Is it possible to reuse this component from a parent component without a formGroup ?
You can try to add #Optional() decorator when injecting ControlContainer like that:
constructor(#Optional() public readonly controlContainer: ControlContainer) {}
Then you also probably need if statement in ngOnInit
ngOnInit(): void {
if (!!this.controlContainer) {
// do your stuff here
} else {
// readonly mode stuff here
}
}

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

pipe operator not behaving as expected RXJS

Please look at my component below the purpose to is to listen on changes to an input, which it does and then emit the value to the parent component. I created a pipe to only emit every so often and therby minimize the calls to the api, for some reason even though I can see through various console.log statements that it goes in the pipe, it emits the value on every change. What is it that I am missing:
import {ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output, KeyValueDiffers, DoCheck, KeyValueDiffer} from '#angular/core';
import {BehaviorSubject, Observable, of} from "rxjs";
import {debounceTime, distinctUntilChanged, map, skip, switchMap, takeUntil, tap} from "rxjs/operators";
#Component({
selector: 'core-ui-typeahead-filter',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './typeahead-filter.component.html',
})
export class TypeaheadFilterComponent implements DoCheck {
#Input() id: string;
#Input() name: string;
#Input() caption: string;
#Input() placeholder: string;
#Input() cssClass: string;
#Input() cssStyle: string;
#Input() function: any;
#Input() data: Observable<string[]>;
differ: any;
detectChange: string = '';
// term$ = new BehaviorSubject<string>('');
text$ = new Observable<string>();
#Output() onTypeahead: EventEmitter<any> = new EventEmitter<any>();
#Output() onSelect: EventEmitter<any> = new EventEmitter<any>();
constructor(private differs: KeyValueDiffers) {
this.differ = this.differs.find({}).create();
}
handleTypeahead = (text$: Observable<string>) =>
text$.pipe(
distinctUntilChanged(),
debounceTime(500),
).subscribe((value) => {
this.onTypeahead.emit(of(value))
})
handleSelectItem(item) {
this.onSelect.emit(item);
}
ngDoCheck() {
const change = this.differ.diff(this);
if (change) {
change.forEachChangedItem(item => {
if (item.key === 'detectChange'){
console.log('item changed', item)
this.text$ = of(item.currentValue);
this.handleTypeahead(this.text$);
}
});
}
}
}
More background: There is an ngModel on the input linked to detectChange when it changes then the ngDoCheck is called and executes. Everything is done in observables so in the parent I can subscribe to the incoming events.
EDIT -------------------------------------------------------------------
Tried the following solution based on my understanding of #ggradnig answer, sadly it skips over my pipe something seems wrong with it, really not sure what:
handleTypeahead = (text$: Observable<string>) => {
this.test.subscribe(this.text$);
this.test.pipe(
distinctUntilChanged(),
debounceTime(500),
// switchMap(value => text$)
).subscribe((value) => {
tap(console.log('im inside the subscription',value))
this.onTypeahead.emit(value)
})
}
handleSelectItem(item) {
this.onSelect.emit(item);
}
ngDoCheck() {
const change = this.differ.diff(this);
if (change) {
change.forEachChangedItem(item => {
if (item.key === 'detectChange'){
console.log('item changed', item)
this.text$ = of(item.currentValue);
this.handleTypeahead(this.test);
}
});
}
}
}
You can do the following -
export class TypeaheadFilterComponent implements DoCheck {
#Input() id: string;
#Input() name: string;
#Input() caption: string;
#Input() placeholder: string;
#Input() cssClass: string;
#Input() cssStyle: string;
#Input() function: any;
#Input() data: Observable<string[]>;
differ: any;
detectChange: string = '';
// term$ = new BehaviorSubject<string>('');
text$ = new BehaviorSubject<string>('');
serachTerm$: Observable<string>;
#Output() onTypeahead: EventEmitter<any> = new EventEmitter<any>();
#Output() onSelect: EventEmitter<any> = new EventEmitter<any>();
constructor(private differs: KeyValueDiffers) {
this.differ = this.differs.find({}).create();
}
// handleTypeahead = (text$: Observable<string>) =>
// text$.pipe(
// distinctUntilChanged(),
// debounceTime(500),
// ).subscribe((value) => {
// this.onTypeahead.emit(of(value))
// })
ngOnInit() {
this.serachTerm$ = this.text$
.pipe(
distinctUntilChanged(),
debounceTime(500),
//filter(), //use filter operator if your logic wants to ignore certain string like empty/null
tap(s => this.onTypeahead.emit(s))
);
}
handleSelectItem(item) {
this.onSelect.emit(item);
}
ngDoCheck() {
const change = this.differ.diff(this);
if (change) {
change.forEachChangedItem(item => {
if (item.key === 'detectChange'){
console.log('item changed', item)
this.text$.next(item.currentValue);
}
});
}
}
}
Now, at the bottom of your template put the following line -
<ng-container *ngIf="searchTerm$ | async"></ng-container>
Having this line will keep your component code free form managing the subscription [i.e. need not to subscribe/unsubscribe]; async pipe will take care of it.

Angular2 : access method of a class in template

I have the following class in an Angular2 app
export class Contact {
constructor(
public has_reply: boolean,
public archived: boolean
) { }
getStatus() : string {
if (this.archived) {
return "Archived";
}
if (this.has_reply) {
return "Answered";
}
return "Waiting";
}
}
which is returned by a service
#Injectable()
export class ContactsService {
private contactsData : BehaviorSubject<Array<Contact>> = null;
constructor(private http: Http) {
this.contactsData = new BehaviorSubject<Array<Contact>>([]);
}
/**
* get the list of contacts
*/
populateContacts() : Array<Contact> {
return this.http.get('/api/contacts/').map(
(res: Response) => {return res.json()}
).subscribe(
jsonData => {
this.contactsData.next(<Array<Contact>> jsonData);
}
);
}
onContactsChanged() : Observable<Array<Contact>>{
return this.contactsData.asObservable();
}
}
which is used in a component
#Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
private contacts: Array<Contact> = [];
constructor(
private contactsApi : ContactsService
) { }
ngOnInit() {
this.contactsApi.onContactsChanged().subscribe(
(contacts: Array<Contact>) => {this.contacts = contacts;}
);
this.contactsApi.populateContacts();
}
}
and displayed in a template
<table class="table table-striped table-bordered">
<tr *ngFor="let contact of contacts">
<td>
{{ contact.getStatus() }}
</td>
</tr>
I get the following error
EXCEPTION: Error in ./HomeComponent class HomeComponent - inline
template:11:8 caused by: self.context.$implicit.getStatus is not a function
What is wrong in my approach? Does Angular2 allow to call a class method like this?
Note : Calling method from a Angular 2 class inside template looks similar question but it did not help
As suggested by #AdnanA, the problem is a casting issue. See How to do runtime type casting in TypeScript?
I fixed by casting each object of the array: See https://stackoverflow.com/a/32186367/117092
// Thank you! https://stackoverflow.com/a/32186367/117092
function cast<T>(obj, cl): T {
obj.__proto__ = cl.prototype;
return obj;
}
#Injectable()
export class ContactsService {
private contactsData : BehaviorSubject<Array<Contact>> = null;
constructor(private http: Http) {
this.contactsData = new BehaviorSubject<Array<Contact>>([]);
}
/**
* get the list of contacts
*/
populateContacts() : Array<Contact> {
return this.http.get('/api/contacts/').map(
(res: Response) => {return res.json()}
).subscribe(
jsonData => {
// WRONG! this.contactsData.next(<Array<Contact>> jsonData);
// FIXED BY
let contactsArray: Array<Contact> = [];
for (let i=0, l=jsonData.length; i<l; i++) {
let contact = cast<Contact>(jsonData[i], Contact);
contactsArray.push(contact);
}
this.contactsData.next(contactsArray);
}
);
}
onContactsChanged() : Observable<Array<Contact>>{
return this.contactsData.asObservable();
}
}
If the data is acquired async you need to guard against null
{{ contact?.getStatus() }}

Categories

Resources