How to update a behaviour subject after patch request? - javascript

How to update the view with new values after making a patch request?
I have created a dummy app on stackblitz.
https://stackblitz.com/edit/angular-j35vrb
On clicking any row from the table, a modal opens and status of the TODO is updated. If the status is incomplete = false, patch request is sent and status is updated to true (can be seen in the console log).
How do i inform the behaviourSubject (through which data is being fetched) that there is some update in the content? so that the table as well as the top section (No. of complete/incomplete Todos) shows updated values

I have updated your stackblitz with solution. Here is what you need to do.
In main.service.ts,
Keep a reference of todos within the service itself for future updates. See below.
private allTodos: Todo[];
getTodo() {
if(!this.allTodos$){
this.allTodos$ = <BehaviorSubject<Todo[]>>new BehaviorSubject(new Array<Todo>());
this.http
.get<Todo[]>(
'https://jsonplaceholder.typicode.com/todos'
).subscribe(
(data: Todo[]) => {
this.allTodos = data;
this.allTodos$.next(data);
}
),
error => console.log(error);
}
}
Return the BehaviourSubject directly from subscribetoTodos method. Subjects can be subscribed directly.
subscribetoTodos(): Observable<Todo[]>{
return this.allTodos$;
}
update 'updateToDo' method as below. Notice how I am updating one reference of Todo in the list here and sending it to subscribers via next.
updateTodo(id){
this.http.patch('https://jsonplaceholder.typicode.com/todos/' + id , { 'completed': true })
.subscribe(
data => {
this.allTodos.find(todo => {
if (todo.id === id) {
Object.assign(todo, data);
return true;
}
return false;
});
console.log(data);
this.allTodos$.next(Object.assign([], this.allTodos));
},
error => console.log(error)
)
}
This will update your view.
In case of paginated data, change the subscription to the below.
ngOnInit() {
this.todos$ = this.mainService.subscribetoTodos();
this.todos$.subscribe(
(data)=>{
this.page = this.page || 1;
this.pageSize = 10;
}
)
}
Notice how I am checking if this.page exist, then use that, else go to page 1.
this.page = this.page || 1;
Earlier it was,
this.page = 1;
which was resetting the page back to 1 whenever any update happened.

Related

Angular rxjs data not updating correctly when using share , map & filter

Based on searchCriteria collected from a form, I have a method that return observable of talents.
getTalents(searchCriteria) {
return this._allUsers$.pipe(
tap((talents) => console.log(talents)),
map((talents: any) => {
let filtered = talents.filter(
(t) =>
(t.userType === 'talent' || t.userType === 'both') &&
this.matchOccupationOrSkill(
searchCriteria.occupation,
t.occupation,
t.skills
)
);
if (searchCriteria.loggedInUserId) {
filtered = filtered.filter(
(t) => t.id !== searchCriteria.loggedInUserId
);
}
return filtered;
})
);
}
where _allUsers$ is a shared observable,
this._allUsers$ = firestore.collection<any>('users').valueChanges().pipe(share());
I am calling getTalents inside ngOnInit method of a route component TalentSearchResult.
ngOnInit(): void {
this.subs.add(
this.route.queryParams.subscribe(params => {
if (params.action==='homesearch' || params.action==='jobsearch'){
this.criteria = {
occupation:params.occupation,
industry:params.industry,
loggedInUserId:params.loggedInUserId,
}
this.dataService.getTalents(this.criteria).pipe(take(1)).subscribe(talents => {
this.count = talents.length;
if (this.count >0){
this.Talents = talents.sort((p1,p2)=> p2.createdate-p1.createdate);
}
})
}
})
)
}
Search works for the first time, then when I go back (using brower back button) and search with a new criteria, it routed correctly to the TalentSearchResult component, however, stale list is being returned. I debugged and found that line 3 in the first code section above, i.e. tap((talents) => console.log(talents)) already logs in the stale value, even before the pipe/map/filter is executed !
Please advice
If you are using _allUsers$ or dataService.getTalents() elsewhere in the app, then you should replace share() with shareReplay(1). Using share() allows multiple subscribers to an observable, but any late subscribers won't get any data until .valueChanges() emits a new value. Using shareReplay(1) will allow late subscribers to immediately get its last cached value.
If you're certain this is the only component subscribing to these observables, then it could be that your component isn't closing subscriptions properly. This may be caused by the nested subscriptions (an anti-pattern) you have inside your ngOnInit() event. I would recommend changing this logic to utilize operators:
this.subs.add(
this.route.queryParams.pipe(
filter(({ action }) => ['homesearch', 'jobsearch'].includes(action)),
map(({ occupation, industry, loggedInUserId }) => ({ occupation, industry, loggedInUserId })),
switchMap(criteria =>
this.dataService.getTalents(criteria).pipe(take(1))
),
filter(talents => !!talents.length),
tap(talents => this.Talents = talents.sort((p1, p2) => p2.createdate - p1.createdate))
).subscribe()
);

