How to determine number of children in a slot - slots

Is there anyway to know how many children a named slot contains? In my Stencil component I have something like this in my render function:
<div class="content">
<slot name="content"></slot>
</div>
What I want to do is style the div.content differently depending on how many children are inside the slot. If there are no children in the slot, then div.content's style.display='none', otherwise, I have a bunch of styles applied to div.content that make the children appear correctly on the screen.
I tried doing:
const divEl = root.querySelector( 'div.content' );
if( divEl instanceof HTMLElement ) {
const slotEl = divEl.firstElementChild;
const hasChildren = slotEl && slotEl.childElementCount > 0;
if( !hasChildren ) {
divEl.style.display = 'none';
}
}
however this is always reporting hasChildren = false even when I have items inserted into the slot.

If you are querying the host element you will get all the slotted content inside of it. That means that the host element's children are going to be all the content that will be injected into the slot.
For example try to use the following code to see it in action:
import {Component, Element, State} from '#stencil/core';
#Component({
tag: 'my-component',
styleUrl: 'my-component.css',
shadow: true
})
export class MyComponent {
#Element() host: HTMLElement;
#State() childrenData: any = {};
componentDidLoad() {
let slotted = this.host.children;
this.childrenData = { hasChildren: slotted && slotted.length > 0, numberOfChildren: slotted && slotted.length };
}
render() {
return (
<div class="content">
<slot name="content"></slot>
<div>
Slot has children: {this.childrenData.hasChildren ? 'true' : 'false'}
</div>
<div>
Number of children: {this.childrenData.numberOfChildren}
</div>
</div>);
}
}

The accepted solution is actually not the correct way to do it. Even the code example is wrong. It is using a named slot name="content". Only elements with slot="content" attribute from the light DOM will be slotted into that slot; hence simply checking this.host.children is not sufficient at all.
Instead, you should work with the slotchange event (which also has the benefit of properly reflecting dynamic changes):
import {Component, Element, State} from '#stencil/core';
export type TSlotInfo = {
hasSlottedElements?: boolean;
numberOfSlottedElements?: number;
}
#Component({
tag: 'my-component',
shadow: true
})
export class MyComponent {
#Element() host: HTMLElement;
#State() slotInfo: TSlotInfo = {};
handleSlotChange = (event: Event) => {
let assignedElementCount = event.currentTarget.assignedElements().length;
this.slotInfo = {
hasSlottedElements: Boolean(assignedElementCount),
numberOfSlottedElements: assignedElementCount,
}
}
render() {
return (
<div class="content">
<slot name="content" onslotchange={this.handleSlotChange}></slot>
<div>
Slot is populated: {this.slotInfo.hasSlottedElements}
</div>
<div>
Number of slotted elements: {this.slotInfo.numberOfSlottedElements}
</div>
</div>);
}
}

Related

What should I do if the Class component of React cannot obtain the updated state in time?

I have a piece of code like this.
Modal.confirm({
tilte: "title",
content: (
<>
<input onChange={
() => this.setState({ A: true });
}></input>
this.state.A && <div>text</div>
</>
)
})
The purpose is that after the input changes, <div>text</div> will be displayed, but it is found that state A cannot be obtained in time after the change, resulting in the content in div not being displayed. How can I modify it? ?
Question:
You are using imperative (Modal.confirm) to display the Modal, and the state change does not trigger the Modal to re-render.
solution:
Modal can be displayed declaratively, for example:
class Test extends React.Component {
state = {
A: false,
visible: false
};
handleClick = () => {
this.setState({ visible: true });
};
render() {
const { visible } = this.state;
return (
<>
<Button onClick={this.handleClick}>Show Modal</Button>
<Modal
visible={visible}
onCancel={() => this.setState({ visible: false })}
>
<input onChange={() => this.setState({ A: true })}></input>
{this.state.A && <div>text</div>}
</Modal>
</>
);
}
}
Extract the content displayed by Modal.confirm into a component
class ConfirmContent extends React.Component {
state = {
A: false
};
render() {
return (
<>
<input onChange={() => this.setState({ A: true })}></input>
{this.state.A && <div>text</div>}
</>
);
}
}
//
Modal.confirm({
tilte: "title",
content: <ConfirmContent />
});
//
Root cause: The update granularity of React is a fiber tree, such an imperative method often recreates a fiber tree (internally it will call createRoot, or ReactDom.render), your question That's when the update operation is triggered with a fiber on fiber tree1 (the fiber tree you created yourself in App.tsx), not with fiber on fiber2 Update (the fiber tree you created with confirm), a component corresponds to a fiber, they belong to that fiber tree after they are passed a createRoot or ReactDom.renders

