(Closed) Integrate delete subject for an existing search stream - javascript

I've been working on a personal app. My goal is to have a search and delete users using Observables. For search functionality I have used this idea, this is how I implemented:
export class UserComponent implements OnInit {
users: Observable<User[]>;
private searchTerms = new Subject<string>();
ngOnInit(): void {
this.setUsers();
}
private setUsers(): void {
this.users = this.searchTerms
.debounceTime(300) // wait 300ms after each keystroke before considering the term
.distinctUntilChanged() // ignore if next search term is same as previous
.switchMap(term => term // switch to new observable each time the term changes
// return the http search observable
? this.userService.search(term)
// or the observable of all users if there was no search term
: this.userService.search(''))
.catch(() => {
this.response = -1;
return Observable.of<User[]>([]);
});
}
}
For delete functionality I use this:
private deleteSubject = new Subject();
delete(user: User): void {
this.userService
.delete(user)
.then(() => {
this.deleteSubject.next('');
})
.catch(() => {
console.log('error');
});
}
private setUsers(): void {
... // here comes the search functionality shown above
this.subscription = this.deleteSubject.subscribe(
(term: string) => this.users = this.userService.search(''),
(err) => console.log(err)
);
}
The problem that I faced with this implementation is that after I delete an user my search functionality is broken because I lost the subscriber for searchTerms because I reassigned this.users. So I've been searching and found that I can use merge operator to append another subject, follow this idea. Sadly I'm facing a problem with the implementation. I tried this:
delete(user: User): void {
...
// change
this.deleteSubject.next('');
// with
this.deleteSubject.next({op: 'delete', id: user.id});
}
private setUsers(): void {
...
this.users.merge(this.deleteSubject)
.startWith([])
.scan((acc: any, val: any) => {
if (val.op && val.op === 'delete') {
let index = acc.findIndex((elt: any) => elt.id === val.id);
acc.splice(index, 1);
return acc;
} else {
return acc.concat(val);
}
});
But my scan operator is never execute because this.deleteSubject doesn't have a subscriber for it. I know how to create and associate a subscriber to my this.deleteSubject but don't know how to mix it with my merge operator. Also see that it's getting a bit complex, I guess it should be another way to get this done. Comments are appreciated.
Please take a look at my plunker, hope gets a bit clear. I've simulated the calls to my services using a memory array userList.

I fixed my problem, I had to do two changes:
Assign this.users.merge(this.deleteSubject) to this.users
In the else block for scan operator change return acc.concat(val); to return val;
I updated my plunker if you want to take a look.
Cheers.

Related

Angular 9 typecast issue after http response

I have a component which retrieves a student info from an api upon its initialization.
This is the onIniti code on my cmponent-version1
ngOnInit(): void {
if(!this.student) {
this.studentsService.getStudentDetail(this.id).subscribe(
(response: Student) => {
this.student = response;
},
error => console.log(error)
)
}
}
and here is the function inside my student-service-version1
getStudentDetail(id: number): Observable<Student> {
return this.httpClient.get<Student>(`${this.studentsUrl}${id}/`, this.baseService.httpOptions);
}
Everything works fine. Now, just for didactic purpose (I'm new to javascript/typescript), I'd like to refactor my service in order to use a single get function which returns the list of students when called without parameter, and instead return a student detail info when called with a specific id.
This is the students-service-version2
getStudents(id?: number): Observable<Student[]> {
if(id)
return this.httpClient.get<Student[]>(`${this.studentsUrl}${id}/`, this.baseService.httpOptions);
else
return this.httpClient.get<Student[]>(this.studentsUrl, this.baseService.httpOptions);
}
Given that the signature of my function states it returns a students array observable, in my component I need a sort of typecasting from Student[] to Student. This is how I do it:
component-version2
ngOnInit(): void {
if(!this.student) {
this.studentsService.getStudents(this.id).subscribe(
(response: Student[]) => {
this.student = response[0] as Student;
},
error => console.log(error)
)
}
}
This doesn't work so after the init, student var remains undefined. I do not understand why, everything seems correct to me (although this refactoring it's not a good idea. Again, I just want to understand the error behind)
I'vs also try
this.student = response.pop() as Student; Same result, not working.
ngOnInit(): void {
if(!this.student) {
this.studentsService.getStudents(this.id).subscribe(
(response: Student[]) => {
// Hope, this.student will have type as any, public student: any
this.student = !this.id ? response[0] as Student : response as Student[];
},
error => console.log(error)
)
}
}
Always, try to return an array to avoid conflicts. In the above code,
the ternary operator will do your work. As, if you have an id that
means you are asking for particular student information otherwise you
are asking for all student records.
your service should be yelling at you right now because you're lying to the compiler... your return type isn't Observable<Student[]> its Observable<Student[] | Student>... i don't agree with the principal of one function for both single and list gets at all, but you could force it to be a list in the single case...
return this.httpClient.get<Student>(`${this.studentsUrl}${id}/`, this.baseService.httpOptions).pipe(
map(student => [student])
);
no typecasting will convert something to an array if its not an array. you need to explicitly make it an array if that's waht you want.
Overload the signature of your method as follows:
class StudentService {
get(id: number) | Observable<Student>;
get(): Observable<Student[]>;
get(id: number | undefined): Observable<Student[]> | Observable<Student> {
if(id !== undefined)
return this.httpClient.get<Student[]>(`${this.studentsUrl}${id}/`, this.baseService.httpOptions);
else
return this.httpClient.get<Student>(this.studentsUrl, this.baseService.httpOptions);
}
}
Notice how the method has been renamed to make sense in either case, how the return type is correlated with the presence of the id parameter, and how the check has been modified to accommodate the possibility of 0 as a valid id.

Angular RXJS retry a task based on a value

Ok, so I have a service that checks to see if a particular 3rd party JS plugin has loaded. I want to listen in for when it has entered the DOM, meanwhile it is in an undefined state. How do I do that? So far I have tried using a subject and retrying periodically but I can't get it to work:
$apiReady: Subject<boolean> = new Subject();
RegisterOnNewDocumentLoadedOnDocuViewareAPIReady(reControl: any): any {
this.$apiReady.asObservable().subscribe((isReady: boolean) => {
if (isReady) {
//Do something
return of(isReady);
}
})
let IsreControlInitialized = ThirdPartyAPI.IsInitialized(reControl);
if (IsreControlInitialized) {
this.$apiReady.next(true);
}
return throwError(false);
}
Then in the component:
this._apiService.RegisterOnAPIReady(this.elementID).pipe(
retryWhen(error => {
return error.pipe(delay(2000)); //<---- Doesn't work
})).subscribe((response: boolean) => {
if (response) {
//Do some stuff
}
});
My intentions were to check if the API element had been loaded, if not retry in 2 seconds but this doesn't work, can anyone help?
Throwing and catching an error until some condition is met is a little counter-intuitive to me.
My approach consists of using the interval operator along with takeUntil operator.
apiReady = new Subject();
interval(2000) // Check every 2s
.pipe(
map(() => this.isApiReady())
takeUntil(this.apiReady)
)
.subscribe(isReady => {
if (isReady) {
this.apiReady.next();
this.apiReady.complete();
}
})

(Angular) How to return different Observable type request in my case?

I am having trouble to return an observable. It seems like the codes inside the mergeMap is not running at all.
Codes:
book.service.ts
import {HttpClient, HttpHeaders} from '#angular/common/http';
export class bookService {
constructor(
private http: HttpClient,
...others
) {}
addNewBook(book): Observable<Book>{
##### Tried to use mergeMap otherwise the return type won't match
return this.tokenService.getToken().mergeMap((token: string) => {
console.log("Fire this...") <===== This is never output.
const myUrl = "www.testurl.com";
const parameters = {
bookTitle: book.name,
};
return this.http.post<Book>(myUrl, book);
})
}
token.service.ts
public token$: Subject<string>;
..others
public getToken(): Observable<string> {
return this.token$; <= return Observable<string> not Observable<Book>
}
book.component.ts that calls the addNewBook method.
...others
return Promise.resolve()
.then(() => {
return bookService.addNewBook(book);
}).then((result) => {
console.log(result);
})
I can't really change the token service because it's used on other place, I am not sure why the codes inside the mergeMap is not running. Can someone help me about it? Thanks a lot!
It won't work unless you subscribe to the results of bookService.addNewBook(book). Just returning it from the then callback won't subscribe. You need to at least add toPromise.
...others
return Promise.resolve()
.then(() => {
return bookService.addNewBook(book).toPromise();
}).then((result) => {
console.log(result);
})
In order for the mergeMap() to be be triggered, the token$ subject inside token.service.ts needs to emit a value (via .next()) after addNewBook() is subscribed to by a consumer.
One of the things to keep in mind with Subjects is that 'late subscribers' won't receive a value off of them until the next time .next([value]) is called on that Subject. If each subscriber, no matter how late, needs to immediately receive the last value generated by that source (Subject) then you can use BehaviorSubject instead.
From your short code example it is hard to see where the Observable generated by addNewBook() is being subscribed to though. Remember, a Observable won't execute until it has a subscriber.

Angular 2 async service cache

In my Angular 2 app, I have implemented a simple service cache using promises, and it works fine. However, I am not yet satisfied with my implementation:
export class Cache<T extends Cacheable<T, I>, I> {
items: T[];
init(items: T[]): void {
this.items = items;
}
isInitialised(): boolean {
return this.items != null;
}
all(): Promise<T[]> {
return Promise.resolve(this.items);
}
get(id: I): Promise<T> {
return Promise.resolve(this.items.find(item => item.id == id));
}
put(item: T): void {
this.items.push(item);
}
update(item: T): void {
this.get(item.id)
.then(cached => cached.copy(item));
}
remove(id: I): void {
let index = this.items.findIndex(item => item.id == id);
if (index > -1) {
this.items.splice(index, 1);
}
}
}
I would like the cache to initialise no matter which method is called, whether it is .all, .get etc. But, the key is that I want the first call to this.items to force the cache to initialise. Any subsequent call should somehow wait until the cache is initialised before manipulating ´this.items´.
To do so, I thought of using a proxy method .itemsProxy() which would return a promise, and replace any call to .items with that method and adapt code consequently. This would require having a cache state which is either NOT_INITIALSED, INITIALSING or INITIALSED.
The proxy method would look like this:
export class Cache<T extends Cacheable<T, I>, I> {
constructor(private service: AbstractService) {}
initPromise: Promise<T[]>;
state: NOT_INITIALISED;
itemsProxy(): Promise<T> {
if (this.state == INITIALISED)
return Promise.resolve(this.items);
if (this.state == NOT_INITIALISED) {
this.initPromise = this.service.all().then(items => {
// Avoid override when multiple subscribers
if (this.state != INITIALISED) {
this.items = items;
this.state = INITIALISED;
}
return this.items;
});
this.state = INITIALISING;
}
return this.initPromise;
}
...
}
Notice how initialisation is done via the this.service.all(), which looks like this:
all(): Promise<T[]> {
if (this.cache.isInitialised()) {
return this.cache.all();
}
return this.http
.get(...)
.toPromise()
.then(response => {
this.cache.init(
response.json().data.map(item => new T(item))
);
return this.cache.all();
}); // No catch for demo
}
Note that new T(...) is not valid, but I simplified the code for demo purposes.
The above solution does not seem to work like I'd expect it to, and asynchronous calls are hard to debug.
I thought of making this.items an Observable, where each call would subscribe to it. However, my understanding of observables is quite limited: I don't know if a subscriber can modify the data of the observable it is subscribed to (i.e. are observables mutable like a T[] ?). In addition, it seems to me that observables "produce" results, so after the cache is initialised, would any new subscriber be able to access the data produced by the observable ?
How would you implement this kind of "synchronization" mechanism ?

How to call a function after the completion of two Angular 2 subscriptions?

I am on Angular 2.3.1 and I am fairly new to both Angular and event based programming. I have two subscriptions, route.params.subscribe and engineService.getEngines(). In my onInit I want to call getEngineName after this.route.params.subscribe and this.engineService.getEngines().subscribe complete.
Reason for this: getEngineName functions depends on the engineId from the queryParams and the engines array which is populated after the completion of getEngines() call.
I did look at flatMap and switchMap but I did not completely understand them.
This is the code in the component:
export class ItemListComponent implements OnInit {
items: Item[];
engines: Engine[];
private engineId: number;
constructor(
private router: Router,
private route: ActivatedRoute,
private itemService: ItemService,
private engineService: EngineService
) {}
ngOnInit() {
this.route.params.subscribe((params: Params) => {
this.engineId = +params['engineId'];
// itemService is irrelevant to this question
this.itemService.getItems({engineId: this.engineId})
.subscribe((items: Item[]) => {
this.items = items;
});
});
this.engineService.getEngines()
.subscribe(engines => this.engines = engines);
// This should only run after engineId and engines array have been populated.
this.getEngineName(this.engineId);
}
getEngineName(engineId: number) {
this.engines.find((engine) => {
return engine.id === engineId;
})
}
}
Why don't you just move the logic inside the route.params callback?
this.route.params.subscribe((params: Params) => {
this.engineId = +params['engineId'];
// itemService is irrelevant to this question
this.itemService.getItems({engineId: this.engineId})
.subscribe((items: Item[]) => {
this.items = items;
});
//this.engineId is defined here (1)
this.engineService.getEngines()
.subscribe((engines) => {
this.engines = engines;
//this.engines is defined here (2)
this.getEngineName(this.engineId);
});
});
with flatMap and forkJoin:
this.route.params.flatMap((params: Params) => {
this.engineId = +params['engineId'];
return Observable.forkJoin(
this.itemService.getItems({engineId: this.engineId}),
this.engineService.getEngines()
)
}).subscribe((data)=>{
let items = data[0];
let engines = data[1];
this.items = items;
this.engines = engines;
this.getEngineName(this.engineId);
});
switchMap is recommended in this scenario.
this.route.params.pluck('engineId') //pluck will select engineId key from params
.switchMap(engineId => {
this.getItems(engineId);
return this.engineService.getEngines().map(engines => {
/*this.engineService.getEngines() emits the list of engines.
Then use map operator to convert the list of engines to engine we are looking for
*/
return engines.find((engine) => {
return engine.id === engineId;
})
})
}).subscribe(engine => {
//engine
})
getItems(engineId) {
this.itemService.getItems({engineId: engineId})
.subscribe((items: Item[]) => {
this.items = items;
});
}
Suppose the engineId in the params changes, the first observable this.route.params.pluck('engineId') will emit data, which will cause the next observable this.engineService.getEngines() to get fired. Now suppose the route changes before this observable emits the data. Here you need to cancel getEngines observable to prevent error. This is done by switchMap.
switchMap cancels inner observable if outer observable is fired.
PS: I have avoided keeping any states like this.engineId etc.

Categories

Resources