Angular: unable to scroll down to bottom in element - javascript

I've been postponing fixing this error that I have been having for a while now. I have the below chatwindow:
The window where I display the messages is a separate component (chat-window.component.ts). I want to scroll to the bottom with ngOnChanges.
When we receive the conversation with the messages from the parent component, where it is received from the server via an asynchronous request, we want to scroll to the bottom of the window element. We do this by calling the this.scrollToBottom() method of the class in the ngOnChanges lifecycle hook.
This.scrollToBottom does get called, but it doesn't scroll to the bottom of the element. Can someone see why?
chat-window.component.ts: in ngOnchanges we do some synchronous stuff before we call this.scrollToBottom()
export class ChatboxWindowComponent implements OnChanges, OnInit, AfterViewChecked {
#Input('conversation') conversation;
#ViewChild('window') window;
constructor() { }
ngOnChanges() {
// If the date separators have already been added once, we avoid doing it a second time
const existingDateObj = this.conversation.messages.findIndex((item, i) => item.dateObj);
if (existingDateObj === -1) {
this.conversation.messages.forEach( (item, index, array) => {
if (index !== 0) {
const date1 = new Date(array[index - 1].date);
const date2 = new Date(item.date);
if (date2.getDate() !== date1.getDate() || date2.getMonth() !== date1.getMonth()) {
this.conversation.messages.splice(index, 0, {date: date2, dateObj: true});
console.log(this.conversation.messages.length);
}
}
});
}
this.scrollToBottom();
}
ngOnInit() {
}
ngAfterViewChecked() {
}
isItMyMsg(msg) {
return msg.from._id === this.conversation.otherUser.userId;
}
scrollToBottom() {
try {
console.log('scrollToBottom called');
this.window.nativeElement.top = this.window.nativeElement.scrollHeight;
} catch (err) {}
}
}
chat-window.component.html
<div #window class="window">
<ng-container *ngFor="let message of conversation.messages">
<div class="date-container" *ngIf="!message.msg; else windowMsg">
<p class="date">{{message.date | amDateFormat:'LL'}}</p>
</div>
<ng-template #windowMsg>
<p
class="window__message"
[ngClass]="{
'window__message--left': isItMyMsg(message),
'window__message--right': !isItMyMsg(message)
}"
>
{{message.msg}}
</p>
</ng-template>
</ng-container>
</div>

The scroll doesn't work because the list of messages is not rendered yet when you call scrollToBottom. In order to scroll once the messages have been displayed, set a template reference variable (e.g. #messageContainer) on the message containers:
<ng-container #messageContainer *ngFor="let message of conversation.messages">
...
</ng-container>
In the code, you can then access these elements with ViewChildren and scroll the window when the QueryList.changes event is triggered:
#ViewChildren("messageContainer") messageContainers: QueryList<ElementRef>;
ngAfterViewInit() {
this.scrollToBottom(); // For messsages already present
this.messageContainers.changes.subscribe((list: QueryList<ElementRef>) => {
this.scrollToBottom(); // For messages added later
});
}