Setting the state from a callback in the parent component doesn't work

I'm trying to set the state from the parent component using a callback. This callback gets passed down to the child component that renders a material ui datatable. The callback responds onClick and passes some values to the callback. The problem is that setting the state with the values from the callback arguments doesn't work.
My assumption is that when the user clicks the button from the child component, it should invoke the callback function and pass the values I needed to set the state.
Parent Component:
export default function ViewJobs() {
const [type, setType] = useState('');
const [params, setParams] = useState({});
const callback = ({ cellValues, componentType, path }) => {
setType(componentType);
setParams(cellValues) // Sets the params with an object.
console.log(cellValues) // Displays the data I need in the console
history.push(path);
};
console.log(params) // Displays undefine in the console.
return(
<React.Fragment>
<TabPanel value={value} index={0} dir={theme.direction} >
<DataTable
jobs={job}
title='All'
parentCallback={callback}
/>
</TabPanel>
</React.Fragment>
);
}
Child Component
import React, { useEffect } from 'react';
export default function DataTable(props) {
const { jobs, parentCallback } = props;
const rows = jobs.payload;
const handleDiaryClick = (event, cellValues) => {
const params = {
cellValues,
componentType: 'diary',
path: "/view/jobs/diary"
};
parentCallback(params);
};
const renderDiaryElement = params => {
return (
<Button
variant="contained"
color="primary"
style={{ backgroundColor: "#000000" }}
onClick={(event) => {
handleDiaryClick(event, params);
}}
>
<MenuBookIcon />
</Button>
);
}
return (
<div
className={classes.root}
style={{ height: 400, width: '100%' }}
>
<DataGrid
rows={rows}
columns={columns}
pageSize={5}
//checkboxSelection
disableSelectionOnClick
/>
</div>
);
}
Since the state has been lifted up to the parent component, I'm under the impression that the code above should be working.
I tried to reproduce the issue but I couldn't replicate it.
Any advice or inputs are appreciated. Thanks.
After further checking on my codebase, I found that the history.push(path) located in my callback is causing the issue. I had to remove this line of code for it to work.

React Search Filter of Objects not filtering

I'm trying to create a search filter that will filter through facility names that lives in an array of objects.If I hard code an array into the state the filter works, but I need it to drab the info from props. The filtered list is being generated and showing all of the names on the screen but when I type it the textbox to filter nothing happens. What have I overlooked?
class FacilitySearch extends React.Component {
constructor(props) {
super(props);
this.state = {
search: ""
};
}
componentDidMount() {
this.props.dispatch(actions.getFacilitiesList());
}
//The subsr limits the # of characters a user can enter into the seach box
updateSearch = event => {
this.setState({ search: event.target.value.substr(0, 10) });
};
render() {
if (!this.props.facilityList) {
return <div>Loading...</div>
}
let filteredList = this.props.facilityList;
filteredList.filter(facility => {
return facility.facilityName.toLowerCase().indexOf(this.state.search.toLowerCase()) !== -1;
});
return (
<div>
<input
type="text"
value={this.state.search}
onChange={this.updateSearch.bind(this)}
placeholder="Enter Text Here..."
/>
<ul>
{filteredList.map(facility => {
return <li key={facility.generalIdPk}>{facility.facilityName}</li>;
})}
</ul>
</div>
);
}
}
const mapStateToProps = state => ({
facilityList: state.facilityList.facilityList
});
export default connect(mapStateToProps)(FacilitySearch)
The problem is that you are not storing the return value of filter in any variable.
You should do something like:
let filteredList = this.props.facilityList.filter(facility => {
return facility.facilityName.toLowerCase().indexOf(this.state.search.toLowerCase()) !== -1;
});
From MDN:
The filter() method creates a new array with all elements that pass the test implemented by the provided function.

Expected null to be truthy. Jasmine / Karma

