Angular2 - Showing "Calculating,.." text till the Rendering of Observable Data is finished, - time

I've the following construct at the rendering side (html-template):
<span>
{{ (__reponseCalculation | async)?.grossPremiumPackage }}
</span>
Calculation takes up to 3 seconds. During this time, I want to show the text "Calculating,.."
For this purpose I have a flag "calculationFinished", which hast to be set to true, after the rendering is finished.
How can I do that best?
Additional Information:
__responseCalculation is coming via:
this.__reponseCalculation = this.__responseData
.flatMap((data: any) => data.calculation);
Please consider:
There is no subscribe function, the data is rendered over Elvis.Operator (?.) and has before an asyncron-pipe,..
The following approach is known to me and I'am not looking for it:
this.__reponseCalculation = this.__responseData
.flatMap((data: any) => data.calculation)
.subscribe(
(data:any) => this.__reponseCalculation = data,
(error:any) => console.debug ('ERROR Calculation: ', error),
() => {
this.__flags.calculationReady = true;
}
);
E.g. Within template I want to send a "signal" to the "controller" (component), as far as the data rendering process is finished. Is it possible or is that a deadlock/impasse/..

Related

Assert that a dynamic table is correctly ordered by date

Given a dynamically-loading table with a variable number of rows, how does one assert that the rows are correctly ordered by date?
This problem has two main challenges: (1) how does one compare dates within table rows using cypress; and (2) how does one handle dynamic loading in such a complex scenario?
So far, I have successfully managed to solve the first problem; however, I am unable to solve the second problem. My test works most of the time, but it will sometimes fail because the page hasn't finished loading before the assertions are hit. For example, the dates are out of order when the page is first loaded:
2023-12-23
2024-01-24
2022-02-25
2027-03-26
And then they get ordered following an XHR request:
2022-02-25
2023-12-23
2024-01-24
2027-03-26
Now, before you say anything: yes, I am already waiting for the XHR request to finish before I make any assertions. The problem is that there remains a small delay between when the request finishes, and when the actual DOM gets updated.
Normally this problem is solved automatically by Cypress. In Cypress, every call to .should() will automatically retry until the expected condition is found, or a timeout is reached. This fixes any problems related to dynamic loading.
However, .should() isn't the only way to assert something in Cypress. Alternatively, you can make direct assertions using Chai expressions, which is what Cypress uses under the hood when you call .should(). This is often required when making complex assertions such as the kind that I am making in this scenario.
Let's take a look at what I have so far:
cy.get('tbody tr').each(($row, $index, $rows) => { // foreach row in the table
if ($index > 0) { // (skipping the first row)
cy.wrap($row).within(() => { // within the current row...
cy.get('td').eq(7).then(($current_td) => { // ... get the eighth td (where the date is)
cy.wrap($rows[$index - 1]).within(() => { // within the previous row...
cy.get('td').eq(7).then(($previous_td) => { // ... get the eighth td
expect(dayjs($current_td.text().toString()).unix()) // assert that the date of the current td...
.gt(dayjs($previous_td.text().toString()).unix()) // ... is greater than the previous one.
})
})
})
})
}
})
Now, one option you have in Cypress is to replace .then() with .should(). Doing this allows the user to continue to benefit from the polling nature of .should() while also using multiple Chai expressions directly. Unfortunately, I can't seem to get this to work. Here's some of the attempts that I made:
cy.get('tbody tr').each(($row, $index, $rows) => {
if ($index > 0) {
cy.wrap($row).within(() => {
cy.get('td').eq(7).then(($current_td) => {
cy.wrap($rows[$index - 1]).within(() => {
cy.get('td').eq(7).should(($previous_td) => { // replacing with .should() here doesn't help, because it only attempts to retry on $previous_td, but we actually need to retry $current_td as well
expect(dayjs($current_td.text().toString()).unix())
.gt(dayjs($previous_td.text().toString()).unix())
})
})
})
})
}
})
cy.get('tbody tr').each(($row, $index, $rows) => {
if ($index > 0) {
cy.wrap($row).within(() => {
cy.get('td').eq(7).should(($current_td) => { // causes an infinite loop!
cy.wrap($rows[$index - 1]).within(() => {
cy.get('td').eq(7).then(($previous_td) => {
expect(dayjs($current_td.text().toString()).unix())
.gt(dayjs($previous_td.text().toString()).unix())
})
})
})
})
}
})
The only other solution I can think of is to hardcode my own polling. This is the sort of thing that I do all the time when writing tests in Selenium. However, my experience with Cypress leads me to believe that I shouldn't need to do this, ever. It's just a matter of wrangling Cypress to do what I expect it to do.
That said, I'm coming up empty handed. So, what do?
UPDATE
After learning from gleb's answer, I finally landed on this simple solution:
const dayjs = require('dayjs')
chai.use(require('chai-sorted'));
cy.get('tbody tr td:nth-of-type(8)').should($tds => {
const timestamps = Cypress._.map($tds, ($td) => dayjs($td.innerText).unix())
expect(timestamps).to.be.sorted()
})
I now feel that a core part of my problem was not understanding jQuery well enough to write a single selection statement. Furthermore, I wasn't familiar with lodash map or chai-sorted.
You need to use a single cy.get(...).should(...) callback where the callback grabs all date strings, converts into timestamps, then checks if the timestamps are sorted. Then Cypress retries the cy.get command - until the table is sorted and the should callback passes. Here is a sample code, see the full dynamic example at https://glebbahmutov.com/cypress-examples/recipes/sorted-list.html
// assuming you want to sort by the second column
cy.get('table tbody td + td').should($cells => {
const timestamps = Cypress._.map($cells, ($cell) => $cell.innerText)
.map((str) => new Date(str))
.map((d) => d.getTime())
// check if the timestamps are sorted
const sorted = Cypress._.sortBy(timestamps)
expect(timestamps, 'sorted timestamps').to.deep.equal(sorted)
})