How to refresh a selector?

I have 2 components which is Component A and Component B , Component A is calling component B via selector , the app-team-users is the selector , it is calling data from API and then populating it on the table which is working fine.
In Component A I called the component B selector , in Component A I have a feature where I create a user or save a user .
How do we refresh or call the app-team-users selector so that if I save a user from Component A then the table on Component B will refresh which is the app-team-users will refresh ? cause they are seperate component. Maybe anyone has idea or enlightenment ? Thank you very much. Good day.
#Component A save user code
saveUser() {
if(this.createUserForm.valid) {
this.accountService.create(this.createUserForm.value).pipe(
finalize(() => {
this.isInProgress = false;
})
).subscribe({next: (res) => { this.notificationService.showSuccess('User has been created successfully.');
//if user is created refresh the selector
},
error: (err) => {this.notificationService.showError('Something went wrong, Try again later.');
this.isInProgress = false;
},
complete: () => {
this.isInProgress = false;
},
});
}
}
app-team-users table
#Code for calling the selector Component A
<form [formGroup]="modelForm" >
<app-team-users [selectedNewUser] = "newUsers" [selectedTransactionTeam]="modelForm.value.teams"
[selectedTransactionUser]="modelForm.value.users" (transactionUserEvent)="transactionUserEvent($event)"
(transactionTeamEvent)="transactionTeamEvent($event)">
</app-team-users>
</form>
#app-team-users code
ngOnInit(): void {
this.transactionUserTable.dataSource = new MatMultiSortTableDataSource(this.sort, this.CLIENT_SIDE);
this.transactionUserTable.nextObservable.subscribe(() => { this._transactionUserPageEvent(); });
this.transactionUserTable.sortObservable.subscribe(() => { this._transactionUserPageEvent(); });
this.transactionUserTable.previousObservable.subscribe(() => { this._transactionUserPageEvent(); });
this.transactionUserTable.sizeObservable.subscribe(() => { this._transactionUserPageEvent(); });
}
private _transactionUserPageEvent() {
this.isTransactionUserLoading = true;
this.transactionUserTable.data = [];
this._userProfileService.getUserProfileTableDropdown(
this.accountId,
this.transactionUserTable.pageIndex + 1,
this.transactionUserTable.pageSize,
this.searchTransactionUserInput.nativeElement.value,
this.transactionUserTable.sortParams,
this.transactionUserTable.sortDirs
)
.pipe(
finalize(() => this.isTransactionUserLoading = false)
)
.subscribe({
error: err => this._notificationService.showError(err),
next: res => {
this.transactionUserTable.totalElements = res.totalItemCount;
this.transactionUserTable.data = res.items as UserProfileDropdownDto[];
this.totalData = res.totalItemCount;
this.currentDisplayedData = res.lastItemOnPage;
},
complete: noop
});
}
You can do it via Observables.
Emit a value to an observable when a user is created in the first place. You can either pass the entire user value inside the observable and use that data to populate the second component from the front side itself. Or you can just pass a value, just to know in the second component that something has been created in the first one, then you call the same api to populate in the second component.

Assign observable within callback in Angular 9

