Angular infinite scrolling strategy with IntersectionObserver - html

I've been reading a lot about infinite scrolling strategies as I need to implement this functionality in my component. However, there seems to exist a lot of different approaches and the examples I've seen are not the most straight forward and easy to understand.
To put it briefly, this component consists of 3 mat-tabs (each represent a different order status), and inside each one of these there is a list of order components for that corresponding order status.
The smart parent component looks something like:
<mat-tab-group dynamicHeight animationDuration="400ms">
<mat-tab>
<app-orders-list
[orders]="loadedPendingOrders"
[timelines]="pendingOrderTimelines"
[isDeliverySlotsActive]="isDeliverySlotsActive"
[searchTerm]="searchTerm"
></app-orders-list>
</mat-tab>
...
/* more tabs */
</mat-tab-group>
And the child app-orders-list looks like this (each section contains orders):
<div class="list-container" [ngClass]="{section: isDeliverySlotsActive === false}">
<div [ngClass]="{section: isDeliverySlotsActive === true}" *ngFor="let date of timelines">
<app-orders-list-section
[orders]="orders"
[timeline]="date"
[isDeliverySlotsActive]="isDeliverySlotsActive"
[searchTerm]="searchTerm"
></app-orders-list-section>
</div>
</div>
When I reach the bottom of this list on a scroll down event, I need to load more orders from the API and present them accordingly.
Given this architecture, what strategy do you recommend for infinite scrolling? I'm mainly looking for something like a directive that implements the IntersectionObserver API and triggers an event when the bottom of the list is reached.
I'm not interested in solutions that use ngx-infinite-scrolling.
Thank you in advance.

Related

Correct way to scroll to dynamic angular page

I am searching for the correct way to correct to scroll in a page that will load dynamic information. These informations are asynchronous so to avoid my user seeing the whole page constructing itself I have a boolean flag like so :
<div *ngIf="loaded"> ... </div>
My problem is that I want to scroll to an anchor using the angular router, but that anchor doesn't exist yet at this moment because the load isn't finished.
The anchor :
<hr id="my_anchor">
the code I use to load the page and get to that anchor :
<a [routerLink]="['/some/route/', idParameter]" fragment="my_anchor">...</a>
You probably want to use virtual scrolling, available through the Angular CDK:
https://material.angular.io/cdk/scrolling/examples
https://medium.com/codetobe/learn-how-to-us-virtual-scrolling-in-angular-7-51158dcacbd4
This approach dynamically loads only those components that are on screen, allowing you to load extremely large datasets.
It is also possible to code this manually using:
const el: any = document.elementFromPoint(x, y);
This Javascript function determines which HTMLElement is located at a specific x, y, coordinate and thereby determining which elements are on screen. Using this information you can wrap all items in a list like so:
<ng-container *ngFor="let data of datas">
<ng-container *ngIf="data.isOnScreen">
<app-my-component></app-my-component>
</ng-container>
<ng-container *ngIf="!data.isOnScreen">
<div class="empty-div"> </div>
</ng-container>
</ng-container>
and style the empty-div to be the same size as a non empty div. This ensures that scrolling works. I got this working well. However, no doubt the CDK makes this a whole world smoother and easier.
The only benefit with my custom approach is it gives you total control. You can easily use:
list.scrollTop = 999;
to scroll to any position in the list, thus supporting anchors (where list is the HTML list element that will scroll). Not for the feint hearted though, would only recommend this for confident coders.

Angular sub-navigation based on route

I'm building an Angular application with two levels of navigation in the header.
The top-level navigation are visible on all routes in the application, however the second-level navigation is context-specific and only exists on certain routes.
For example, when viewing a specific course in a university webapp, the second-level navigation might have links to description, prerequisites, the different subjects, etc.
One way of doing it is creating a subnav component that has a switch statement and renders the correct subnav component based on the route:
<div [ngSwitch]="currentRoute">
<course-sub-nav *ngSwitchCase="course"></course-sub-nav>
<students-sub-nav *ngSwitchCase="students">...</students-sub-nav>
<support-sub-nav *ngSwitchCase="support">...</support-sub-nave>
</div>
I don't like that solution because now the sub-nav component knows too much about all the different context-specific sub-navs.
Another approach is to use a service to manage it. i.e. having a SubNavService.setNavigationLinks(links: NavLink[]) that takes an array of links. The sub-nav component can then listen to changes to the SubNavService and dynamically render a list of sublinks.
This solution is a bit cleaner however it does mean that I need to call that service on ever top-level component to ensure that the sub-nav is being updated. I'd also need to clear the links on pages that don't require sub-navs.
A third solution might be doing something like:
<!-- In header.component.html -->
<div class="sub-nav">
<ng-content="subnav"></ng-content>
</div>
<!-- IN course-page.component.html -->
<div>
<subnav>
<!-- my nav links -->
</subnav>
<div class="course-content">
<router-outlet></router-outlet>
</div>
</div>
I like this approach the most given that if a subnav component exists, the subnav will be rendered, and if not, nothing will show. Additionally, the relevant component has full control over rendering the links within the subnav. however given that course-page isn't actually a child of the header, I'm not sure how I can make it work.
I was wondering if there's any way to get the last approach to work. If not, would the second approach be the most pragmatic solution to this problem or is there a cleaner solution that exists?
The cleanest solution would be the second you mentioned.
Having a service that would handle the menu - menu component would subscribe and listen for changes, meanwhile other components would handle what items they need to have in the menu.