angular subscribe to changes only if previous value of the form control !== currentvalue of the formcontrol

I would like to call onDataChange() only if previous value of input / selection != current Value of input / selection.
However I've noticed that it is still calling onDataChange() whenever put the cursor on the input field (but didn't change any value of the input field / change selection of the dropdown value).
I have used startWith(null), pairwise, and then filter(prev!=next) but it still executing the onDataChange() even though the previous value is same as next value.
The main problem is when putting the cursor on the input field but doesn't change any value, it still call onDataChange().
I would like to call onDataChange() only if there is changes in the input value/ changes in the dropdown selection. onDataChange() will invoke the api call, which might takes longer time to retrieve the result.
Does anybody experience similar issue and could provide guidance? Thanks.
'
<form class="form" [formGroup]="numberForm" noValidate [style.width.px]="width">
<select name="types" id="dropdown-number-types" formControlName="dropdown">
<option *ngFor="let type of types" [value]="type">{{type}}</option>
</select>
<input id="onValue" type="text" name="onValue" formControlName="onValue">
<select name="equalsUnit" id="dropdown-number-unit" formControlName="onValueUnit">
<option *ngFor="let unit of units" [value]="unit">{{unit}}</option>
</select>
</form>
ngOnInit() {
const valueChanges: Observable<any>[] = [
this.numberForm.get('dropdown').valueChanges,
this.numberForm.get('onValue').valueChanges,
this.numberForm.get('onValueUnit').valueChanges
].map(obs =>
obs.pipe(
startWith(null), pairwise(),
filter(([prev, next]) => prev !== next),
)
);
// Merge all valueChanges observables into one - and debounce time for 250ms to avoid processing
// events in quick succession which can happen if user is changing values quite fast in UI.
merge(...valueChanges)
.pipe(
takeUntil(this.ngUnsubscribe),
debounceTime(this.delay)
)
.subscribe(([prev, next]: [any, any]) => {
this.onDataChange();
});
this.updateValidators();
super.ngOnInit();
}
if I remove the merge and just watch for changes for formcontrolname of 'onValue', then it will execute onDataChange() only when there is difference in the value.
Is there any alternative that I can use for merging those 3 formcontrolname and watch for changes for any of the formcontrolname?
If understand correctly you want the distinctUntilChanged operator and this is how I would refactor your code:
const valueChanges: Observable<any>[] = [
this.numberForm.get('dropdown').valueChanges,
this.numberForm.get('onValue').valueChanges,
this.numberForm.get('onValueUnit').valueChanges
].map(obs => obs.pipe(startWith(null)));
combineLatest(valueChanges).pipe(
debounceTime(this.delay),
distinctUntilChanged((prev, curr) =>
prev[0] === curr[0] && prev[1] === curr[1] && prev[2] === curr[2]
),
takeUntil(this.ngUnsubscribe),
).subscribe(() => {
this.onDataChange();
});
Note:
I am using combineLatest, because merge will emit 3 seperate events and combineLatest will emit 1 combined event at the beginning. Afterwards combineLatest will emit on every change and will give you the updated combined value of 3 Observables, where merge would give you only one updated value not the combined.
use distinctUntilChanged to compare new combined value to the previous combined value. I specifically put it after the debounceTime, because you need to compare new debounced value to the old debounced value.

