Trigger a function once 2 inputs are set and then afterwards if either of the value changes in Angular - javascript

I have a component with 2 inputs (or mor) and I want to:
Trigger a method X the first time, when both value are set and exists
Trigger a method X each time, if either one of the both value changes
<some-cmp [itemid]="activeItemId$ | async" [userId]="activeUserId$ | async"></some-cmp>
Both values can change at any time, so I figured using rxjs to build a stream lets me control everything. My current solution seems a bit hacky and is difficult to test. I use 2 BehaviourSubjects and combineLatest with a debounceTime.
#Input() set itemId (id){this.itemId$.next(id)};
#Input() set userId (id){this.userId$.next(id)};
itemId$ = new BehaviourSubject$(null);
userId$ = new BehaviourSubbject$(null);
ngOnInt(){
combineLatest([
this.itemId$.pipe(filter(item=>item!===null)),
this.userId$.pipe(filter(item=>item!===null))
]).pipe(
debounceTime(10),
switchMap(...)
).subscribe(...)
}
So my question are
Is there a more elegant way to achieve this behavior?
Is there a way to avoid the debounceTime, which makes testing difficult?
The debounceTime is used in case both value do arrive at the same time and I don't want combineLatest to trigger the method twice.

You are right in using combineLatest, it will only emit the first time after each source has emitted once and then will emit any time any source emits.
Is there a way to avoid the debounceTime. [It] is used in case both value do arrive at the same time and I don't want combineLatest to trigger the method twice.
Maybe debounceTime isn't necessary due to the initial behavior of combineLatest; it will not emit the first time until all sources emit. However if you typically receive subsequent emissions from both sources that occur within a short timeframe, use of debounceTime may be an appropriate optimization.
Is there a more elegant way to achieve this behavior?
I think your code is fine. However, it may not be necessary to use BehaviorSubject since you aren't really using the default value. You could use plain Subject or ReplaySubject(1).
You could assign the result of your combineLatest to another variable and subscribe to that inside ngOnInit or use the async pipe in the template:
#Input() set itemId (id){ this.itemId$.next(id) };
#Input() set userId (id){ this.userId$.next(id) };
itemId$ = new Subject<string>();
userId$ = new Subject<string>();
data$ = combineLatest([
this.itemId$.pipe(filter(i => !!i)),
this.userId$.pipe(filter(i => !!i))
]).pipe(
debounceTime(10),
switchMap(...)
);
ngOnInit() {
this.data$.subscribe(...);
}

Angular provides ngOnChanges hook which can be used in this scenario. It'll trigger ngOnChanges method whenever there's a change in any of the inputs of the component.
Below is an example of how this can be achieved:
export class SomeComponent implements OnChanges {
#Input() itemId: any;
#Input() userId: any;
ngOnChanges(changes: SimpleChanges) {
const change = changes.itemId || changes.userId;
if (change && change.currentValue !== change.previousValue) {
this.doSomething();
}
}
private doSomething() {
// Your logic goes here
}
}
Your HTML will now look clean and you can get rid of async as well:
<some-cmp [itemid]="itemId" [userId]="userId"></some-cmp>

Related

Modify data in stream used with angular async pipe reactive way

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 need to return a String but only have an Observable

Another Observable question.
I'm working with angular translation service and Kendo.UI components.
According to the Kendo.UI documentation, I can implement a MessageService to translate its components.
To do so, I have to extend an abstract class that contains a method that return a string:
#Injectable()
export class MyMessageService extends MessageService {
private messages = {'kendo.upload.select': 'initial value'}
constructor(private readonly translate: TranslateService) {
super();
this.translate.get('my_translation_key')
.subscribe((value) => {
this.messages['kendo.upload.select'] = value;
});
}
public get(key: string): string {
return this.messages[key];
}
}
The problem is, the moment kendo injects my custom service, the observables were not resolved yet and my button never get its value changed from "'initial value'" to the translated.
From my understatement, kendo's service should accept an observable, not a string. But I'm not very familiar with promises/subjects/observables.
Is there a workaround for this?
ps.: I know TranslateService has a .instant(key) method, but the same way, the values are not loaded yet.
It sounds like you are saying it the observable is pushed before kendo subscribes. If that is the case then you should use a ReplaySubject instead of an Observable as the Replaysubject will push all the values that were given to the observable to the subscriber when it subscribes.You can tell it how many observed values to replay.
Also you could try instead to call your server after view init or even OnInint. Calling the service in the constructor is not recommended as the view is not done rendering and any html async pipes may not be set yet. Worst case you can use the Replay subject.