I'm trying to test my component injected into a mock class I created. Although the component works when I try to test its existence it returns null.
Injectable Component:
import { Injectable, ElementRef, Renderer2, RendererFactory2 } from '#angular/core';
#Injectable()
export class NgBackdropComponent {
private renderer: Renderer2;
private appElementRef: ElementRef;
message: string = 'Carregando...';
constructor(rendererFactory: RendererFactory2) {
this.renderer = rendererFactory.createRenderer(null, null);
this.appElementRef = new ElementRef(<Element>document.getElementsByTagName('body').item(0));
}
show() {
const divSpinnerItem1 = this.renderer.createElement('i');
const divSpinnerItem2 = this.renderer.createElement('i');
const divSpinnerItem3 = this.renderer.createElement('i');
const divSpinner = this.renderer.createElement('div');
this.renderer.addClass(divSpinner, 'spinner');
this.renderer.appendChild(divSpinner, divSpinnerItem1);
this.renderer.appendChild(divSpinner, divSpinnerItem2);
this.renderer.appendChild(divSpinner, divSpinnerItem3);
const spanMensagem = this.renderer.createElement('span');
spanMensagem.innerHTML = this.message;
const div = this.renderer.createElement('div');
this.renderer.addClass(div, 'lock-content');
this.renderer.appendChild(div, divSpinner);
this.renderer.appendChild(div, spanMensagem);
this.renderer.appendChild(this.appElementRef.nativeElement, div);
}
hide() {
const elemento = this.appElementRef.nativeElement.querySelector('.lock-content');
if (elemento) {
elemento.remove();
}
}
}
my testing environment:
import { async, ComponentFixture, TestBed } from '#angular/core/testing';
import { NgBackdropComponent } from './ng-backdrop.component';
import { Component } from '#angular/core';
import { By } from '#angular/platform-browser';
#Component({
template: `
<button (click)="clickButton()"></button>
`
})
class MockNgBackdropComponent {
constructor(private backdrop: NgBackdropComponent) { }
clickButton() {
this.backdrop.message = 'Teste BackDrop aesdas';
this.backdrop.show();
console.log('iniciei backdrop');
}
closeBackdrop() {
this.backdrop.hide();
}
}
describe('NgBackdropComponent', () => {
let component: MockNgBackdropComponent;
let fixture: ComponentFixture<MockNgBackdropComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [MockNgBackdropComponent],
providers: [NgBackdropComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MockNgBackdropComponent);
component = fixture.componentInstance;
});
describe('Deve injetar', async () => {
it('Deve ter uma div principal', function () {
const btnClick = fixture.nativeElement.querySelector('button');
btnClick.click();
fixture.detectChanges();
const el = fixture.nativeElement.querySelector('.lock-content');
console.log(el);
expect(el).toBeTruthy();
});
});
});
In testing I create a Mock class where I inject my component.
I do not understand why it can not find the class because it exists.
The reason you can't find it in the component is because you did not create it in the component. If you look at this line in your constructor:
this.appElementRef = new ElementRef(<Element>document.getElementsByTagName('body').item(0))
You are creating it on the document directly in the <body> element. If you search for that in your spec, you will find it there. I created a STACKBLITZ to show you what I mean. Here is the spec from that stackblitz:
it('Deve ter uma div principal', () => {
const btnClick = fixture.nativeElement.querySelector('button');
console.log(btnClick);
btnClick.click();
fixture.detectChanges();
const appElementRef = new ElementRef(<Element>document.getElementsByTagName('body').item(0));
const el = appElementRef.nativeElement.querySelector('.lock-content');
expect(el).toBeTruthy();
});
Adding a little more clarification:
If you console.log(appElementRef) you'll notice that its tagName is body, and note the contents of its nativeElement.innerHTML Here is what that would look like "prettyfied":
<body>
<div class="jasmine_html-reporter">
<div class="jasmine-banner"><a class="jasmine-title" href="http://jasmine.github.io/" target="_blank"></a><span
class="jasmine-version">3.3.0</span></div>
<ul class="jasmine-symbol-summary"></ul>
<div class="jasmine-alert"></div>
<div class="jasmine-results">
<div class="jasmine-failures"></div>
</div>
</div>
<div id="nprogress" style="transition: none 0s ease 0s; opacity: 1;">
<div class="bar" role="bar" style="transform: translate3d(0%, 0px, 0px); transition: all 200ms ease 0s;">
<div class="peg"></div>
</div>
</div>
<div id="root0" ng-version="7.0.1">
<button></button>
</div>
<div class="lock-content">
<div class="spinner">
<i></i>
<i></i>
<i></i>
</div>
<span>Teste BackDrop aesdas</span>
</div>
</body>
Note how the button was created within the div with id="root0"? However, the div with class="lock-content" was created right off the root <body> element, and therefore is not within the div of the component.
In fact, you can see this very clearly when you console.log(fixture.nativeElement) and see that the tagName is "div", its innerHTML is <button></button>, and it has two attributes: id: "root0" and ng-version: "7.0.1". Put that all together and it looks like this:
<div id="root0" ng-version="7.0.1">
<button></button>
</div>
So you can clearly see that you cannot find the div you created in the component because you created it outside the component.
I hope this helps.
I think you should use DebugElement, for example:
it('Deve ter uma div principal', function () {
const btnClick = fixture.debugElement.query(By.css('button'));
btnClick.click();
fixture.detectChanges();
const el = fixture.debugElement.query(By.css('.lock-content'));
console.log(el);
expect(el).toBeTruthy();
});
Follow this link for more information.

