I have a page form with values already setted, previous and next button elements.
At ngOnInit, I'm getting a List with 3 items from an observable, as initial value - sometimes I get 4 items.
Before I go to the next page I have to click, necessarily, on a button that will call the function calculate() that will make a request and my observable List will have 4 items.
So, when I click on next button onNextButton() I would like to use the initial value to compare with the current, if they are the same, or check if this list had any changes (any incrementing).
The way that I'm doing, I'm not manage to keep/store the first value. On next button click, i'm getting the updated value, instead the previous.
My code:
export class PageComponent implements OnInit {
questions$: Observable<any[]>;
hasChange: boolean;
ngOnInit() {
// GETTING INITIAL VALUE
this.questions$.subscribe((quest: any[]) => {
this.hasChange = quest.length > 3 ? true : false
});
}
calculate() {
// calling a request
// at this point, my observable will update and will have 4 items.
}
onNextButton() {
if (hasChange) {
// submit()
} else {
// navigate()
}
}
}
So, in this scenario the initial value should be a list w/ 3 items, but i'm getting 4 and its breaking my logic...
How do I get the previous value of Observable and store it in a variable?
Or, how can I detect any changes?
I tried behavioursubject and pairwise from RxJS, but I'm not sure how to apply it.
Thank you!!!
As you stated, you must use ReplaySubject or BehaviourSubject, since Observable does not have that "memory" for retrieve last emitted value.
ReplaySubject and BehaviourSubject are similar, both send last emitted value on subscription, but ReplaySubject only emits last emitted value on subscription and BehaviourSubject stores a list of last emitted values (configurable size). I'll be using BehavoiurSubject since is a bit more complete.
Just replace the Observable you were using with the BehaviourSubject, and keep in mind that you must definde that memory size when you instance the class. For example, you could create a method that returns a BehaviourSubject with last boolean stored like this:
private fooBehaviourSubject;
function ngOnInit: void{
this.fooBehaviourSubject = new BehaviorSubject<boolean>(1); // 1 is the "stack" size
this.fooBehaviourSubject.next(true); // <- First value emitted here
}
function getValue: BehaviourSubject<boolean> {
return this.fooBehaviourSubject;
}
When you subscribe like this:
getValue().subscribe(e => console.log(e))
the last stored value (true) automatically will be retrieved and shown in console, but ensure you at least have emitted one first, or you wont execute the subscription until one next method is called. After that, every update of the value will trigger that console.log with the updated value.
Applied to your code, you could create the BehaviourSubject in the ngOnInit, subscribe to it also in the ngOnInit to define the callback event, and call the next method of the BehaviourSubject once the list must be updated.
export class PageComponent implements OnInit {
questions$: Observable<any[]>
hasChange: boolean
ngOnInit() {
// GETTING INITIAL VALUE
let hasSome = false
this.questions$
.subscribe((quest: any[]) => {
// ### This is works for me!
hasSome = quest.some(item => item.id === 'whatever')
// ### This is the way that I was trying to do.
// ### Is's not wrong but didn't work for me =(
// this.hasChange = quest.some(item => item.id === 'whatever')
)
this.hasChange = hasSome
}
calculate() {
// calling a request
// at this point, my observable will update and will have 4 items.
}
onNextButton() {
// ### Now this.hasChange is assigned once and where I want, OnInit.
if (this.hasChange) {
// submit()
} else {
// navigate()
}
}
}
Related
iam trying to use async pipes instead of manually subscribing in components, in case, iam just displaying data which i get from server, everything works fine. But what if i need to change displayed part of displayed data later, based on some event?
For example, i have a component which displays timetable, which can be later accepted by user. I create timesheet$ observable and i use it in my template with async pipe. For accepting i created subject, so i can emit, when user accepts the timesheet. But how do i combine these two streams(approvementChange$ and timesheet$), so the $timesheet gets updated? I was trying combineLatest, but it returns the latest values, so i cant decide, if the value from approvementChange stream is new or old. Any ideas how to solve this?
export class TimesheetComponent {
errorMessage: string = '';
timesheet$: Observable<Timesheet>;
private approvementSubject = new Subject<TimesheetApprovement>();
approvementChange$ = this.approvementSubject.asObservable();
constructor(
private planService: PlanService,
private statusService: StatusService,
private notifService: NotificationService
) {}
this.timesheet$ = this.statusService.appStatus.pipe(
switchMap((status) => {
return this.planService.getTimesheet(
status.selectedMonth,
status.selectedAgency.agNum
);
})
); //get data every time user changed selected month
approveTimesheet(isApproved: boolean = true) {
const approvement: TimesheetApprovement = {
isApproved: isApproved,
approveTs: new Date(),
isLock: true,
};
this.approvementSubject.next(approvement);
}
}
But what if i need to change displayed part of displayed data later, based on some event?
RxJS provides lots of operators for combining, filtering, and transforming observables.
You can use scan to maintain a "state" by providing a function that receives the previous state and the newest emission.
Let's look at a simplified example using a generic Item interface:
interface Item {
id : number;
name : string;
isComplete : boolean;
}
We will use 3 different observables:
initialItemState$ - represents initial state of item after being fetched
itemChanges$ - represents modifications to our item
item$ - emits state of our item each time a change is applied to it
private itemChanges$ = new Subject<Partial<Item>>();
private initialItemState$ = this.itemId$.pipe(
switchMap(id => this.getItem(id))
);
public item$ = this.initialItemState$.pipe(
switchMap(initialItemState => this.itemChanges$.pipe(
startWith(initialItemState),
scan((item, changes) => ({...item, ...changes}), {} as Item),
))
);
You can see we define item$ by piping the initialItemState$ to the itemChanges$ observable. We use startWith to emit the the initialItemState into the changes stream.
All the magic happens inside the scan, but the set up is really simple. We simply provide a function that accepts the previous state and the new change and returns the updated state of the item. (In this case, I'm just naively apply the changes to the previous state; it's possible this logic would need to be more sophisticated for your case.)
This solution is completely reactive. The end result is a clean item$ observable that will emit the updated state of the current item (based on id), whenever the id changes or the changes occur on the item.
Here's a StackBlitz where you can see this behavior.
you need to track only approvementChange
after that you can pick the latest timesheet via withLatestFrom
approvementChange$
.pipe(withLatestFrom(this.timesheet$))
.subscribe(([accepted, latestTimesheet]) => accepted ? save(latestTimesheet) : void 0)
I'm at a loose end here and trying to understand the flow of how angular subscriptions work.
I make a call to an API and in the response I set the data in a behaviourSubject. So I can then subscribe to that data in my application.
Normally I would use async pipes in my templates cause its cleaner and it gets rid of all the subscription data for me.
All methods are apart of the same class method.
my first try.....
exportedData: BehaviourSubject = new BehaviourSubject([]);
exportApiCall(id) {
this.loadingSubject.next(true)
this.api.getReport(id).pipe(
catchError((err, caught) => this.errorHandler.errorHandler(err, caught)),
finalize(() => => this.loadingSubject.next(false))
).subscribe(res => {
this.exportedData.next(res)
})
}
export(collection) {
let x = []
this.exportCollection(collection.id); /// calls api
this.exportedData.subscribe(exportData => {
if(exportData){
x = exportData
}
})
}
console.log(x)//// first time it's empthy, then it's populated with the last click of data
/// in the template
<button (click)="export(data)">Export</button>
My problem is....
There is a list of buttons with different ID's. Each ID goes to the API and gives back certain Data. When I click, the console log firstly gives a blank array. Then there after I get the previous(the one I originally clicked) set of data.
I'm obviously not understanding subscriptions, pipes and behavior Subjects correctly. I understand Im getting a blank array because I'm setting the behaviour subject as a blank array.
my other try
export(collection) {
let x = []
this.exportCollection(collection.id).pip(tap(res => x = res)).subscribe()
console.log(x) //// get blank array
}
exportApiCall(id) {
return this.api.getReport(id).pipe(
catchError((err, caught) => this.errorHandler.errorHandler(err, caught))
)
}
Not sure about the first example - the placement of console.log() and what does the method (that is assigned on button click) do - but for the second example, you're getting an empty array because your observable has a delay and TypeScript doesn't wait for its execution to be completed.
You will most likely see that you will always receive your previous result in your console.log() (after updating response from API).
To get the initial results, you can update to such:
public exportReport(collection): void {
this.exportCollection(collection.id).pipe(take(1)).subscribe(res => {
const x: any = res;
console.log(x);
});
}
This will print your current iteration/values. You also forgot to end listening for subscription (either by unsubscribing or performing operators such as take()). Without ending listening, you might get unexpected results later on or the application could be heavily loaded.
Make sure the following step.
better to add console.log inside your functions and check whether values are coming or not.
Open your chrome browser network tab and see service endpoint is get hitting or not.
check any response coming from endpoints.
if it is still not identifiable then use below one to check whether you are getting a response or not
public exportReport(collection): void {
this.http.get(url+"/"+collection.id).subscribe(res=> {console.log(res)});
}
You would use BehaviourSubject, if there needs to be an initial/default value. If not, you can replace it by a Subject. This is why the initial value is empty array as BehaviourSubject gets called once by default. But if you use subject, it wont get called before the api call and you wont get the initial empty array.
exportedData: BehaviourSubject = new BehaviourSubject([]);
Also, you might not need to subscribe here, instead directly return it and by doing so you could avoid using the above subject.
exportApiCall(id) {
this.loadingSubject.next(true);
return this.api.getReport(id).pipe(
catchError((err, caught) => this.errorHandler.errorHandler(err, caught)),
finalize(() => => this.loadingSubject.next(false))
);
}
Console.log(x) needs to be inside the subscription, as subscribe is asynchronous and we dont knw when it might get complete. And since you need this data, you might want to declare in global score.
export(collection) {
// call api
this.exportApiCall(collection.id).subscribe(exportData => {
if (exportData) {
this.x = exportData; // or maybe this.x.push(exportData) ?
console.log(this.x);
}
});
}
Is there a way to treat an NgRx select as a Subject instead of BehaviorSubject?
I don't want to execute the next when the subscription is made the first time (default behavior). Instead, a want to execute it just when the state is changed.
this.store.pipe(select(fromAuth.selectErrorMessage)).subscribe((message) => {
/* This content is executed the first time the subscription is made,
and after that is executed everytime the state change */
this.isLoading = false;
this.errorMessage = { ...message };
});
this.store.pipe(select(fromAuth.selectErrorMessage)).pipe(skip(1))
will ignore the current value and only emit the next value.
I'm working with Observables to get data from the backend. In my function I'm subscribing to my observable and when I console.log() the data that is passed back it appears to return twice. First an empty object and then later the data I'm expecting.
This is causing a problem for me because I'm trying to use a for...in loop to compare the keys of the data with the keys of another object so I can match the values. I get a TypeError: Cannot read property '0' of undefined
because the data first returns an empty object. This is confusing to me because I'm doing console.log() inside the subscribe's callback method.
Isn't the whole point of the callback method to wait until the data has arrived?
I've tried a callback function as well as putting the for...in directly inside the subscribe and neither work because the object returns empty first. What am I doing wrong?
this.memberService.memberDetails.subscribe(member => {
this.member = member;
this.member_id = this.authService.loggedInUser.app_user_data.member_id;
this.parseAddress();
console.log('member subscribe', this.member);
this.formData();
});
// UPDATE: MemberService code
private _memberDetails = new BehaviorSubject<any>({});
public get memberDetails() {
return this._memberDetails.asObservable();
}
// Notice the console.log() has fired twice
formData() {
for (const flatProfileKey in this.profileData['permanent_address'][0]) {
for (const key in this.member['addresses'][0]) {
if (flatProfileKey === key) {
this.profileData[key] = this.flatProfile[key];
console.log('profileData perament_address', this.profileData['permanent_address'][0])
}
}
}
}
// If I try to loop through the data it returns an undefined error presumably because the subscribe first returns an empty object so there is nothing to loop through
This is expected as you are setting initial value to BehaviorSubject to empty object here:
private _memberDetails = new BehaviorSubject<any>({});
If you don't need an initial value you can consider using Subject instead of BehaviorSubject.
Read more about BehaviorSubject here: https://www.learnrxjs.io/subjects/behaviorsubject.html
This is working as expected.
Behavior subject will re-emit the last emitted value or the default value if no previous value has been emitted.
Now, see that you are providing the default value to the BehaviorSubject as an empty object. Since you are passing a default value to it, it will emit this default value to all the subscribers.
And when you retrieve the data and change the value of the BehaviorSubject then again it will emit the new data to the subscribers.
Also, if you are thinking to pass nothing as a default value to BehaviourSubject, you cannot do that.
Here, you have two options you can do:
You can add an additional if to check if the value emitted is default value {} or not. And according to that you handle the stuff. Here's the code:
formData() {
if(this.profileData['permanent_address']) {
// Data fetched, do something
for (const flatProfileKey in this.profileData['permanent_address'][0]) {
}
}
else {
// If data is not fetched, do something if you want to
}
}
You can use Subject instead of BehaviorSubject that doesn't need a default value. Since it doesn't have a value, it will not emit it before fetching the data. Just change below line:
private _memberDetails = new BehaviorSubject<any>({})
to
private _memberDetails = new Subject<any>()
And this will work as you expect it to work.
Hope this helps.
I am working through a Udemy course on RxJs 6 and need to ask this as it was not crystal clear to me.
Note: This is a type ahead tutorial I am currently in at the moment. So on the keyup event this method is firing off.
ngAfterViewInit() {
const searchLessons$ = fromEvent<any>(this.input.nativeElement, 'keyup')
.pipe(
map(event => event.target.value),
debounceTime(400),
distinctUntilChanged(),
// switchMap cancels prior calls.
switchMap(search => this.loadLessons(search))
);
const initialLessons$ = this.loadLessons();
this.lessons$ = concat(initialLessons$, searchLessons$);
}
Does the code mean,
for all events that fire the code will collect responses from completed calls to the loadLessons
the value of the event is referenced as search
then the => will trigger a call to the loadLessons(search)
Continue of 3: If the value of the event were lets just say an array of values, would that mean that for the => call, a separate call to the loadLessons(search) would be made passing for each individual array value
Continue of 3: or would it just pass in the entire array?
Here is line per line explanation:
ngAfterViewInit() {
const searchLessons$ = fromEvent<any>(this.input.nativeElement, 'keyup') // whenever keyup is triggered on this.input
.pipe(
map(event => event.target.value), // we extract input value from event target
debounceTime(400), // we wait for last event in 400ms span
distinctUntilChanged(), // we check that the input value did change
switchMap(search => this.loadLessons(search)) // and with that input value changed we call this.LoadLessons and then wait for its return
);
const initialLessons$ = this.loadLessons(); // this will call initial loadLeason
this.lessons$ = concat(initialLessons$, searchLessons$); // this will connect return of initial call and changes triggered by key up this is not secure for race conditions
}
Ad1. all key up events on input
Ad2. the value of input is referenced as search
Ad3. yes it would just push array as argument
Without seeing the loadLessons func, i can only assume. i will also assume you are using the concat Rxjs method.
So basically what the code does, get the "initial load of lessons" , and subscribe to it on the concatMethod, after that call completes, it goes to subscribe to the second observable searchLessons.
The searchLessons will be called again every input search, and add the new values to the lessons subscription, on the search observable .
If the params given to the loadSeassion is an array, it will depend how that method (loadSessions) works. not with rxjs although it can be done in this case i cant really tell you :)