Chaining RXJs promises Observable.from

In RxJS 6, how can I chain and pass data with promises? I have to do a bunch of JIRA api calls in a row using the jira-connector npm library. But I'm not sure how to execute and pass the data to functions.
Thanks!
Ex:
const pipeData = Observable.from(jira.search.search({
jql: 'team = 41 and type = "Initiative"'
}))
pipeData.pipe(
data => operators.flatMap(data.issues),
issue => Observable.from(jira.search.search({
jql: `team = 41 and "Parent Link" = ${issue.key}`
}))).subscribe(results => {
console.log(results)
})
In the first place, you must use lettable operators in pipe function.
What appears you are trying to do is to:
Make one call and get array of issues;
Make separate call for every issue;
Get results
So something like:
pipeData.pipe(
// when pipeData emits, subscribe to the following and cancel the previous subscription if there was one:
switchMap(data => of(data.issues))),
// now you get array of issues, so concatAll and get a stream of it:
concatAll(),
// now to call another HTTP call for every emit, use concatMap:
concatMap(issue => jira.search.search({
jql: `team = 41 and "Parent Link" = ${issue.key}`
})).subscribe(results => {
console.log(results)
})
Note that I didn't wrapped jira.search.search with from as you can also pass promises into concatMap and you can also pass a second param - resultSelector function to select just some of the properties, if needed:
concatMap(issue => jira.search.search({
jql: `team = 41 and "Parent Link" = ${issue.key}`),
result => result.prop1
)

How to combine a parent and a dependent child observable

There is a continuous stream of event objects which doesn't complete. Each event has bands. By subscribing to events you get an event with several properties, among these a property "bands" which stores an array of bandIds. With these ids you can get each band. (The stream of bands is continuous as well.)
Problem: In the end you'd not only like to have bands, but a complete event object with bandIds and the complete band objects.
// This is what I could come up with myself, but it seems pretty ugly.
getEvents().pipe(
switchMap(event => {
const band$Array = event.bands.map(bandId => getBand(bandId));
return combineLatest(of(event), ...band$Array);
})),
map(combined => {
const newEvent = combined[0];
combined.forEach((x, i) => {
if (i === 0) return;
newEvent.bands = {...newEvent.bands, ...x};
})
})
)
Question: Please help me find a cleaner way to do this (and I'm not even sure if my attempt produces the intended result).
ACCEPTED ANSWER
getEvents().pipe(
switchMap(event => {
const band$Array = event.bands.map(bandId => getBand(bandId));
return combineLatest(band$Array).pipe(
map(bandArray => ({bandArray, event}))
);
})
)
ORIGINAL ANSWER
You may want to try something along these lines
getEvents().pipe(
switchMap(event => {
const band$Array = event.bands.map(bandId => getBand(bandId));
return forkJoin(band$Array).pipe(
map(bandArray => ({bandArray, event}))
);
})
)
The Observable returned by this transformation emits an object with 2 properties: bandArray holding the array of bands retrieved with the getBand service and event which is the object emitted by the Observable returned by getEvents.
Consider also that you are using switchMap, which means that as soon as the Observable returned by getEvents emits you are going to switch to the last emission and complete anything which may be on fly at the moment. In other words you can loose some events if the time required to exectue the forkJoin is longer than the time from one emission and the other of getEvents.
If you do not want to loose anything, than you better use mergeMap rather than switchMap.
UPDATED ANSWER - The Band Observable does not complete
In this case I understand that getBand(bandId) returns an Observable which emits first when the back end is queried the first time and then when the band data in the back end changes.
If this is true, then you can consider something like this
getEvents().pipe(
switchMap(event => {
return from(event.bands).pipe(
switchMap(bandId => getBand(bandId)).pipe(
map(bandData => ({event, bandData}))
)
);
})
)
This transformation produces an Observable which emits either any time a new event occurs or any time the data of a band changes.

RxJs Observable: Execute function if empty/filtered

I've got an Observable that listens to some user input from a text box. If the observed string's length is >=3 (filter), it executes some HTTP call (switchMap).
Now I'd like to detect somehow if the user input has been filtered. Reason:
If the HTTP call has been done, it should show the results.
If the user input got filtered (== is invalid), it should clear the results.
Here's the code I'd like to have (see: ifFiltered):
this.userInput.valueChanges
.filter(val => val && val.length >= 3)
.ifFiltered(() => this.results = [])
.switchMap(val => getDataViaHTTP())
.subscribe(val => this.results = val);
I know, I could place that logic within the filter function for this simple example. But what if I have 10 different filters?
Did I miss any method that satisfies my needs?
Thanks in advance!
Either use partition like here RxJS modeling if else control structures with Observables operators
Or instead of filter use map and pipe the object if the former filter condition is true or null otherwise. so you can catch the null where ever you want in your chain with a filter.
Last option call some function in the else part of the filter function
We've had a similar case and tried it with partition as mentioned above but found it much handier to use throw here. So for your code
this.userInput.valueChanges
.do(val => {
if (!val || val.length < 3) {
throw new ValueTooShortError();
}
})
.switchMap(val => getDataViaHTTP())
.do(val => this.results = val)
.catch(() => this.results = [])
.subscribe();
I suggest having a common event stream, creating two filtered streams, and merging the two before subscription:
var o = this.userInput.valueChanges;
var empty= o.filter(t=> t.length < 3)
.map(t=>[])
var nonempty = o.filter(t=> t.length >= 3)
.switchMap(t=> getDataViaHTTP());
empty.merge(nonempty).subscribe(val => this.results = val);
I found another nice solution for my use case using Validators:
(I know that this is no solution using Observables as the question stated. Instead it's using Angular2 features to workaround the problem nicely.)
this.userInput.validator = Validators.compose([
Validators.required,
Validators.minLength(3)
]);
this.userInput.valueChanges
.filter(val => this.userInput.valid)
.switchMap(val => getDataViaHTTP())
.subscribe(val => this.results = val);
Now I can use the userInput.valid property and/or the userInput.statusChanges Observable to keep track of the input value.
May be it's late, but wanted to post for the members still seeking a more cleaner approach to validate IF EMPTY inside .map:
of(fooBar).pipe(
map(
(val) =>
({
...val,
Foo: (val.Bar
? val.Foo.map((e) => ({
title: e.Title,
link: e.Link,
}))
: []) as fooModal[],
}));
This code returns a empty array if val.bar is missing, but it's just an example you can use any validation & expression instead.

Resources