Overriding previous observable - html

I have an events app which has an all-events component which lists all the created events. When the all-events component is loaded, the events are retrieved from my database.
This is the relevant code from the all-events Typescript file:
eventParams: EventParams;
getAllEventsObs: Observable<PaginatedResult<BeachCleanEvent[]>>;
ngOnInit() {
this.eventParams = new EventParams();
}
getEvents() {
this.getAllEventsObs = this.eventService.getAllEvents(this.eventParams);
console.log("getting events");
}
pageChanged(event: any) {
this.eventParams.pageNumber = event.page;
this.eventService.setEventParams(this.eventParams);
this.getEvents();
}
Here is the html from the all-events component:
<div class="background-img">
</div>
<div class="container">
<div class="events-container" *ngIf="getAllEventsObs | async as getAllEventsObs">
<div class="event-item" *ngFor="let event of getAllEventsObs.result">
<app-event-card [existingEvent]="event"></app-event-card>
</div>
<pagination *ngIf="getAllEventsObs.pagination as eventPagination"
[boundaryLinks]="true"
[totalItems]="eventPagination.totalItems"
[itemsPerPage]="eventPagination.itemsPerPage"
[(ngModel)]="eventPagination.currentPage"
(pageChanged)="pageChanged($event)">
</pagination>
</div>
</div>
The getAllEvents method is just a simple http request. I won't show this code as I know it works correctly.
This is the PaginatedResult class:
export interface Pagination {
currentPage: number;
itemsPerPage: number;
totalItems: number;
totalPages: number;
}
export class PaginatedResult<T> {
result: T;
pagination: Pagination;
}
This is the EventParams class:
export class EventParams {
pageNumber = 1;
pageSize = 4;
}
When the page initially loads, it loads correctly and displays the first four events:
The issue I am having is that when clicking on the next page, it gets stuck and won't load the next four events. No error is displayed on the console but the "getting events" console.log I created in the getEvents method above just keeps firing:
I suspect this is something to do with the way I am consuming the observable in the html code (using the async pipe). How would I go about resolving this? Is this where I should be using the switchMap RXJS operator? If so, how would I use it in this scenario?

You are on the right track... and yes, you should use switchMap :-)
Instead of re-assigning your source observable inside getEvents(), you could simply define it to depend on your params, and just push new params when they change. switchMap will execute the service method each time the params change.
But if the EventService is keeping track of the params anyway, it's probably the simplest to have it expose the events$ based on those params.
If you aren't already, define the params as a BehaviorSubject with default value and add a method for consumers to modify them. Then expose a single events$ observable that represents the events based on the specific params:
service:
private params$ = new BehaviorSubject(new EventParams());
public setEventParams(params) {
this.params$.next(params);
}
public events$ = this.params$.pipe(
switchMap(params => this.getAllEvents(params))
);
component:
events$ = this.eventService.events$;
pageChanged(params) {
// build proper params object if necessary
this.eventService.setEventParams(params);
}
Your component code becomes a lot simpler since it doesn't need to be concerned with managing the observable and keeping track of params.

Related

is using array.inclues in templare a performance issue in angular

my component have changeDetection:  ChangeDetectionStrategy.OnPush and component have a array variable
myids:Array<number>;
addId(id:number){
this.myids.push(id)
}
in the template I am using:
<div [class.active]="myids.includes(step.id)"></div>
My question, is it a performance problem to use myids.includes in template ?
Yes it is an performance issue. Everytime the template must be rerendered, this array iteration must be run, also. It's a good choice to run with changeDetectionStragy.OnPush, which minimizes the amount of template rerendering.
Good practice:
You should seperate html templates which are responsible for viewing and the code of your component which are responsible to react of events and bring the model to the view.
The core concept of that is Model-View-Controller. (MVC)
I recommend to calculate your "active" property in the controller or a service.
This property is calculated only one time and can easily bound to your template.
Do it like this:
public class MyComponent {
public isActive: boolean;
private myIds: string[];
constructor() {
this.isActive = false;
this.myIds = [];
}
public ngOnInit(): void {
// load myIds
// this.myIds = this._myIdsService.getIds();
this.isActive = this._myids.includes(step.id);
}
}
And then bind this property to your view...
<div [class.active]="isActive"></div>

Angular 9 - Cannot find a differ supporting object 'getData()