You can add the following code into your HTML element.
#window [scrollTop]="window.scrollHeight" *ngIf="messages.length > 0"
Full code according to your code sample as follows,
<div #window [scrollTop]="window.scrollHeight" *ngIf="messages.length > 0" class="window">
<ng-container *ngFor="let message of conversation.messages">
<div class="date-container" *ngIf="!message.msg; else windowMsg">
<p class="date">{{message.date | amDateFormat:'LL'}}</p>
</div>
<ng-template #windowMsg>
<p
class="window__message"
[ngClass]="{
'window__message--left': isItMyMsg(message),
'window__message--right': !isItMyMsg(message)
}"
>
{{message.msg}}
</p>
</ng-template>
</ng-container>
</div>
This is work for me. (Currently, I'm using Angular 11) 😊👍

Related

Free Drag with cdkDropList

I'm working on a project where I need to implement some sort of drop zone where you can drag element from a list, then drop them in a zone where they can be dragged freely. I also would like to use a cdkDropList for the zone, because it provides all the tools for connecting lists.
I used this example as a reference for my implementation, but I am not able to make it work right.
When I drop an item in the zone, it does not drop where the cursor was, it just goes to the top left of my zone, like it was in a list.
When I drag an item in the zone, it either drags correctly to where I want it to be dropped, gets dropped near the drop point, or just goes back to the top left.
Here is my cdkDrag element, it differs from the example linked above because I absolutely need it to be in it's own component (I would like to apply some logic to it in the future), but it is essentially the same concept (cdkDrag div in cdkDropList div). I managed to route all the needed events to the parent element (the zone) using Outputs.
<div class="element-box"
cdkDrag
cdkDragBoundary={{boundary_name}}
(cdkDragDropped)="dragDroppedEventToParent($event)"
(cdkDragStarted)="dragStartedEventToParent($event)"
(cdkDragMoved)="dragMovedEventToParent($event)">
{{object_name}}
<div *cdkDragPlaceholder class="field-placeholder"></div>
</div>
Here is the logic for the drag element:
export class ElementBoxComponent implements OnInit {
#Input () object_name: string;
#Input () boundary_name: string;
#Input () itemSelf: any;
#Output () dragMovedEvent = new EventEmitter<CdkDragMove>();
#Output () dragStartedEvent = new EventEmitter<CdkDragStart>();
#Output () dragDroppedEvent = new EventEmitter<any>();
constructor() {}
ngOnInit(): void {
}
dragMovedEventToParent(event: CdkDragMove) {
this.dragMovedEvent.emit(event);
}
dragStartedEventToParent(event: CdkDragStart){
this.dragStartedEvent.emit(event);
}
dragDroppedEventToParent(event: CdkDragEnd){
this.dragDroppedEvent.emit({event, "self": this.itemSelf});
}
}
Here is my drop zone element, where I render the drag elements (you can see that I routed the Outputs to methods in my logic for the zone):
<div class="drop-container"
#cdkBoard
cdkDropList
[id]="'cdkBoard'"
[cdkDropListData]="itemsInBoard"
[cdkDropListConnectedTo]="connectedTo"
cdkDropListSortingDisabled="true"
(cdkDropListDropped)="itemDropped($event)">
<app-element-box *ngFor="let item of itemsInBoard; let i=index"
object_name="{{item.name}}"
boundary_name=".drop-container"
itemSelf="{{item}}"
style="position:absolute; z-index:i"
[style.top]="item.top"
[style.left]="item.left"
(dragMovedEvent)="elementIsMoving($event)"
(dragStartedEvent)="startedDragging($event)"
(dragDroppedEvent)="stoppedDragging($event)"></app-element-box>
<svg-container class="bin-container" containerId="bin-image-container" *ngIf="_binVisible" height=40>
<svg-image class="bin-icon" [imageUrl]="BIN_ICON_URL" height=40 width=40></svg-image>
</svg-container>
</div>
And here are the relevant methods in the TS file for my drop zone:
itemDropped(event: CdkDragDrop<any[]>) {
if (event.previousContainer === event.container) {
moveItemInArray(this.itemsInBoard, event.previousIndex, event.currentIndex);
} else {
copyArrayItem(
event.previousContainer.data,
event.container.data,
event.previousIndex,
event.currentIndex
)
}
}
changePosition(event: CdkDragDrop<any>, field) {
const rectZone = this.dropZone.nativeElement.getBoundingClientRect();
const rectElement = event.item.element.nativeElement.getBoundingClientRect();
const top = rectElement.top + event.distance.y - rectZone.top;
const left = rectElement.left + event.distance.x - rectZone.left;
const out = (top < 0) || (left < 0) || (top > (rectZone.height - rectElement.height)) || (left > (rectZone.width - rectElement.width));
if (!out) {
event.item.element.nativeElement.style.top = top + 'px';
event.item.element.nativeElement.style.left = left + 'px';
} else {
this.itemsInBoard = this.itemsInBoard.filter((x) => x != event.item);
}
}
Again, the only differences are that my elements are encapsulated in their own components, and that the way I access the top and left style elements of the components are different (the code in the example did not work).
I know that the problem is the way I calculate the top and left variable, but I've been stuck on this for a week and cannot seem to find out what's wrong with it.
Here is a short demonstrative video if you want to better visualize what I am talking about.
Does anyone know what could be wrong with this ? I am open to any suggestions, thank you :)
It's difficut without a stackblitz know what is wrong
In this stackblitz I made two ckd-list
One cdkDropList (todoList) it's a typical "list", the other one is a dropZone (doneList). My elements are in the way
{label:string,x:number,y:number,'z-index':number}
The cdkDrag in the dropZone is in the way
<div cdkDrag class="item-box"
[style.top.px]="item.y"
[style.left.px]="item.x"
[style.z-index]="item['z-index']"
>
I choose that the todoList is connected to the dropZone, but the dropZone is not connected to anything. When I mover the elements of the dropZone if it's move away this one, simply add to the list
We need get the doneList as ElementRef
#ViewChild('doneList',{read:ElementRef,static:true}) dropZone:ElementRef;
And the neccesary functions
drop(event: CdkDragDrop<any[]>) {
if (event.previousContainer === event.container) {
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
} else {
transferArrayItem(
event.previousContainer.data,
event.container.data,
event.previousIndex,
event.currentIndex,
);
event.item.data.y=(this._pointerPosition.y-this.dropZone.nativeElement.getBoundingClientRect().top)
event.item.data.x=(this._pointerPosition.x-this.dropZone.nativeElement.getBoundingClientRect().left)
this.changeZIndex(event.item.data)
}
this.posInside={source:null,x:0,y:0}
}
moved(event: CdkDragMove) {
this._pointerPosition=event.pointerPosition;
}
changeZIndex(item:any)
{
this.done.forEach(x=>x['z-index']=(x==item?1:0))
}
changePosition(event:CdkDragDrop<any>,field)
{
const rectZone=this.dropZone.nativeElement.getBoundingClientRect()
const rectElement=event.item.element.nativeElement.getBoundingClientRect()
let y=+field.y+event.distance.y
let x=+field.x+event.distance.x
const out=y<0 || x<0 || (y>(rectZone.height-rectElement.height)) || (x>(rectZone.width-rectElement.width))
if (!out)
{
field.y=y
field.x=x
}
else{
this.todo.push(field)
this.done=this.done.filter(x=>x!=field)
}
}
The .html like
<div class="wrapper">
<div
cdkDropList
#todoList="cdkDropList"
[cdkDropListData]="todo"
[cdkDropListConnectedTo]="[doneList]"
class="example-list"
(cdkDropListDropped)="drop($event)"
>
<div
class="example-box"
*ngFor="let item of todo"
cdkDrag
[cdkDragData]="item"
(cdkDragMoved)="moved($event)"
>
{{ item.label }}
<div *cdkDragPlaceholder class="field-placeholder"></div>
</div>
</div>
<div
cdkDropList
#doneList="cdkDropList"
[cdkDropListData]="done"
class="drag-zone"
cdkDropListSortingDisabled="true"
(cdkDropListDropped)="drop($event)"
>
<ng-container *ngFor="let item of done">
<div
cdkDrag
class="item-box"
[style.top.px]="item.y"
[style.left.px]="item.x"
[style.z-index]="item['z-index']"
(cdkDragStarted)="changeZIndex(item)"
(cdkDragDropped)="changePosition($event, item)"
>
{{ item.label }}
<div *cdkDragPlaceholder class="field-placeholder"></div>
</div>
</ng-container>
</div>
</div>