Angular 2 Scroll to bottom (Chat style)

I have a set of single cell components within an ng-for loop.
I have everything in place but I cannot seem to figure out the proper
Currently I have
setTimeout(() => {
scrollToBottom();
});
But this doesn't work all the time as images asynchronously push the viewport down.
Whats the appropriate way to scroll to the bottom of a chat window in Angular 2?
I had the same problem, I'm using a AfterViewChecked and #ViewChild combination (Angular2 beta.3).
The Component:
import {..., AfterViewChecked, ElementRef, ViewChild, OnInit} from 'angular2/core'
#Component({
...
})
export class ChannelComponent implements OnInit, AfterViewChecked {
#ViewChild('scrollMe') private myScrollContainer: ElementRef;
ngOnInit() {
this.scrollToBottom();
}
ngAfterViewChecked() {
this.scrollToBottom();
}
scrollToBottom(): void {
try {
this.myScrollContainer.nativeElement.scrollTop = this.myScrollContainer.nativeElement.scrollHeight;
} catch(err) { }
}
}
The Template:
<div #scrollMe style="overflow: scroll; height: xyz;">
<div class="..."
*ngFor="..."
...>
</div>
</div>
Of course this is pretty basic. The AfterViewChecked triggers every time the view was checked:
Implement this interface to get notified after every check of your component's view.
If you have an input-field for sending messages for instance this event is fired after each keyup (just to give an example). But if you save whether the user scrolled manually and then skip the scrollToBottom() you should be fine.
Simplest and the best solution for this is :
Add this #scrollMe [scrollTop]="scrollMe.scrollHeight" simple thing on Template side
<div style="overflow: scroll; height: xyz;" #scrollMe [scrollTop]="scrollMe.scrollHeight">
<div class="..."
*ngFor="..."
...>
</div>
</div>
Here is the link for WORKING DEMO (With dummy chat app) AND FULL CODE
Will work with Angular2 and also upto 5, As above demo is done in
Angular5.
Note :
For error : ExpressionChangedAfterItHasBeenCheckedError
Please check your css,it's a issue of css side,not the Angular side
, One of the user #KHAN has solved that by removing overflow:auto; height: 100%; from div. (please check conversations for detail)
The accepted answer fires while scrolling through the messages, this avoids that.
You want a template like this.
<div #content>
<div #messages *ngFor="let message of messages">
{{message}}
</div>
</div>
Then you want to use a ViewChildren annotation to subscribe to new message elements being added to the page.
#ViewChildren('messages') messages: QueryList<any>;
#ViewChild('content') content: ElementRef;
ngAfterViewInit() {
this.scrollToBottom();
this.messages.changes.subscribe(this.scrollToBottom);
}
scrollToBottom = () => {
try {
this.content.nativeElement.scrollTop = this.content.nativeElement.scrollHeight;
} catch (err) {}
}
I added a check to see if the user tried to scroll up.
I'm just going to leave this here if anyone wants it :)
<div class="jumbotron">
<div class="messages-box" #scrollMe (scroll)="onScroll()">
<app-message [message]="message" [userId]="profile.userId" *ngFor="let message of messages.slice().reverse()"></app-message>
</div>
<textarea [(ngModel)]="newMessage" (keyup.enter)="submitMessage()"></textarea>
</div>
and the code:
import { AfterViewChecked, ElementRef, ViewChild, Component, OnInit } from '#angular/core';
import {AuthService} from "../auth.service";
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/concatAll';
import {Observable} from 'rxjs/Rx';
import { Router, ActivatedRoute } from '#angular/router';
#Component({
selector: 'app-messages',
templateUrl: './messages.component.html',
styleUrls: ['./messages.component.scss']
})
export class MessagesComponent implements OnInit {
#ViewChild('scrollMe') private myScrollContainer: ElementRef;
messages:Array<MessageModel>
newMessage = ''
id = ''
conversations: Array<ConversationModel>
profile: ViewMyProfileModel
disableScrollDown = false
constructor(private authService:AuthService,
private route:ActivatedRoute,
private router:Router,
private conversationsApi:ConversationsApi) {
}
ngOnInit() {
}
public submitMessage() {
}
ngAfterViewChecked() {
this.scrollToBottom();
}
private onScroll() {
let element = this.myScrollContainer.nativeElement
let atBottom = element.scrollHeight - element.scrollTop === element.clientHeight
if (this.disableScrollDown && atBottom) {
this.disableScrollDown = false
} else {
this.disableScrollDown = true
}
}
private scrollToBottom(): void {
if (this.disableScrollDown) {
return
}
try {
this.myScrollContainer.nativeElement.scrollTop = this.myScrollContainer.nativeElement.scrollHeight;
} catch(err) { }
}
}
Consider using
.scrollIntoView()
See https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
If you want to be sure, that you are scrolling to the end after *ngFor is done, you can use this.
<div #myList>
<div *ngFor="let item of items; let last = last">
{{item.title}}
{{last ? scrollToBottom() : ''}}
</div>
</div>
scrollToBottom() {
this.myList.nativeElement.scrollTop = this.myList.nativeElement.scrollHeight;
}
Important here, the "last" variable defines if you are currently at the last item, so you can trigger the "scrollToBottom" method
this.contentList.nativeElement.scrollTo({left: 0 , top: this.contentList.nativeElement.scrollHeight, behavior: 'smooth'});
If you are in recent version of Angular, following is enough:
<div #scrollMe style="overflow: scroll; height: xyz;" [scrollTop]="scrollMe.scrollHeight>
<div class="..."
*ngFor="..."
...>
</div>
</div>
const element = document.getElementById('box');
element.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' });
Vivek's answer has worked for me, but resulted in an expression has changed after it was checked error. None of the comments worked for me, but what I did was change the change detection strategy.
import { Component, ChangeDetectionStrategy } from '#angular/core';
#Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'page1',
templateUrl: 'page1.html',
})
The title of the question mentions "Chat Style" scroll to bottom, which I also needed. None of these answers really satisfied me, because what I really wanted to do was scroll to the bottom of my div whenever child elements were added or destroyed. I ended up doing that with this very simple Directive that leverages the MutationObserver API
#Directive({
selector: '[pinScroll]',
})
export class PinScrollDirective implements OnInit, OnDestroy {
private observer = new MutationObserver(() => {
this.scrollToPin();
});
constructor(private el: ElementRef) {}
ngOnInit() {
this.observer.observe(this.el.nativeElement, {
childList: true,
});
}
ngOnDestroy() {
this.observer.disconnect();
}
private scrollToPin() {
this.el.nativeElement.scrollTop = this.el.nativeElement.scrollHeight;
}
}
You just attach this directive to your list element, and it will scroll to the bottom whenever a list item changes in the DOM. It's the behavior I was personally looking for. This directive assumes that you are already handling height and overflow rules on the list element.
Sharing my solution, because I was not completely satisfied with the rest. My problem with AfterViewChecked is that sometimes I'm scrolling up, and for some reason, this life hook gets called and it scrolls me down even if there were no new messages. I tried using OnChanges but this was an issue, which lead me to this solution. Unfortunately, using only DoCheck, it was scrolling down before the messages were rendered, which was not useful either, so I combined them so that DoCheck is basically indicating AfterViewChecked if it should call scrollToBottom.
Happy to receive feedback.
export class ChatComponent implements DoCheck, AfterViewChecked {
#Input() public messages: Message[] = [];
#ViewChild('scrollable') private scrollable: ElementRef;
private shouldScrollDown: boolean;
private iterableDiffer;
constructor(private iterableDiffers: IterableDiffers) {
this.iterableDiffer = this.iterableDiffers.find([]).create(null);
}
ngDoCheck(): void {
if (this.iterableDiffer.diff(this.messages)) {
this.numberOfMessagesChanged = true;
}
}
ngAfterViewChecked(): void {
const isScrolledDown = Math.abs(this.scrollable.nativeElement.scrollHeight - this.scrollable.nativeElement.scrollTop - this.scrollable.nativeElement.clientHeight) <= 3.0;
if (this.numberOfMessagesChanged && !isScrolledDown) {
this.scrollToBottom();
this.numberOfMessagesChanged = false;
}
}
scrollToBottom() {
try {
this.scrollable.nativeElement.scrollTop = this.scrollable.nativeElement.scrollHeight;
} catch (e) {
console.error(e);
}
}
}
chat.component.html
<div class="chat-wrapper">
<div class="chat-messages-holder" #scrollable>
<app-chat-message *ngFor="let message of messages" [message]="message">
</app-chat-message>
</div>
<div class="chat-input-holder">
<app-chat-input (send)="onSend($event)"></app-chat-input>
</div>
</div>
chat.component.sass
.chat-wrapper
display: flex
justify-content: center
align-items: center
flex-direction: column
height: 100%
.chat-messages-holder
overflow-y: scroll !important
overflow-x: hidden
width: 100%
height: 100%
Here's another good solution on stackblitz.
Alternatively:
The accepted answer is a good solution, but it can be improved since your content/chat may often scroll to the bottom involuntarily given how the ngAfterViewChecked() lifecycle hook works.
Here's an improved version...
COMPONENT
import {..., AfterViewChecked, ElementRef, ViewChild, OnInit} from 'angular2/core'
#Component({
...
})
export class ChannelComponent implements OnInit, AfterViewChecked {
#ViewChild('scrollMe') private myScrollContainer: ElementRef;
/**Add the variable**/
scrolledToBottom = false;
ngAfterViewChecked() {
this.scrollToBottom();
}
scrollToBottom(): void {
try {
/**Add the condition**/
if(!this.scrolledToBottom){
this.myScrollContainer.nativeElement.scrollTop = this.myScrollContainer.nativeElement.scrollHeight;
}
} catch(err) { }
}
/**Add the method**/
onScroll(){
this.scrolledToBottom = true;
}
}
TEMPLATE
<!--Add a scroll event listener-->
<div #scrollMe
style="overflow: scroll; height: xyz;"
(scroll)="onScroll()">
<div class="..."
*ngFor="..."
...>
</div>
</div>
In angular using material design sidenav I had to use the following:
let ele = document.getElementsByClassName('md-sidenav-content');
let eleArray = <Element[]>Array.prototype.slice.call(ele);
eleArray.map( val => {
val.scrollTop = val.scrollHeight;
});
In case anyone has this problem with Angular 9, this is how I manage to fix it.
I started with the solution with #scrollMe [scrollTop]="scrollMe.scrollHeight" and I got the ExpressionChangedAfterItHasBeenCheckedError error as people mentioned.
In order to fix this one I just add in my ts component:
#Component({
changeDetection: ChangeDetectionStrategy.OnPush,
...})
constructor(private cdref: ChangeDetectorRef) {}
ngAfterContentChecked() {
this.cdref.detectChanges();
}
ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'
After reading other solutions, the best solution I can think of, so you run only what you need is the following:
You use ngOnChanges to detect the proper change
ngOnChanges() {
if (changes.messages) {
let chng = changes.messages;
let cur = chng.currentValue;
let prev = chng.previousValue;
if(cur && prev) {
// lazy load case
if (cur[0].id != prev[0].id) {
this.lazyLoadHappened = true;
}
// new message
if (cur[cur.length -1].id != prev[prev.length -1].id) {
this.newMessageHappened = true;
}
}
}
}
And you use ngAfterViewChecked to actually enforce the change before it renders but after the full height is calculated
ngAfterViewChecked(): void {
if(this.newMessageHappened) {
this.scrollToBottom();
this.newMessageHappened = false;
}
else if(this.lazyLoadHappened) {
// keep the same scroll
this.lazyLoadHappened = false
}
}
If you are wondering how to implement scrollToBottom
#ViewChild('scrollWrapper') private scrollWrapper: ElementRef;
scrollToBottom(){
try {
this.scrollWrapper.nativeElement.scrollTop = this.scrollWrapper.nativeElement.scrollHeight;
} catch(err) { }
}
Just in case someone is using Ionic and Angular,
here is a link that uses a very simple code to do that king of scroll to bottom (or top) :
https://forum.ionicframework.com/t/scroll-content-to-top-bottom-using-ionic-4-solution/163048
To add smooth scroll do this
#scrollMe [scrollTop]="scrollMe.scrollHeight" style="scroll-behavior: smooth;"
and
this.cdref.detectChanges();

Resources