I am completely a newbie to Angular. Some how struggled to get the initial code working now I am stuck here.
Use case : Show loading bar. Fetch merchant list to an observable. Stop loading bar.
Problem:
fetchUsers($event) {
let searchTerm = $event.term;
if (searchTerm.length > 3) {
this.isLoading=true;
this.people$ = null;
this.userService.getMerchantsBySearch(searchTerm);
--> this.isLoading=false;
}
}
this.isLoading becomes false even before getting the response from getMerchantsBySearch call. I know I will need to perform this step in callback but I am not sure how to.
I want to do something like this but I a missing something. What is the correct way to do it.
fetchUsers($event) {
let searchTerm = $event.term;
if (searchTerm.length > 3) {
this.isLoading=true;
this.people$ = null;
this.userService.getMerchantsBySearch(searchTerm).subscribe(
res={
---> this.people$ =res;
---> this.isLoading=false;
}
);
}
}
dashboard.component.ts
people$: Observable<IMerchant[]>;
fetchUsers($event) {
let searchTerm = $event.term;
if (searchTerm.length > 3) {
this.isLoading=true;
this.people$ = null;
this.userService.getMerchantsBySearch(searchTerm);
this.isLoading=false;
}
}
user.service.ts
getMerchantProfile(id){
var options = this.getOptions();
return this.http.get<IMerchant[]>(environment.APIBaseURL+"/getMerchantProfile/"+id,options);
}
merchant.model.ts
export interface IMerchant{
email:String,
mobile : String,
password: String,
businessName : String,
otp:Number,
info:string,
url:String
}
i prefer using the teardown method .add(). it doesn't pollute your pipe chain of data manipulation and is also executed after an error is handled:
this.userService.getMerchantsBySearch(searchTerm)
.pipe(
// manipulate data here
)
.subscribe(
res => {
// handle final data
},
error => {
// handle error
}
).add(() => {
// teardown here (e.g. this.isLoading=false;)
});
By the way: if this.people$ is an observable, you can not assign it inside the subscribe callback. you'll need to assign it to the response of the userService method you are using. Be careful not to call subscribe on the stream since it will return you a subscription instead of an observable:
this.people$ = this.userService.getMerchantsBySearch(searchTerm).pipe(...)
You can actually pipe it and add a finalize
this.userService.getMerchantsBySearch(searchTerm)
.pipe(finalize(() => this.isLoading=false))
.subscribe(
res => {
this.people$ =res;
});

Can I use condition in my action reducer?