Why is this change emitted using Rxjs

I'm a little bit confused as why the following snipped works as expected.
The idea of this service is to have a list of strings where if you add a string, it is removed 5 seconds later. Rxjs is used here:
#Injectable()
export class ErrorService {
private errors: Array<string> = [];
private emitErrorsChanged = new Subject<any>();
public emitErrorsChanged$ = this.emitErrorsChanged.asObservable();
constructor() {
this.emitErrorsChanged$.delay(5000).subscribe(
() => {
if (this.errors.length > 0) {
this.errors.shift();
}
}
);
}
public emitErrorChange(error: string) {
this.errors.push(`${error}`);
this.emitErrorsChanged.next(this.errors);
}
}
An error component is subscribed to this service errorService.emitErrorsChanged$.subscribe(...) and shows the strings in a list. Other components/services add strings by this.errorService.emitErrorChange(error.message).
My question is: why are the removed errors (5s) emitted to the error component? The errors are just removed from the list this.errors.shift(); but the change is not emitted by this.emitErrorsChanged.next(this.errors);
The behavior occurs because you are passing reference to your object (list in this case). The changes made by this.errors.shift(); are not emitted, but I guess you can see current state of this.errors thanks to Angular's change detection. I have prepared a demo (click) so you can see that the object reference is passed in your case - what means that the list in subscription is the exactly same array list. To prevent it you can pass a copy of your list, e.g. using spread operator like in this example:
this.emitErrorsChanged.next([...this.errors]);

Subcription outside OnInit

