Please don't mark it as duplicate
I'm new in angular material design and I have a problem with mat-autocomplete. I have multiple Mat-Autocomplete in FormArray of FromGroup. On keyup in the input field, it's getting data from API calls and filled the autocomplete. After getting data on Keyup it will open the panel.
when I press word then a list of autocomplete opens then I want this list as infinite-scroll
I have multiple autocomplete in formArray of formGroup.
I don't want to use third-party dependency in the project like ngx-infinite-scroll.
Working Demo in this Stackblitz Link
When you want to detect autocomplete scroll end position you can use custom directive. In this directive you can calculate position of panel control and detect scroll end position, and once scroll end detected you can emit event to component. Directive Name is mat-autocomplete[optionsScroll] so that it auto detects mat-autocomplete component with optionScroll event and this custom directive is applied to all of this matching component. Directive is as follow..
export interface IAutoCompleteScrollEvent {
autoComplete: MatAutocomplete;
scrollEvent: Event;
}
#Directive({
selector: 'mat-autocomplete[optionsScroll]',
exportAs: 'mat-autocomplete[optionsScroll]'
})
export class MatAutocompleteOptionsScrollDirective {
#Input() thresholdPercent = 0.8;
#Output('optionsScroll') scroll = new EventEmitter<IAutoCompleteScrollEvent>();
_onDestroy = new Subject();
constructor(public autoComplete: MatAutocomplete) {
this.autoComplete.opened
.pipe(
tap(() => {
// Note: When autocomplete raises opened, panel is not yet created (by Overlay)
// Note: The panel will be available on next tick
// Note: The panel wil NOT open if there are no options to display
setTimeout(() => {
// Note: remove listner just for safety, in case the close event is skipped.
this.removeScrollEventListener();
this.autoComplete.panel.nativeElement.addEventListener(
'scroll',
this.onScroll.bind(this)
);
}, 5000);
}),
takeUntil(this._onDestroy)
)
.subscribe();
this.autoComplete.closed
.pipe(
tap(() => this.removeScrollEventListener()),
takeUntil(this._onDestroy)
)
.subscribe();
}
private removeScrollEventListener() {
if (this.autoComplete?.panel) {
this.autoComplete.panel.nativeElement.removeEventListener(
'scroll',
this.onScroll
);
}
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
this.removeScrollEventListener();
}
onScroll(event: Event) {
if (this.thresholdPercent === undefined) {
console.log('undefined');
this.scroll.next({ autoComplete: this.autoComplete, scrollEvent: event });
} else {
const scrollTop = (event.target as HTMLElement).scrollTop;
const scrollHeight = (event.target as HTMLElement).scrollHeight;
const elementHeight = (event.target as HTMLElement).clientHeight;
const atBottom = scrollHeight === scrollTop + elementHeight;
if (atBottom) {
this.scroll.next();
}
}
}
}
Now, you have to call scroll event to mat-autocomplete. On every scroll end onScroll() event is called by our directive.
<mat-autocomplete (optionsScroll)="onScroll()" > ... </mat-autocomplete>
Now, You have to load first and next chunk of data to mat-autocomplete like this..
weightData$ = this.startSearch$.pipe(
startWith(''),
debounceTime(200),
switchMap(filter => {
//Note: Reset the page with every new seach text
let currentPage = 1;
return this.next$.pipe(
startWith(currentPage),
//Note: Until the backend responds, ignore NextPage requests.
exhaustMap(_ => this.getProducts(String(filter), currentPage)),
tap(() => currentPage++),
//Note: This is a custom operator because we also need the last emitted value.
//Note: Stop if there are no more pages, or no results at all for the current search text.
takeWhileInclusive((p: any) => p.length > 0),
scan((allProducts: any, newProducts: any) => allProducts.concat(newProducts), [] ) );
})
);
private getProducts(startsWith: string, page: number): Observable<any[]> {
const take = 6;
const skip = page > 0 ? (page - 1) * take : 0;
const filtered = this.weightData.filter(option => String(option).toLowerCase().startsWith(startsWith.toLowerCase()));
return of(filtered.slice(skip, skip + take));
}
onScroll() {
this.next$.next();
}
So at first time we load only first chunk of data, when we reached end of scroll then we again emit event using next$ subject and our stream weightData$ is rerun and gives us appropiate output.
Related
I have an issue with a directive that I created in Angular.
In this directive, I want to execute some code after the window.onload event to be sure that all the page resources have been loaded (because I want to know the page top offset of some elements in the directive and if the images are not loaded, this offset is not correct)
If I refresh the page, it works because of the window.onload event is fired but when using the angular navigation to reach my component, this event is not fired anymore.
I tried to use the lifecycle of angular like AfterViewInit but most of the time the AfterViewInit method is executed before the images are loaded.
UPDATE
This is the code of my directive :
export class ParallaxDirective implements OnInit, OnDestroy {
#Input() coef = 1;
start = 0;
path = 0;
initialTranslate = 0;
intersectObserver: IntersectionObserver;
inView: boolean;
constructor(
private element: ElementRef
) {
}
ngOnInit(): void {
window.addEventListener('load', () => this.initParallax());
}
ngOnDestroy(): void {
this.intersectObserver.unobserve(this.element.nativeElement);
window.removeEventListener('scroll', () =>this.setTransform());
}
initParallax(): void {
this.intersectObserver = new IntersectionObserver(this.intersect.bind(this));
this.intersectObserver.observe(this.element.nativeElement);
const initialY = new DOMMatrixReadOnly(window.getComputedStyle(this.element.nativeElement).getPropertyValue('transform')).m42;
this.start = this.element.nativeElement.offsetTop + initialY;
this.path = this.element.nativeElement.clientHeight;
this.initialTranslate = (initialY /
this.element.nativeElement.clientHeight) * 100;
if (window.pageYOffset > 0) {
this.setTransform(true);
}
window.addEventListener('scroll', () => this.setTransform());
}
setTransform(force = false): void {
if (!this.inView && !force) {
return;
}
const offset = window.pageYOffset + window.innerHeight - this.start;
const t = (offset * 10) / this.path;
const i = t * this.coef + this.initialTranslate;
this.element.nativeElement.style.setProperty('transform', `translate3d(0, ${i}%, 0)`);
}
intersect(entries: IntersectionObserverEntry[], observer: IntersectionObserver): void {
entries.forEach(entry => {
this.inView = entry.isIntersecting || entry.intersectionRatio > 0;
});
}
}
As you can see, I need to retrieve some offsets and dom elements height. The problem is that these offsets and heights are not the same after resources in the page (like images) are completely loaded that's why I need to have an event like window.onload to be sure that everything is loaded. And I didn't find any Angular lifecycle that should be triggered after resources load.
I hope someone can help me with this.
Thanks.
After a long long exchange of comments, I finally understood your problem, Romain, and here's what I came up with.
My solution involves adding a directive and a service. The directive will be attached to all <img> tags, and will subscribe to their load events, and the service will coordinate all the load events firing and maintain a running list of images that are still being loaded.
Here's the directive:
#Directive({
selector: 'img'
})
export class MyImgDirective {
constructor(private el: ElementRef, private imageService: ImageService) {
imageService.imageLoading(el.nativeElement);
}
#HostListener('load')
onLoad() {
this.imageService.imageLoadedOrError(this.el.nativeElement);
}
#HostListener('error')
onError() {
this.imageService.imageLoadedOrError(this.el.nativeElement);
}
}
Here's the service:
#Injectable({
providedIn: 'root'
})
export class ImageService {
private _imagesLoading = new Subject<number>();
private images: Map<HTMLElement, boolean> = new Map();
private imagesLoading = 0;
imagesLoading$ = this._imagesLoading.asObservable();
imageLoading(img: HTMLElement) {
if (!this.images.has(img) || this.images.get(img)) {
this.images.set(img, false);
this.imagesLoading++;
console.log('images loading', this.imagesLoading);
this._imagesLoading.next(this.imagesLoading);
}
}
imageLoadedOrError(img: HTMLElement) {
if (this.images.has(img) && !this.images.get(img)) {
this.images.set(img, true);
this.imagesLoading--;
console.log('images loading', this.imagesLoading);
this._imagesLoading.next(this.imagesLoading);
}
}
}
And here's how I would use it in your parallax directive:
constructor(private imageService: ImageService) {}
ngOnInit() {
this.sub = imageService.imagesLoading$.pipe(filter(r => r === 0)).subscribe(_ => {
this.initParallax()
});
}
ngOnDestroy() {
this.sub.unsubscribe();
}
How does this work? The image directive gets attached to all images regardless of how deep they are in your component tree, registers the image with the service, and then tracks the loading progress by listening to the load event. Every time a new directive is created it means a new image is created, so a counter is incremented, and every time load fires, it means an image has finished loading, so the counter is decremented. We emit this counter in an observable, and so we can detect when all images are ready by waiting for the observable to emit the value 0.
Stackblitz demo
EDIT Added error handling in case an image points to a broken link
You can use HostListener
import { HostListener, Component } from "#angular/core";
#Component({
selector: 'app',
template: `<h1>{{message}}</h1>`
})
class AppComponent {
message = "Window not loaded";
#HostListener('load', ['$event'])
windowLoadedEvent(event) {
this.message = "Window loaded";
}
}
Note: It will work only if the component is initialized before window is loaded. Otherwise it will not work
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
I have applied this sort of line in order to make the component UI updated everytime. But in some other cases it start to refreshing the page event if it should reuse the route.
How can we overcome this issue?
Actually in my application there are three tabs in left panel. In each tab there are some listings clicking on list items opens the content on right panel. But in one of the listing there is a common UI that is getting open on some list item, but the problem is that when we don't apply above sort of code then the UI is not getting updated. But if we apply the code then the UI is updated everytime we click on other list item. But the problem is that when we apply this code it start to refresh the page everytime we click on other list in different tabs also, that should not be the case.
If we apply this code this.router.routeReuseStrategy.shouldReuseRoute = () => false; then how can we revert this functionality under this.router?
To take less risks I'm just reverting it back to what it was once the reload is done:
refresh() {
const prev = this.router.routeReuseStrategy.shouldReuseRoute;
const prevOSN = this.router.onSameUrlNavigation;
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.router.onSameUrlNavigation = 'reload';
this.router.navigate([this.router.url]);
setTimeout(() => {
this.router.routeReuseStrategy.shouldReuseRoute = prev;
this.router.onSameUrlNavigation = prevOSN;
}, 0);
}
I have the same issue, I changed that line for this:
// override the route reuse strategy
this.router.routeReuseStrategy.shouldReuseRoute = function () {
return false;
};
this.router.events.subscribe((evt) => {
if (evt instanceof NavigationEnd) {
// trick the Router into believing it's last link wasn't previously loaded
this.router.navigated = false;
// if you need to scroll back to top, here is the right place
window.scrollTo(0, 0);
}
});
I don't even know if this works well or do the same thing.
private saveRouterStrategyReuseLogic: any;
ngOnInit() {
// Save logic
this.saveRouterStrategyReuseLogic = this.router.routeReuseStrategy.shouldReuseRoute;
this.router.routeReuseStrategy.shouldReuseRoute = (future, curr) => { return false; };
}
ngOnDestroy() {
this.router.routeReuseStrategy.shouldReuseRoute =
this.saveRouterStrategyReuseLogic;
}
Click event is working fine when using mouse with computer. Even when I put mouse button down on button move cursor and then release mouse button inside button area, click event is firing. But same with touchscreen it is not working. I know that reason is that in touchscreen that kind of dragging is considered as scrolling. Click event is fired when I don't move finger too much on button. So only down and up without moving. My client has problem that they are moving finger too much and it is too hard to get click event. Is it possible to set bigger threshold for how much finger can move that it is still considered as click and not scroll?
I found this article where touch events are handled byself and translated them to click event. http://phonegap-tips.com/articles/fast-touch-event-handling-eliminate-click-delay.html I would not to like to go this road.
Have you any suggestion how can I solve this?
Here is more detail about touch events https://developer.mozilla.org/en-US/docs/Web/API/Touch_events Look at Handling clicks there is described how click is working in touchscreens. Still I didn't managed to work. Few months ago I but evt.preventDefault(); to my touchmove event handler and it did fix problem but currently it seems not.
EDIT:2019.11.5
Here is what was working earlier but no anymore:
html
<body (touchmove)="touchMoveEvent($event)"></body>
TypeScript
touchMoveEvent(ev: Event): void
{
ev.preventDefault();
}
And here is basic angular example of button and click handler which is not working if user is moving finger too much. I haven't check what is threshold but my I assume it is something near 10px-20px.
<button (click)="onClickEventHandler($event)">Press button</button>
onClickEventHandler(ev: Event) {
//do the thing here
}
I have tested touchscreen functionality with chrome's devtools toggle device toolbar.
Here is a nice solution. by using the touchstart and touchend events you can measure the distance between the 2 points and fire a click event if the events where close (in terms of pixels). read my comments.
class ScrollToClick {
constructor(elem, maxDistance = 20) {
this.elem = elem;
this.start = null;
this.maxDistance = maxDistance;
// Bind the touches event to the element
this.bindTouchEvents();
}
bindTouchEvents() {
this.elem.addEventListener('touchstart', this.onTouchStart.bind(this), false);
this.elem.addEventListener('touchend', this.onTouchEnd.bind(this), false);
}
onTouchStart(e) {
// hold the touch start position
this.start = e.touches[0];
// clear the position after 2000 mil (could be set for less).
setTimeout(() => { this.start = null; }, 2000);
}
onTouchEnd(e) {
// if the timeout was called, there will be no start position
if (!this.start) { return; }
// calculate the distance between start and end position
const end = e.changedTouches[0],
dx = Math.pow(this.start.pageX - end.pageX, 2),
dy = Math.pow(this.start.pageY - end.pageY, 2),
distance = Math.round(Math.sqrt(dx + dy));
// if the distance is fairly small, fire
// a click event. (default is 20 but you can override it through the constructor)
if (distance <= this.maxDistance) {
this.elem.click();
}
// clear the start position again
this.start = null;
}
}
Then you can use it with any element like so:
// use any element you wish (here I'm using the body)
const elem = document.body;
// initialize the class with the given element
new ScrollToClick(elem);
// listen to a click event on this element.
elem.addEventListener('click', (e) => {
console.log('Clicked');
})
This is a GitHub Issue that seems to be similar. I am not a JS dev so I am not sure but hope this helps.
My final solution is here. I forgot to mention in text that I am using Angular although I but in tag.
So I made Angular directive and but in AfikDeri's suggestion which was really close with directive style code.
import { Directive, ElementRef, Input, OnInit } from '#angular/core';
#Directive({
selector: '[touchClick]'
})
export class TouchClickDirective implements OnInit {
#Input() maxDistance = 100;
#Input() maxTime = 2000;
#Input() touchClick: boolean;
start: Touch;
constructor(private elem: ElementRef) {
this.start = null;
}
ngOnInit(): void {
// Bind the touches event to the element
this.bindTouchEvents();
}
bindTouchEvents() {
this.elem.nativeElement.addEventListener('touchstart', this.onTouchStart.bind(this), false);
this.elem.nativeElement.addEventListener('touchend', this.onTouchEnd.bind(this), false);
}
onTouchStart(e: TouchEvent) {
// hold the touch start position
this.start = e.touches[0];
// clear the position after 2000 mil (could be set for less).
setTimeout(() => {
this.start = null;
}, this.maxTime);
}
onTouchEnd(e: TouchEvent) {
// if the timeout was called, there will be no start position
if (!this.start) {
return;
}
// calculate the distance between start and end position
const end = e.changedTouches[0],
dx = Math.pow(this.start.pageX - end.pageX, 2),
dy = Math.pow(this.start.pageY - end.pageY, 2),
distance = Math.round(Math.sqrt(dx + dy));
// if the distance is fairly small, fire
// a click event. (default is 20 but you can override it through the constructor)
if (distance <= this.maxDistance) {
this.elem.nativeElement.click();
}
// clear the start position again
this.start = null;
}
}
And here is how it can be used
<button mat-flat-button [touchClick] [maxDistance]="100" [maxTime]="300" (click)="doWarning()">
Generate Warning
</button>
I worked out a quick solution to this problem based only on external value state set on different event listeners. Btn click fn will be triggered on touchend event if moveState variable will not change value by touchmove event. Touch start is always resetting state.
const moveState = false;
btn.addEventListener("click", (e) => handleBtnClick(e));
btn.addEventListener("touchstart", (e) => handleBtnTouchStart(e));
btn.addEventListener("touchmove", (e) => handleBtnTouchMove(e));
btn.addEventListener("touchend", (e) => handleBtnClick(e));
function handleHotspotTouchStart(e){
moveState = false;
}
function handleHotspotTouchMove(e){
moveState = true;
}
function handleBtnClick(e){
e.preventDefault;
if(e.type === 'touchend'){
if(moveState) return;
}
// trigger btn click action for both cursor click and touch if no movement detected
btnClick();
}
To add to the accepted answer, here is my react implementation:
import React, { useState } from 'react';
import './Button.css';
interface ButtonProps {
className: string,
value: string,
icon?: string,
onClick: () => void,
onPointerDown?: () => void,
onPointerUp?: () => void,
style?: React.CSSProperties,
}
function Button(props: ButtonProps): JSX.Element {
const [touchStart, setTouchStart] = useState(null);
const onTouchStart = (e) => {
// store the touchStart position
setTouchStart(e.touches[0]);
// clear the position after 2000ms
setTimeout(() => setTouchStart(null), 2000);
};
const onTouchEnd = (e) => {
// if the timeout was called, there will be no touchStart position
if (!touchStart) return;
// calculate the distance between touchStart and touchEnd position
const touchEnd = e.changedTouches[0],
dx = Math.pow(touchStart.pageX - touchEnd.pageX, 2),
dy = Math.pow(touchStart.pageY - touchEnd.pageY, 2),
distance = Math.round(Math.sqrt(dx + dy));
// if the distance is fairly small, fire a click event.
if (distance <= 50 && distance > 5) {
props.onClick();
}
// clear the start position again
setTouchStart(null);
};
return (
<button
className={`${props.className}`}
onClick={props.onClick}
onPointerDown={props.onPointerDown}
onPointerUp={props.onPointerUp}
onTouchStart={onTouchStart}
onTouchEnd={onTouchEnd}
style={props.style}
>
{props.icon ? <img className="button__icon" src={props.icon} alt=""/> : ''}
{props.value}
</button>
);
}
export default Button;
I'm just new to Laravel and Vuejs. And I have this problem wherein the whole component is re-rendering when the "Load More" button is clicked or when scrolled down to the bottom. The button or scroll is just acting like a pagination, but all you can do is to load more or add more displays. My problem is how can i render the new displays without re-rendering the whole component.
I tried creating a variable wherein it will pass how many paginate will be displayed. Yes it does the work but the component is re-rendering and the size of the reply from the server gets larger and larger.
here's my script on my Vue component:
<script>
export default {
props: ['user','review_count'],
data(){
return{
reviews: {},
limit: 2,
scrolledToBottom: false,
}
},
created(){
this.getReviews();
this.scroll();
},
methods: {
getReviews: function(page){
axios.get('/datas/reviews?user='+ this.user + '&limit='+ this.limit)
.then((response) =>{
this.reviews = response.data.data;
})
.catch(()=>{
});
},
countType: function(data, type) {
return data.filter(function(value) { return value.type === type }).length;
},
loadMore: function(){
this.limit+=6;
this.getReviews();
},
scroll () {
window.onscroll = () => {
let bottomOfWindow = Math.max(window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop) + window.innerHeight === document.documentElement.offsetHeight
if (bottomOfWindow&&this.review_count>this.limit) {
this.loadMore();
}
}
}
}
}
</script>
here's my controller:
public function reviews()
{
if($users = \Request::get('user')){
if($limit = \Request::get('limit')){
$reviews = Review::select(\DB::raw('id, product_id, review_rating, review_content'))
->where('user_id', $users)
->with('products:product_image,id,product_name')
->with('activities')
->orderBy('reviews.created_at', 'DESC')
->paginate($limit);}}
return $reviews;
}
I just solved my own Question, the solution is using pagination. I just pushed all of the data from the server in an object every time it scrolled down.
here's my code for scroll down:
scroll () {
window.onscroll = () => {
let bottomOfWindow = Math.max(window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop) + window.innerHeight === document.documentElement.offsetHeight
if (bottomOfWindow&&this.review_count>this.reviews.length) {
this.getReviews();
}
}
}
here's my codes for getReviews():
getReviews: function(){
let vm = this;
axios.get('/datas/reviews?user='+ this.user + '&page='+ this.page)
.then((response) =>{
$.each(response.data.data, function(key, value) {
vm.reviews.push(value);
console.log((vm.reviews));
});
})
.catch(()=>{
});
this.page+=1;
},
I've come up with this idea so that I will not use pagination anymore to view the next posts. It's more like a infinite scroll pagination component.
In my opinion, there is two things you need to do.
1/ in stead of increasing the limit eveytime, you should look into paging your results on the server, so you can ask the next page from your server. Now, on each consecutive call you are fetching what you already had again, which will eliminate your goal of making this less stressful on your server
2/ in your client code obviously you need to support the paging as well, but also make sure you properly set your key on the looped elements. VueJS used the key to determine whether it should rerender that particular element in the loop.
Let me know if this helps!
I'm trying to use PrimeNg TabView component along with confirmDialog unsuccessfully, here is my code:
<p-tabView (onChange)="onTabChange($event)" [(activeIndex)]="index">...</p-tabView>
onTabChange(event){
this.confirmationService.confirm({
message: 'Do you confirm ?',
accept: () => {
this.index = event.index;
},
reject:() =>{ }
});
}
Do you have an idea on how to prevent or allow tab change using confirm dialog ?
Thanks
Based on similar solution for material design tabs, here is the solution for my issue:
in html Declare a local variable referencing TabView DOM object:
<p-tabView #onglets>...</p-tabView>
in component.ts, change default function called when click on tab with specific
function to match your case:
#ViewChild('onglets') onglets: TabView;
this.onglets.open = this.interceptOngletChange.bind(this);
...
interceptOngletChange(event: Event, tab: TabPanel){
const result = confirm(Do you really want to leave the tab?);
return result && TabView.prototype.open.apply(this.onglets, argumentsList);
});
}
I had similar problem. Needed show dialog before tab change.
My solution:
HTML
<p-tabView #tabView (onChange)="onChange($event)" />
TS
#ViewChild('tabView') tabView: TabView;
onChange(event: any) {
const previoustab = this.tabView.tabs[this.prevIndex]; //saved previous/initial index
previoustab.selected = true;
const selectedTab = this.tabView.tabs[event.index];
selectedTab.selected = false;
this.tabView.activeIndex = this.prevIndex;
this.nextIndex= event.index;
}
GoToNextTab() {
this.tabView.activeIndex = this.nextIndex;
this.prevIndex= this.nextIndex;
this.tabView.open(undefined, this.tabView.tabs[this.nextIndex]);
}
With this code you will stay on the selected tab without tab style changes.