statusChanges not called when switch TAB

I have a parent component which has two TABs. Each TAB hold one child component. Every child component has a form.
On the parent component there is a button. What I want is once two reactive forms are valid then enable the button.
The parent component likes
<Button (click)="Submit()" [disabled]="!isChild1FormValid || !isChild2FormValid">Submit</Button>
<kendo-tabstrip (tabSelect)="onTabSelect($event)">
<kendo-tabstrip-tab [title]="'Paris'" [selected]="true">
<ng-template kendoTabContent>
<app-child1 (isChild1FormValid)="trigger1($event)">
</app-child1>
</ng-template>
</kendo-tabstrip-tab>
<kendo-tabstrip-tab [title]="'New York City'">
<ng-template kendoTabContent>
<app-child2 (isChild2FormValid)="trigger2($event)">
</app-child2>
</ng-template>
</kendo-tabstrip-tab>
</kendo-tabstrip>
I parent's ts file. We have the methods.
trigger1(isValid: boolean) {
isChild1FormValid = isValid;
}
trigger2(isValid: boolean) {
isChild2FormValid = isValid;
}
In child1 component.
#Output() isChild1FormValid: EventEmitter<boolean> = new EventEmitter<boolean>();
In its `ngOnInit()`, we have
childForm1.statusChanges.subscribe(res => {
if(res == 'VALID') {
this.isChild1FormValid.emit(true);
}
});
}
Similar in child2 component,
#Output() isChild2FormValid: EventEmitter<boolean> = new EventEmitter<boolean>();
In its `ngOnInit()`, we have
childForm2.statusChanges.subscribe(res => {
if(res == 'VALID') {
this.isChild2FormValid.emit(true);
}
});
}
Let's say child1 form and child2 form have some textbox, which are required.(Omitted code here since it just Validators.required.
Now I typed some text in the controls and set breakpoint in the statusChanges event. Now the question is child2 statusChanges method is not called. But child1 is okay, the validation does work. I guess that is the TAB issue when I switch it, it may reload???
UPDATED:
Not sure why it works in stackblitz but failed in my application.
It turns out a lower level error, I didn't put the formcontrol's name in the element. I should be careful to check it.
Such as
<label>
First Name:
<input type="text" formControlName="firstName">
`

ngx-bootstrap accordion open panel dynamically

I use ngx-bootstrap accordion to show a list of blog posts.
Here is the template:
<accordion id="blog-list">
<accordion-group *ngFor="let post of posts; let first = first;" [isOpen]="first" id="post-{{post.id}}">
<!-- Here goes content irrelevant to the question -->
</accordion-group>
</accordion>
I also use some global config, to have only one open accordion panel at a time.
export function getAccordionConfig(): AccordionConfig {
return Object.assign(new AccordionConfig(), { closeOthers: true });
}
Now, when a post gets updated, I update it in the list, like so:
constructor(private elementRef: ElementRef, private postService: PostService) {
this.postService.updatedPost.subscribe(val => {
let i = this.posts.findIndex(post => post.id === val.id);
this.posts[i] = val;
let element = elementRef.nativeElement.querySelector('#post-' + val.id);
element.setAttribute('isOpen', true); // <- this does not work
element.scrollIntoView(true);
});
}
Updating and scrolling works fine, but I can't figure out how to get the panel to open. After the view gets updated and scrolled all panels are closed. I want the panel with the updated post to be open.
So issue is in [isOpen]="first", first post will be opened by default
direct manipulation with DOM will no trigger bindings updates
what you could do is:
[isOpen]="activPostIndex === index"
activPostIndex = 0;
constructor(private elementRef: ElementRef, private postService: PostService) {
this.postService.updatedPost.subscribe(val => {
this.activPostIndex = this.posts.findIndex(post => post.id === val.id);
this.posts[i] = val;
});
}

Start List at the bottom

I am using Ionic 2. I have a list of items:
this.firelist = this.dataService.findMessages(this.chatItem).map(items => {
this.updateReadMessages(items);
return items.reverse();
});
Displayed in a list:
<ion-content padding class="messages-page-content">
<ion-list class="message-list">
<ion-item class="message-item" *ngFor="let item of firelist | async">
....
This works, but as you can see, I have a reverse list. So the latest item is at the bottom. As a result, I would like to start the display at the bottom.
I have tried:
window.setTimeout(()=> {this.content.scrollToBottom();}, 2000);
This works, but there is a delay on the scroll, and visually the scroll doesn't look as good as if the list could just start at the bottom, and not have to scroll.
Is this possible?
Thanks
I doubt you will find a very elegant solution for this, but you can try the following:
Try using ionViewWillEnter:
ionViewWillEnter() {
this.content.scrollToBottom(0)
}
You could also try bind to the last item of your ngFor and then fire the scroll as the last item is rendered. Something similar to this:
<ion-item class="message-item" *ngFor="let item of firelist | async; let last = last">
{{ item }}
{{ last ? doScroll() : '' }}
</ion-item>
In your component:
export class somePage{
...
constructor(...) {
setTimeout(() => {
for (let i = 0; i < 100; i++) {
this.items[i] = i
}
}, 300)
}
doScroll(){
this.content.scrollToBottom(0)
}
}

How to trigger an event when component is navigated to?

I have an angular2 application (RC5), where I have a chapter component. This basically has a big template file - chapter.html - which has this:
<div *ngIf="chapter == 1">
<!-- Chapter 1 content -->
</div>
<div *ngIf="chapter == 2">
<!-- Chapter 2 content -->
</div>
<!-- etc. -->
I then have some arrow buttons for next and for previous chapters. These trigger an event e.g.
if (this.chapter !== '6') {
var next = parseInt(this.chapter) + 1;
console.log(next);
let link = ['/tutorial/chapter', next];
this.router.navigate(link);
}
So that all works fine, however, the buttons are at the bottom of a chapter, so when they are clicked, the next chapter is displayed, but automatically scrolled down. So on the router click, I would like to trigger an event and scroll to the top of the page, however, as I am navigating to the same component, ngOnInit() isn't triggered.
How can I do this?
If you navigate to the same component, at least a parameter has changed, otherwise the router wouldn't re-navigate.
You can subscribe to parameter changes and invoke scrolling on changes
constructor(private route:ActivatedRoute) {
this.route.params.forEach(p => this.doScroll());
}
constructor(private route:ActivatedRoute) {
this.route.params.subscribe(params => {
//do your stuffs...
});
}

Categories

Resources