I am currently writing an ngbTypeahead search and I am stuck because I have never really worked with Observables, which are the expected return type of the ngbTypeahead search.
The search function in my component looks like this:
search: OperatorFunction<string, readonly LoqateResponseModel[]> = (text$: Observable<string>) => {
return text$.pipe(
switchMap(term => this.addressService.searchAddress(term, this.countryCode)),
map(results => {
const searchResults = results[LoqateKeyEnum.ITEMS] as [];
const searchResultLoqateModels: LoqateResponseModel[] = [];
searchResults.forEach(result => {
searchResultLoqateModels.push(new LoqateResponseModel(
result[LoqateKeyEnum.ID],
result[LoqateKeyEnum.TYPE],
result[LoqateKeyEnum.TEXT],
result[LoqateKeyEnum.HIGHLIGHT],
result[LoqateKeyEnum.DESCRIPTION]));
});
return searchResultLoqateModels;
})
);
}
resultFormatter = (loqateResponse: LoqateResponseModel): string => loqateResponse.display();
I am conducting a loqate search and am storing the results as model objects in a list and return them.
public searchAddress(searchValue, countryCode): Observable<object>
{
const httpParams = new HttpParams();
return this.httpClient.post(this.addressSearchUrl, {}, {
headers: this.headers,
params: new HttpParams()
.set('Key', loqateKey)
.set('Text', searchValue)
.set('Origin', countryCode)
});
}
The Model looks like this:
export class LoqateResponseModel {
constructor(
public id: string,
public type: LoqateTypeEnum,
public text: string,
public highlight: string,
public description: string) {
}
public isAddress(): boolean { return this.type === LoqateTypeEnum.ADDRESS; }
public display(): string { return this.text + ', ' + this.description; }
}
Now I thought, that a list of LoqateResponseModels is stored as result of the search and then each of these list items are being formatted properly to display in the typeahead popup through the resultFormatter.
tldr: I want to search something with the ngbTypeahead and query the search term from an API endpoint and display the search results in the typeahead popup.
Edit: I've edited the answer, this code is working.
I think you are looking for switchMap. This operator will subscribe you an observable source and emit its results:
You don't want to return null, you simply return the observable with your piped modification.
Your operator function should take an observable and return an observable. Your observable can simply use switchMap to map the incoming text to the api call.
search: OperatorFunction<string, readonly LoqateResponseModel[]> = text$ =>
text$.pipe(
switchMap(text => this.searchAddress(text))
);
Each time switchMap receives some text, it will do a few things for you:
subscribe to searchAddress(text)
emit results from this subscription
stop emitting results from previous subscriptions
Related
I have a dbService that calls the database!
//DB service code -----------------------
private changedEvents = new BehaviorSubject<IEvent[]>(null);
broadCastEvents = this.changedEvents.asObservable();
getEvents() {
this.http.get<IEvent[]>(this.configUrl + 'Event/GetEvents').subscribe(
(data: IEvent[]) => {
this.changedEvents.next(data)
});
}
In my component on ngOnInit I starts listening to this
ngOnInit(): void {
this.dbService.broadCastEvents.subscribe(data => {
this.events = data;
})
// this.dbService.getEvents();
}
Now all of this working like a charm! But now I'm only interested in records where this.events.type == 2
I tried by a standard filtering like below!
ngOnInit(): void {
this.dbService.broadCastEvents.subscribe(data => {
this.events = data.filter(event => event.eventTypeRefId == 2);
})
// this.dbService.getEvents();
}
But it results in the following Error!? Any ideas how to this in a better way (that works :-))
core.js:6241 ERROR TypeError: Cannot read property 'filter' of null
at SafeSubscriber._next (start-training.component.ts:26)
at SafeSubscriber.__tryOrUnsub (Subscriber.js:183)
at SafeSubscriber.next (Subscriber.js:122)
at Subscriber._next (Subscriber.js:72)
at Subscriber.next (Subscriber.js:49)
at BehaviorSubject._subscribe (BehaviorSubject.js:14)
at BehaviorSubject._trySubscribe (Observable.js:42)
at BehaviorSubject._trySubscribe (Subject.js:81)
at BehaviorSubject.subscribe (Observable.js:28)
at Observable._subscribe (Observable.js:76)
ngOnInit(): void {
this.dbService.broadCastEvents.pipe(filter(event => event.eventTypeRefId == 2)).subscribe(data => {
this.events = data
})
// this.dbService.getEvents();
}
Reference:
https://rxjs-dev.firebaseapp.com/guide/operators
There are multiple ways for it. One way is to use array filter like you did. Other way would be to use RxJS filter pipe as shown by #enno.void. However both these methods might still throw an error when the notification is null. And since the default value of your BehaviorSubject is null, there is high probability of hitting the error again.
One workaround for it is to use ReplaySubject with buffer 1 instead. It's similar to BehaviorSubject in that it holds/buffer the last emitted value and emits it immediately upon subscription, except without the need for a default value. So the need for initial null value is mitigated.
Try the following
private changedEvents = new ReplaySubject<IEvent[]>(1);
broadCastEvents = this.changedEvents.asObservable();
...
Now the error might still occur if you were to push null or undefined to the observable. So in the filter condition you could check for the truthiness of the value as well.
ngOnInit(): void {
this.dbService.broadCastEvents.subscribe(data => {
this.events = data.filter(event => {
if (event) {
return event.eventTypeRefId == 2;
}
return false;
});
});
}
I have a plain text on local server that i want to display on my web page using angular. I have my model, service and component.
So I am getting an error at this line >> this.paragraphs = this.announcement.details.split('#');
I tried using ? operator (this.paragraphs = this.announcement?.details.split('#')) but it could not build.
MODEL
export class Announcements
{
public id: number;
public details: string;
public date: Date;
}
SERVICE
getAnnouncementById(id)
{
return this.http.get<Announcements>('http://localhost:49674/api/Announcements/' + id)
.pipe(catchError(this.errorHandler));
}
COMPONENT-.ts
import { Announcements } from '../models/Announcement';
export class ReadMoreComponent implements OnInit
{
public announcementId;
public announcement : Announcements
public paragraphs = [];
constructor(
private route: ActivatedRoute,
private router:Router,
private announcementservice: AnnouncementsService
){}
ngOnInit()
{
this.route.paramMap.subscribe((params:ParamMap) => {
let id = parseInt(params.get('id'));
this.announcementId = id;
this.getAnnouncenentById(id)
//split
this.paragraphs = this.announcement.details.split('#');
})
}
getAnnouncenentById(id){
this.announcementservice.getAnnouncementById(id)
.subscribe(data => this.announcement = data);
}
COMPONENT-.html
<div class="article column full">
<div *ngFor=" let paragraph of paragraphs">
<p>{{paragraph.details}}</p>
</div>
</div>
this.paragraphs = this.announcement.details.split('#'); is called before this.announcement = data so this.annoucement is undefined in that moment.
To be sure that both values already comes form observables you can use combineLatest function or switchMap operator.
Adding ? operator is workaround. Your observable still can call with unexpected order.
e.g.:
this.route.paramMap.pipe(switchMap((params: ParamMap) => {
let id = parseInt(params.get('id'));
this.announcementId = id;
this.getAnnouncenentById(id);
return this.announcementservice.getAnnouncementById(id)
})).subscribe((data) => {
this.announcement = data
this.paragraphs = this.announcement.details.split('#');
});
In that code subscription will start after first observable emits value.
I have a model class named user :
export class User {
constructor(
public UserId?: number,
public Name?: string,
public Password?: string,
public IsActive?: boolean,
public RoleId?: number
) {
}
}
For ng2-select component, I need these properties : text and id.
Now when I set them via
this.userService.getAllUsers().subscribe(
data => this.users = data,
err => this.error(err));
there are no text and id property.
Is there a method where I can set these properties on initialization.
I dont wan't to write everytime a workaround with :
data.forEach(g =>
{
g.text = g.Name;
g.id = g.Id;
});
this.users = data;
Since it seems you are not typing your array users to your User class anyway in the above code snippet, so you could introduce another class and where you handle this assignment in map.
export class UserSelect {
constructor(
public text: string,
public id: number
) {
}
and in your Service:
getAllUsers() {
return this.http......
.map(res => res.json().map(x => new UserSelect(x.Name, x.Id)))
}
and then you just subscribe normally
this.userService.getAllUsers()
.subscribe(data => {
this.users = data;
})
Then you would have an Array of type UserSelect which has the proper properties you need :) Of course you can extend this and include any properties you need, keeping in mind that not knowing to which extent you are using the users array and if you are needing the other properties, or original properties...
I have an array of Threads Objects with ID, title and a isBookmarked:boolean.
I've created a Subject and I want to subscribe to it in order to get an array of Thread Objects with isBookmarked=true.
https://plnkr.co/edit/IFGiM8KoSYW6G0kjGDdY?p=preview
Inside the Service I have
export class Service {
threadlist:Thread[] = [
new Thread(1,'Thread 1',false),
new Thread(2,'Thread 2',true),
new Thread(3,'Thread 3',false),
new Thread(4,'Thread 4',true),
new Thread(5,'Thread 5',true),
new Thread(6,'Thread 6',false),
new Thread(7,'Thread 7',false),
]
threadlist$:Subject<Thread[]> = new Subject<Thread[]>()
update() {
this.threadlist$.next(this.threadlist)
}
}
in the component
export class AppComponent implements OnInit {
localThreadlist:Thread[];
localThreadlistFiltered:Thread[];
constructor(private _service:Service){}
ngOnInit():any{
//This updates the view with the full list
this._service.threadlist$.subscribe( threadlist => {
this.localThreadlist = threadlist;
})
//here only if isBookmarked = true
this._service.threadlist$
.from(threadlist)//????
.filter(thread => thread.isBookmarked == true)
.toArray()
.subscribe( threadlist => {
this.localThreadlistFiltered = threadlist;
})
}
update() {
this._service.update();
}
}
which Instance Method do I use in general to split an array?
Also is there a better way to do it?
Thanks
You would leverage the filter method of JavaScript array within the map operator of observables:
this._service.threadlist$
.map((threads) => {
return threads.filter((thead) => thread.isBookmarked);
})
.subscribe( threadlist => {
this.localThreadlistFiltered = threadlist;
});
See this plunkr: https://plnkr.co/edit/COaal3rLHnLJX4QmvkqC?p=preview.
As I'm learning Angular 2 I used an observable to fetch some data via an API. Like this:
getPosts() {
return this.http.get(this._postsUrl)
.map(res => <Post[]>res.json())
.catch(this.handleError);
}
My post model looks is this:
export class Post {
constructor(
public title: string,
public content: string,
public img: string = 'test') {
}
The problem I'm facing is that the map operator doesn't do anything with the Post model. For example, I tried setting a default value for the img value but in the view post.img displays nothing. I even changed Post[] with an other model (Message[]) and the behaviour doesn't change. Can anybody explain this behaviour?
I had a similar issue when I wanted to use a computed property in a template.
I found a good solution in this article:
http://chariotsolutions.com/blog/post/angular-2-beta-0-somnambulant-inauguration-lands-small-app-rxjs-typescript/
You create a static method on your model that takes an array of objects and then call that method from the mapping function. In the static method you can then either call the constructor you've already defined or use a copy constructor:
Mapping Method
getPosts() {
return this.http.get(this._postsUrl)
.map(res => Post.fromJSONArray(res.json()))
.catch(this.handleError);
}
Existing Constructor
export class Post {
// Existing constructor.
constructor(public title:string, public content:string, public img:string = 'test') {}
// New static method.
static fromJSONArray(array: Array<Object>): Post[] {
return array.map(obj => new Post(obj['title'], obj['content'], obj['img']));
}
}
Copy Constructor
export class Post {
title:string;
content:string;
img:string;
// Copy constructor.
constructor(obj: Object) {
this.title = obj['title'];
this.content = obj['content'];
this.img = obj['img'] || 'test';
}
// New static method.
static fromJSONArray(array: Array<Object>): Post[] {
return array.map(obj => new Post(obj);
}
}
If you're using an editor that supports code completion, you can change the type of the obj and array parameters to Post:
export class Post {
title:string;
content:string;
img:string;
// Copy constructor.
constructor(obj: Post) {
this.title = obj.title;
this.content = obj.content;
this.img = obj.img || 'test';
}
// New static method.
static fromJSONArray(array: Array<Post>): Post[] {
return array.map(obj => new Post(obj);
}
}
You can use the as keyword to de-serialize the JSON to your object.
The Angular2 docs have a tutorial that walks you through this. However in short...
Model:
export class Hero {
id: number;
name: string;
}
Service:
...
import { Hero } from './hero';
...
get(): Observable<Hero> {
return this.http
.get('/myhero.json')
.map((r: Response) => r.json() as Hero);
}
Component:
get(id: string) {
this.myService.get()
.subscribe(
hero => {
console.log(hero);
},
error => console.log(error)
);
}