I am getting this error trying to bind my control to its data. Here is some relevant code.
Template.
<tree-control [nodes]="getData"></tree-control>
Component.
public getData(): Observable<Array<any>> {
const assets: any = this.service.get('url', headers);
return assets;
}
Anything I have found so far is not helping. Any idea what's wrong with my code?
Thanks
First of all, you assign a function (getData) to the nodes property. I assume you want to assign the data from getData to it instead.
Secondly, the call to this.service.get is probably not being executed. Reason for that is that you do not subscribe to, what I assume, is a http-call that returns an Observable.
To fix this, you can do the following:
export class Foo {
nodeData: Observable<any>;
constructor(
private readonly service: YourService,
) {
this.nodeData = this._getData();
}
private _getData() {
return this.service.get(...);
}
}
Inside your template you can then subscribe and unsubscribe to the data automatically by using the async pipe.
<tree-control [nodes]="nodeData | async"></tree-control>
For all that to work I assume your service.get method returns an Observable.

Angular Template/Interpolation parse error on find/filter

Error:
Error: Template parse errors: Parser Error: Bindings cannot contain
assignments at....
line: <div>Closed: {{blah}}.find()...
HTML:
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="4px">
<div>Total: {{(issuesData$ | async)?.length}}</div>
<div>Closed: {{(issuesData$ | async)?.filter(data => {data.closedOn}).length}}</div>
</div>
I'm curious if it is possible to use find/filter without running into the template parse error when find/filter are called on a collection in the interpolation statement.
EDIT: Angular 2 - Bindings cannot contain assignments - This does not work for me because I'm passing in an Observable to the component. OnInit I assign the #Input data variable to the issuesData$ observable. Using something like {{getCount()}} in the interpolation results in no data. I tried implementing that like this:
ANGULAR:
#Input()
data;
ngOnInit() {
this.issuesData$ = this.data;
}
getCount(){
this.issuesData$.subscribe(data => {
return data.length;
})
}
HTML:
<div>Total: {{getCount()}}</div>
But there is nothing to subscribe to when the getCount() is called in the interpolation and this doesn't work either {{(getCount() | async}}
In this scenario you should use map and format the response in a way that makes the data easily consumable by the component/template.
Also you can't return a value from subscribe, you can assign a value though.
find returns one match, filter returns an array. You meant to use filter if you are wanting to see the total number of matches using length
If you use shareReplay the observable will be resolved once so it can be called multiple times using either async pipes or subscribe without incurring additional costs if it does something with external resources like making an http request.
Finally you should define types / definitions, typescript is (can be) strongly typed and this is a big advantage over javascript (what it supersedes).
Template.html
<div>Total: {{(issuesData$ | async)?.length}}</div>
<div>Closed: {{(issuesData$ | async)?.closedOnLength}}</div>
YourComponent.ts
import { Observable } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
#Input()
data: Observable<{closedOn:any}[]>;
issuesData$: Observable<{length: number, closedOnLength: number}>;
ngOnInit() {
this.issuesData$ = this.data.pipe(map((_) => {
return {
length: _.length,
closedOnLength: _.filter(d => d.closedOn).length
};
}), shareReplay());
}
count: number;
readCount() {
// you can't return data in a subscribe callback, it is asynchronous
// you can assign a value though
this.issuesData$.subscribe(_ => this.count = _.length);
}
You should subscribe to your Observable and manually assign the variables you want in there instead, purely to reduce the complexity of your template code.
#Input()
data;
total: number;
closed: number;
ngOnInit() {
this.issuesData$ = this.data;
this.issuesData$.subscribe(next => {
this.total = next.length;
this.closed = next.filter(x => x.closedOn).length;
}
}
Then just use the total and closed variables in your template.
To call find or filter from the async array in the template, define the callback in the component class. For example, you can set the method isClosedOn as the filter callback:
<div>Closed: {{ (issuesData$ | async)?.filter(isClosedOn).length }}</div>
and define it as follows in the component class:
public isClosedOn = data => data.closedOn;
See this stackblitz for a demo.

How to target an element in the template loaded based on an *ngIf condition

I'm trying to target an element in my template through a template variable but it keeps returning undefined.
When you navigate to a certain route in my application, my component fetches some data inside the forkJoin method from rxjs. It's quite a bit of data and I'm on a slow connection so there is a 2 second delay, hence, I have an intermediate state, where I display a loading spinner, which then disappears when all the data comes back.
My template code looks something like...
<div class="container" *ngIf="ready; else loading">
<mat-horizontal-stepper labelPosition="bottom" #stepper>
<mat-step label="Step 1"> Step 1 Content... </mat-step>
<mat-step label="Step 2"> Step 2 Content... </mat-step>
<mat-step label="Step 3"> Step 3 Content... </mat-step>
</mat-horizontal-stepper>
</div>
<ng-template #loading>
<mat-spinner></mat-spinner>
</ng-template>
My component.ts file looks like this...
public ready = false;
public dataset1;
public dataset2;
#ViewChild('stepper') private myStepper: MatStepper;
constructor(private dataService: DataService) {}
ngOnInit() {
this.fetch();
console.log(this.myStepper); // returns undefined
}
ngAfterViewInit() {
console.log(this.myStepper); // also returns undefined
}
public fetch() {
return forkJoin(
this.dataService.getDataSet1(),
this.dataService.getDataset2()
).subscribe(res => {
this.dataset1 = res[0];
this.dataset2 = res[1];
this.ready = true;
}, error => console.log('Error : ', error));
}
I'd like to be able to target the stepper and get the total count of the steps and use those to navigate to a different step based on some user activity but I can't do that if I keep getting undefined.
How might I fix this?
Because you have it inside a *ngIf block, you will not able to access it until whatever he *ngIf is conditional to is true as this directive does not load the html elements until it is true, including your stepper. This means you could check for the value where I have added it in the snippet below. As at this point you set your ready variable to true meaning that section of the html will also be available. This might be a good point to call a function to run your logic.
As mentioned in the comment it would be ideal to call the change detection before trying to access. This can be done by injecting ChangeDetectorRef through the constructor.
import { ChangeDetectorRef } from '#angular/core';
Make sure your imports above include, ChangeDetectorRef.
constructor(private ref: ChangeDetectorRef,
private dataService: DataService) {}
public fetch() {
return forkJoin(
this.dataService.getDataSet1(),
this.dataService.getDataset2()
).subscribe(res => {
this.dataset1 = res[0];
this.dataset2 = res[1];
this.ready = true;
this.ref.detectChanges();
console.log(this.stepper); // you will be able to read it here.
}, error => console.log('Error : ', error));
This article helps explain the dom behaviour with *ngIf.
Something else to take into account would be using a setter for the ViewChild, this means the value would only be set when the *ngIf is true. Meaning it would be available as soon as it has been rendered in the dom. This is a good way to get the reference and would look like. This will also be initially set with undefined until change detection has run or been manually run.
public myStepper: MatStepper;
#ViewChild('stepper') set content(content: MatStepper) {
this.myStepper = content;
}
That way you can reference your ViewChild and interact with it through this.myStepper.

angular 2 RC4 create component

So here is the problem, I am attempting to create a new component from within a service that is injected within the App Component. I need the new component to be placed within the app component html tag not outside. The thing is I really do not want the app component to have to provide anything to the service I may need to inject the service into other places and hence not have it tightly coupled to the app component. So far I have created a DIV at the end of the app component html and then used #ViewChild to read the ViewContainerRef from this element located within the app component. This is then provided to the service via a function call so that it can make use of createComponent. This allows for the NEW component to be placed within the scope of the app component, not within the body. Unfortunately this is too dependant on the app component providing the ViewContainerRef. Any ideas of how I can create the new component as described.
Code Example
app.component.html
<app-component>
<div #newCompHook></div>
</app-component>
app.component.ts
#ViewChild('newCompHook', {read: ViewContainerRef}) newCompViewRef: ViewContainerRef;
constructor(appService: AppService) {
appService.setViewRef(this.newCompViewRef);
}
app.service.ts
private myViewRef;
constructor(private compiler: ComponentResolver){
this.myViewRef = null;
}
public setViewRef(vr: ViewContainerRef): void {
this.myViewRef = vr; // <-- DO NOT WANT TO DO THIS !!!
}
public createNewComp(childCmp: Type): void {
if (this.myViewRef !== null){
this.compiler.resolveComponent( childCmp ).then((compFactory:ComponentFactory) => this.myViewRef.createComponent(compFactory) )
}
}
createNewComp is called by an external source and may or may not provide the childCmp type to be resolved.
So any ideas of how I can do this without needing to provide anything from the app component ???
If you need to have the viewContainerRef in your service that is the only solution...
But it is not a good practice to generate HCI components in a service. It's the role of other components.
Let's take an exemple : your server send you a list of objects (a list of strings for exemple) and you want to generate a button for each string.
In your service you just manage the string list :
#Injectable()
export class MyService {
private myModel : Array<String> = new Array();
public get MyModel () : Array<String> {
return this.myModel;
}
/// Assume you have method in the service to populate the model...
}
Then it's your component which generate the HCI :
export class AppComponent {
/// Use dependency injection to get service :
constructor (private _myService : MyService){}
public get MyModel () : Array<String> {
return this.myService.MyModel;
}
}
Finally in your component template :
<div>
<ul>
<li *ngFor="let s of MyModel">
<!-- Button with your model text -->
<button>s</button>
</li>
</ul>
</div>
That is a better solution than generate the components in the service because just imagine you don't want buttons list but a togglebuttons list in your HCI, here you just have to change the HTML. The service is still the same, and the components typescipt part is still the same too !