Angular4 ng-content gets built when ngIf is false

I have a problem with the new ng-content transclusion.
Let's say I have a component my-component that, in its ngOnInit() function does some heavy operation on load (for now, just a console.log()).
I have a wrapper, that displays the content via transclusion (my-wrapper.component.html).
<ng-content></ng-content>
If I set the surroundings up like this, the log statement doesn't show:
<my-wrapper *ngIf="false">
<my-component></my-component>
</my-wrapper>
I assume, the my-wrapper component does not get built, so the content is ignored.
But if I try to move the logic into the my-wrapper component like this (my-wrapper.component.html):
<ng-container *ngIf="false">
<ng-content></ng-content>
</ng-container>
I always see the console.log() output. I guess, the my-component gets built and then stored away until the *ngIf becomes true inside my-wrapper.
The intention was to build a generic "list-item + detail" component. Say I have a list of N overview-elements (my-wrapper), that get rendered in a *ngFor loop. Every of those elements has its own detail component (my-component) that is supposed to load its own data, once I decide to show more infos to a specific item.
overview.html:
<ng-container *ngFor="let item of items">
<my-wrapper>
<my-component id="item.id"></my-component>
</my-wrapper>
</ng-container>
my-wrapper.component.html:
<div (click)="toggleDetail()">Click for more</div>
<div *ngIf="showDetail">
<ng-content></ng-content>
</div>
Is there a way to tell Angular, to ignore the transcluded content until it is necessary to be added to the page? Analogously to how it was in AngularJS.
Based on the comment of #nsinreal I found an answer. I find it to be a bit abstruse, so I'm trying to post it here:
The answer is to work with ng-template and *ngTemplateOutlet.
In the my-wrapper component, set up the template like this (my-wrapper.component.html):
<div (click)="toggleDetail()">Click for more</div>
<div *ngIf="showDetail" [hidden]="!isInitialized">
<ng-container *ngTemplateOutlet="detailRef"></ng-container>
</div>
Note, that the [hidden] there is not really necessary, it hides the "raw" template of the child until it decides it is done loading. Just make sure, not to put it in a *ngIf, otherwise the *ngTemplateOutlet will never get triggered, leading to nothing happening at all.
To set the detailRef, put this in the component code (my-wrapper.component.ts):
import { ContentChild, TemplateRef } from '#angular/core';
#Component({ ... })
export class MyWrapperComponent {
#ContentChild(TemplateRef) detailRef;
...
}
Now, you can use the wrapper like this:
<my-wrapper>
<ng-template>
<my-component></my-component>
</ng-template>
</my-wrapper>
I am not sure, why it needs such complicated "workarounds", when it used to be so easy to do this in AngularJS.
By doing this:
<my-wrapper *ngIf="false">
<my-component></my-component>
</my-wrapper>
You are not calling MyComponent component, because the *ngIf is false. that means, that not calling it you are not instancing it and, therefore, not passing through its ngOnInit. And that's why you are not getting the console log.
By doing this:
<ng-container *ngIf="false">
<ng-content></ng-content>
</ng-container>
You are inside the component, you are just limiting what to render in your template, but you already instanced your component and, therefore, you passed through your ngOnInit and you get your console log done.
If, you want to limit something (component call with selector or a ng-content or even a div) until you have some data available, you can do the following:
datasLoaded: Promise<boolean>;
this.getData().subscribe(
(data) => {
this.datasLoaded = Promise.resolve(true); // Setting the Promise as resolved after I have the needed data
}
);
And in your template:
<ng-container *ngIf="datasLoaded | async">
// stuff here
</ng-container>
Or:
<my-component *ngIf="datasLoaded | async">
// Didn't test this one, but should follow the same logic. If it doesn't, wrap it and add the ngIf to the wrapper
</my-component>
It’s because Ng content happens at the build time and when you pass the content it is actually not removed or recreated with the ngIf directive. It is only moved and the component is instantiated .
I encountered this problem recently as well but settled on a different solution than the currently accepted one.
Solution (TL;DR)
(Solution is for AngularDart; I figure it's similar in Angular though)
Use a structural directive; tutorials linked below.
Instead of:
<my-wrapper>
<my-contents></my-contents>
</my-wrapper>
your usage becomes:
<div *myWrapper>
<my-contents></my-contents>
</div>
which is shorthand for the following (in AngularDart; I think Angular uses <ng-template>)
<template myWrapper>
<div>
<my-contents></my-contents>
</div>
</template>
The MyWrapper directive logic is similar to NgIf except it has its own logic to compute the condition. Both of the following tutorials explain how to create an NgIf-like directive and how to pass it your own inputs using the special microsyntax (e.g. *myWrapper="myInput: expression"). Note that the microsyntax doesn't support outputs (#Output), but you can mimic an output by using an input that is a function.
Tutorial for Angular
Tutorial for AngularDart
Caveat: Since this is just a directive, it shouldn't do anything more complicated than instantiating a template ref at the appropriate time and maybe specifying some DI providers. For example, I would avoid trying to apply styles or instantiating a complex tree of components in the directive. If I wanted to create a list component, I would probably take the #ContentChild(TemplateRef) approach described in another answer; you would lose the asterisk shorthand for creating <template> but you would gain the full power of components.
My problem
My team owns an app that's part of a larger web application with other apps owned by other teams. Our components assume they can inject a MyAppConfiguration object, but this object can only be injected after it is loaded with an asynchronous request. In our app this is not a problem: we have a "shell" component that hides everything behind an ngIf until the configuration is loaded.
The problem is when other teams want to reference our components. We don't want them to duplicate the "wait until configuration is loaded" logic every time, so I tried creating a wrapper component that can be used like so:
<my-app-wrapper>
<my-app-component></my-app-component>
</my-app-wrapper>
The wrapper injects a service object and hides its contents behind an ngIf until the service says that the configuration is loaded.
Like the question poster, I discovered that the ng-content approach doesn't work as intended: while the contents are correctly hidden from the DOM, Angular still instantiates the components causing dependency injection to fail.
The solution that I settled on was to rewrite the wrapper component as a structural directive.

Ng-Repeat nested in ng-virtual-repeat isn't working

I'm working on a project for work where we display a dynamic table to a user. This table shows them a list of companies they are associated with, and the columns of that table can be specified by the user, where the columns correspond to properties of the company objects being displayed. Here is a snipped of my code:
<md-virtual-repeat-container id="vertical-container" style="height: 500px;">
<div md-virtual-repeat="company in companies" flex>
<div ng-repeat="filter in filters">{{::company[filter]}}</div>
</div>
</md-virtual-repeat-container>
Where filters is a list of columns the user wants to see. However, Angular is simply displaying nothing. I know that the data is there and I can display the data just fine if I don't use the virtual repeat, however there can potentially be 10k+ companies in the list, and eventually they will need to be data-bound, so the virtual repeat is almost necessary.
What am I doing wrong here? Is there a better way to implement a table in the manner I'm describing?
Also under consideration would be refactoring using React instead, would React be better equipped to build this kind of table?
Looks like it was the interaction between the repeat-container and the div that had the repeat directive on it. Replacing the div with an md-list-item resolved the issue.
<md-virtual-repeat-container id="vertical-container" style="height: 500px;">
<md-list-item md-virtual-repeat="company in companies" flex>
<div ng-repeat="filter in filters">{{::company[filter]}}</div>
</md-list-item>
</md-virtual-repeat-container>

Generate different pages on the basis of different contents

Let me assume that I have the following architecture
_components (folders)
x1.html
x2.html
x3.html
I have a first page where I got the information from the YAM section for every component.
At this point I would like to add a link for every component to another page where I will display the component in a bigger manner.
So, let me assume I have, in the first page :
<div class="col-md-1">
<span id= "logos" class="material-icons"></span>
</div>
and In the componentbig.html I would like to open the right component on the basis of the link.
Do you have any suggestions for me ?
If I understand You correctly, then collections might be the feature You are looking for. For example, I'm usually using collections to generate a list of products and then have a specific page for each product also. Also make sure You check the easy tutorial by Ben Balter.