I have two child components and i want pass data between them, to do this i use service.
In service i have
headerObject = new Subject<HeaderObject>();
In first component i have method
onClick() {
this.listImage = this.imageService.getImages();
let headers = new HeaderObject();
headers.file = this.listImage[0].data;
this.documentService.getOcrDocument(headers).subscribe(res => {
headers = res;
this.ocrService.headerObject.next(headers);
})
In second component
headerSubcribed: HeaderObject;
private headerSubscription: Subscription;
ngOnInit() {
this.headerSubscription =
this.ocrService.headerObject.subscribe((ocrHeader: HeaderObject) => {
this.headerSubcribed = ocrHeader;
}
But ngOnInit() only once after render view i want to move this code to outside ngOnit and exeute it but i don't know how ? Some tips how execute that code? I set breakpoint at next() method that emit value but later method subscribe isn't execute? What could I do wrong?
There are 2 parts which you observable/subscribe
Observing something and notifying.
In your service, you have a Subject type variable and you are pushing values to it by calling next(). when this method called, it notifies all the observers means who ever is subscribing it.
listening to the observer and do action
When you listen to an observer, you get an update when they change the value. It does not matter if you put your subscription in onNginit or constructor, if the code is there, it will notify you when there is a change into it.
You can also explore BevavourSubject if it fits to your situation.

Angular 2: How to detect changes in an array? (#input property)

I have a parent component that retrieves an array of objects using an ajax request.
This component has two children components: One of them shows the objects in a tree structure and the other one renders its content in a table format. The parent passes the array to their children through an #input property and they display the content properly. Everything as expected.
The problem occurs when you change some field within the objects: the child components are not notified of those changes. Changes are only triggered if you manually reassign the array to its variable.
I'm used to working with Knockout JS and I need to get an effect similar to that of observableArrays.
I've read something about DoCheck but I'm not sure how it works.
OnChanges Lifecycle Hook will trigger only when input property's instance changes.
If you want to check whether an element inside the input array has been added, moved or removed, you can use IterableDiffers inside the DoCheck Lifecycle Hook as follows:
constructor(private iterableDiffers: IterableDiffers) {
this.iterableDiffer = iterableDiffers.find([]).create(null);
}
ngDoCheck() {
let changes = this.iterableDiffer.diff(this.inputArray);
if (changes) {
console.log('Changes detected!');
}
}
If you need to detect changes in objects inside an array, you will need to iterate through all elements, and apply KeyValueDiffers for each element. (You can do this in parallel with previous check).
Visit this post for more information: Detect changes in objects inside array in Angular2
You can always create a new reference to the array by merging it with an empty array:
this.yourArray = [{...}, {...}, {...}];
this.yourArray[0].yourModifiedField = "whatever";
this.yourArray = [].concat(this.yourArray);
The code above will change the array reference and it will trigger the OnChanges mechanism in children components.
Read following article, don't miss mutable vs immutable objects.
Key issue is that you mutate array elements, while array reference stays the same. And Angular2 change detection checks only array reference to detect changes. After you understand concept of immutable objects you would understand why you have an issue and how to solve it.
I use redux store in one of my projects to avoid this kind of issues.
https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html
You can use IterableDiffers
It's used by *ngFor
constructor(private _differs: IterableDiffers) {}
ngOnChanges(changes: SimpleChanges): void {
if (!this._differ && value) {
this._differ = this._differs.find(value).create(this.ngForTrackBy);
}
}
ngDoCheck(): void {
if (this._differ) {
const changes = this._differ.diff(this.ngForOf);
if (changes) this._applyChanges(changes);
}
}
It's work for me:
#Component({
selector: 'my-component',
templateUrl: './my-component.component.html',
styleUrls: ['./my-component.component.scss']
})
export class MyComponent implements DoCheck {
#Input() changeArray: MyClassArray[]= [];
private differ: IterableDiffers;
constructor(private differs: IterableDiffers) {
this.differ = differs;
}
ngDoCheck() {
const changes = this.differ.find(this.insertedTasks);
if (changes) {
this.myMethodAfterChange();
}
}
This already appears answered. However for future problem seekers, I wanted to add something missed when I was researching and debugging a change detection problem I had. Now, my issue was a little isolated, and admittedly a stupid mistake on my end, but nonetheless relevant.
When you are updating the values in the Array or Object in reference, ensure that you are in the correct scope. I set myself into a trap by using setInterval(myService.function, 1000), where myService.function() would update the values of a public array, I used outside the service. This never actually updated the array, as the binding was off, and the correct usage should have been setInterval(myService.function.bind(this), 1000). I wasted my time trying change detection hacks, when it was a silly/simple blunder. Eliminate scope as a culprit before trying change detection solutions; it might save you some time.
Instead of triggering change detection via concat method, it might be more elegant to use ES6 destructuring operator:
this.yourArray[0].yourModifiedField = "whatever";
this.yourArray = [...this.yourArray];
You can use an impure pipe if you are directly using the array in your components template. (This example is for simple arrays that don't need deep checking)
#Pipe({
name: 'arrayChangeDetector',
pure: false
})
export class ArrayChangeDetectorPipe implements PipeTransform {
private differ: IterableDiffer<any>;
constructor(iDiff: IterableDiffers) {
this.differ = iDiff.find([]).create();
}
transform(value: any[]): any[] {
if (this.differ.diff(value)) {
return [...value];
}
return value;
}
}
<cmp [items]="arrayInput | arrayChangeDetector"></cmp>
For those time travelers among us still hitting array problems here is a reproduction of the issue along with several possible solutions.
https://stackblitz.com/edit/array-value-changes-not-detected-ang-8
Solutions include:
NgDoCheck
Using a Pipe
Using Immutable JS NPM github

Categories

Resources