Basically, in our case, we need to either get an alerts list that shows the first few items (mounting it first time in the DOM) or show the initial list + the next list (clicking a load more button).
Hence we needed to do this condition in our GET_ALERTS action:
case "GET_ALERTS":
if (action.initialList) {
newState.list = [...newState.list, action.res.data.list];
} else {
newState.list = newState.list.concat(
action.res.data.list
);
}
And when we call the action reducer in our Alerts component, we need to indicate whether initialList is true or false.
E.g.
componentDidMount() {
this.props.getAlerts(pageNum, true);
}
markAllAsRead() {
// other code calling api to mark all as read
this.props.getAlerts(pageNum, false);
}
readMore() {
// other code that increases pageNum state counter
this.props.getAlerts(pageNum, true);
}
Anyway in such a case, is it fine to use conditional statement in the reducer?
I am against this idea. The reducer has a single responsibility: update Redux state according to the action.
Here are three ways to slove this:
easy way - initialize your list in Redux state to empty list
if you set the list in state to empty list ([]) then it's much simpler.
You can basically just change your reducer to this:
case "GET_ALERTS":
return {...state, list: [...state.list, action.res.data.list]
This will make sure that even if you have get initial list or more items to add to the list, they will be appended. No need to add any logic - which is awesome IMHO.
redux-thunk and separating type into two different types
create two actions: GET_INIT_ALERTS and GET_MORE_ALERTS.
switch(action.type) {
case "GET_INIT_ALERTS":
return {...state, list: action.res.data.list }
case "GET_MORE_ALERTS":
return {...state, list: [...state.list, ...action.res.data.list]}
case "CHECK_READ_ALERTS":
return {...state, read: [...state.read, ...action.res.data.list]}
}
In the component I will have:
componentDidMount() {
this.props.getInitAlerts();
}
markAllAsRead() {
// other code calling api to mark all as read
this.props.getAlerts(pageNum, false);
}
readMore() {
// other code that increases pageNum state counter
this.props.getAlerts(pageNum);
}
In alerts action with the help of redux-thunk:
export const getAlerts = (pageNum : number) => (dispatch) => {
return apiAction(`/alerts/${pageNum}`, 'GET').then(res => dispatch({type: "GET_MORE_ALERTS", res});
}
export const getInitAlerts = () => (dispatch) => {
return apiAction('/alerts/1', 'GET').then(res => dispatch({type: "GET_INIT_ALERTS", res});
}
I guess you update pageNum after readMore or componentDidMount. Of course you can save that state in Redux and map it back to props and just increment it when calling the getAlerts action.
write your own middleware
Another way to do this is to write an ad-hoc/feature middleware to concat new data to a list.
const concatLists = store => next => action => {
let newAction = action
if (action.type.includes("GET") && action.initialList) {
newAction = {...action, concatList: action.res.data.list}
} else if (action.type.includes("GET") {
newAction = {...action, concatList: [...state[action.key].list, action.res.data.list]}
}
return next(newAction);
}
And change your reducer to simply push concatList to the state:
case "GET_ALERTS":
return {...state, list: action.concatList}
In addition, you will have to change your action to include key (in this case the key will be set to alert (or the name of the key where you store the alert state in redux) and initialList to determine whether to concat or not.
BTW, it's a good practice to put these two under the meta key.
{
type: "GET_ALERT",
meta: {
initialList: true,
key: "alert",
},
res: {...}
}
I hope this helps.
I would suggest you to have following set of actions:
ALERTS/INIT - loads initial list
ALERTS/LOAD_MORE - loads next page and then increments pageNo, so next call will know how many pages are loaded
ALERTS/MARK_ALL_AS_READ - does server call and reinitializes list
The store structure
{
list: [],
currentPage: 0
}
And component code should not track pageNum
componentDidMount() {
this.props.initAlerts();
}
markAllAsRead() {
this.props.markAllAsRead();
}
readMore() {
this.props.loadMore();
}

Angular 6: continuously execute http request inside ngAfterContentChecked after receive data from parent component

I'm new to Angular and I'm from React.js
I have a problem where Angular makes HTTP requests forever inside ngAfterContentChecked after receiving data from the parent component
I cannot make a request inside ngAfterContentInit because I need to wait for the parent to finish making a request
I just want it to call once right after getting data from the parent
in React.js normally I make a request inside componentWillReceiveProps
While in Angular I do this.
Parent component:
//codes...
groupId = '';
//codes...
ngOnInit() {
const id = '23esaddq3sdasd';
this.dataService.getUser(id).subscribe(data => {
//codes...
this.groupId = data.groupId;
//codes...
});
}
Child component:
data = [];
#Input() groupId;
fetch;
ngAfterContentChecked() {
if (this.groupId) {
const res = this.dataService.getGroupMembers(this.groupId).subscribe(data =>
this.data = data;
this.fetch = 'done';
});
if (this.fetch === 'done') {
res.unsubscribe();
}
}
}
I try to unsubscribe it but still continuously execute the code.
Anybody, please help me 😭
Instead of ngAfterContentChecked, you can use ngOnChanges() which get trigged when parent pass data to child component using #input().
ngAfterContentChecked lifecycle will be used if you have projection content 'ng-content' and access any DOM on parent Component change detection.
You want to use ngOnChanges because that is fired every time the #Input value changes.
The full signature for ngOnChanges is:
ngOnChanges(changes: SimpleChanges): void
So check the changes object to see if it contains a change to your #Input and that the value is as expected and THEN make the HTTP request like this:
ngOnChanges(changes: SimpleChanges) {
if (changes.groupId != null && fetch !== 'done') {
const res = this.dataService.getGroupMembers(this.groupId).subscribe(data =>
this.data = data;
this.fetch = 'done';
});
}
}
There is no need to unsub because subs to HTTP requests will complete automatically. To prove this do this:
const res = this.dataService.getGroupMembers(this.groupId).subscribe(data =>
this.data = data;
this.fetch = 'done';
},
err => {
console.log('I errored');
},
() => {
console.log('I completed and won't fire again');
});
PS The reason ngAfterContentChecked is firing like crazy is that it executes extremely regularly.
For me, I would go something like this:
Parent trigger http, you need to set a flag like isLoading = true
In the .html use *ngIf="!isLoading " to that child component
After parent done receiving request, set isLoading = false
This will make the child component rendered, and you can use ngOnInit hook inside child component to receive data from parent
After child component receive inputs from parent in ngOnInit you can trigger your http
Instead of ngOnChanges, you can use setter and getter which get trigged only when parent pass data to child component using #input().
ngOnChanges runs always when something is changed in any of the parent or child component.
private _name;
get name() { return this._name };
#Input() set name(val: string) {
if (val) {
this._name = val;
this.getGreetings(this.name)
}
};
greetings = '';
processing = false;
constructor(){}
getGreetings(name){
this.processing = true;
setTimeout(()=>{
this.greetings = 'Hi.. '+name+', How are you';
this.processing = false;
},2000)
}
Working demo link :- Ckick Here

Categories

Resources