In my angular UI code, I have a component class that calls a like below
app.component.html
//...
<div class="banner">
<p-dialog [(visible)]="displayCOI" styleClass="coiDialog" [contentStyle]="{'overflow-y': 'hidden'}" [modal]="true" [style]="{width: '75vw'}" [baseZIndex]="10000" [showHeader]="false"
[draggable]="false" [resizable]="false">
<coi (notify)="onCoIAccept($event)"></coi>
</p-dialog>
</div>
...///
coi.component.html looks like below
<div>
<div class="row" style="padding: 10px 0px">
<div class="col-sm-4">
<p align="center"><b>Instructions</b></p>
<br>...//
</div>
<div #scrollDiv id="scrollDiv" class="col-sm-6" style="height:350px; overflow-y: scroll;" (scroll)="onScroll($event)">
<p-table #dt [columns]="cols" [scrollable]="true" [value]="usersLi" [(selection)]="selectedUsersLi" dataKey="id">
//....
..///
</p-table>
</div>
<div class="col-sm-2">
<div align="center">
<button pButton type="button" label="Accept" [disabled]="disableAccept" (click)="close()" class="ui-button-rounded"></button>
</a>
</div>
</dv>
</div>
coi.component.ts code is as below:
export class coiComponent {
#ViewChild("scrollDiv") scrollDiv: ElementRef;
disableAccept: boolean = false;
ngOnInit():void {
this.keys = Object.keys(this.propertyObj);
this._utilService.convertKeysToHeader(this.keys,this.cols);
this.disableAccept = true;
this.loadRecords();
}
onScroll(event: any) {
// visible height + pixel scrolled >= total height
if (event.target.offsetHeight + event.target.scrollTop >= event.target.scrollHeight) {
this.disableAccept = false;
console.log("End");
}
}
ngOnChanges(changes: SimpleChanges){
console.log("ngOnChanges" , changes);
for ( const propName in changes){
let change = changes[propName];
if ( propName == 'coi'){
// console.log('CHANGED...DO HERE');
console.log(this.scrollDiv.nativeElement.offsetHeight);
console.log(this.scrollDiv.nativeElement.scrollHeight);
}
}
}
}
As you can see the modal is divided into 3 DIV. 1. instrucations, 2: table, 3. Accept button
The modal by itself has a fixed height and scroll hidden. The div with table has a fixed height and overflow scroll and it works perfectly. Now the table can be with 30-50 records so vertical scrolling is enabled. I want the accept button on the 3rd div to be enabled only when the user had scrolled the table and has seen all the records. So the function (scroll)="onScroll($event)" enables only when the scroll is scrolled completely and it works perfectly.
my question is Some users may see less than 5-10 records which means scroll wouldn't be enabled for those users and accept also need to be enabled for them. Any suggestion on how to do this, please? I tried adding an id for the div tag called "scrollDiv" and #scrollDiv and passing this as an ElementRef and on ngOnChange trying to get the offsetHeight and scrollHeight but I get value '0' on all the cases.` Can someone help me with this?
I have updated my question. Please give some suggestions. Thank you.
I'll try to give you a running idea, then you can understand and apply it to your case. You can check at this link.
Explanation of the example
In the component you have 4 important things to consider:
isActionDisabled a variable that says if your action should be disabled or not
#ViewChild('containerElement') containerElement a refer to the scrollable container of the table
onScrollContainer a method that's executed when you scroll the containerElement
disableByScroll a method that changes the value of isActionDisabled according to the position of the scrollbar of the containerElement. If it's on bottom isActionDisabled is false otherwise is true.
The most important method is disableByScroll:
disableByScroll(): void {
if (this.containerElement) {
const element = this.containerElement.nativeElement;
this.isActionDisabled = !(
element.scrollTop ===
element.scrollHeight - element.clientHeight
);
} else {
this.isActionDisabled = true;
}
}
Please read this article to understand what I did.
disableByScroll is called each time a scroll event is called on containerElement
onScrollContainer(): void {
this.disableByScroll();
}
and after view init
ngAfterViewInit(): void {
this.disableByScroll();
this.cdr.detectChanges();
}
That is useful if you have a number of items that do not activate the scrollbar. Please, read this guide to understand the lifecycle events of an Angular application. As you can see I called a method detectChanges of the ChangeDetectorRef. Reading that guide you'll understand why.
About the template, it's pretty simple and you can figure it out.
Related
I've got an html element with overflow: auto, but I want to give it a border only when it's scrollable.
How do I evaluate the element's size from within ngClass without getting any kind of null errors?
Note: The element's enclosing div doesn't get rendered until after getting a response from an observable.
Attempt 1:
The html element is set up like this:
<div ngIf="!loading">
<div id="{{someID}}" [ngClass]="{'border-class': isScrollable}"> ... </div>
</div>
In my ngOnInit, I call a function to see if the given element can be scrolled.
ngOnInit() {
// this.loading gets set to false after an observable is returned
/* ... */
// scroll check
let e = document.getElementById(`${this.someID}`);
if (element !== null) {
this.isScrollable = e.scrollHeight > e.clientHeight;
}
}
If I don't check for null, I get errors. If I do check for null, then even if I have scrollable content, when the page is loaded, the border doesn't show up.
I thought the issue might be with this.loading, so I added the scroll check within the observable response, but after loading was set to false. Still no border.
Attempt 2:
<div #textDiv [ngClass]="{'border-class': isScrollable}"> ... </div>
#ViewChild('textDiv') element: ElementRef;
/* ... */
ngAfterViewInit() {
this.isScrollable = this.element.scrollHeight > this.element.clientHeight;
}
But the border still doesn't show up on scrollable content when the page is loaded.
Attempt 3:
The only thing that has worked, is this hot mess:
setTimeout( () => {
this.isScrollable = this.element.scrollHeight > this.element.clientHeight;
});
Is there a way I can get this to work without calling setTimeout?
The problem is here:
<div ngIf="!loading">
<div id="{{someID}}" [ngClass]="{'border-class': isScrollable}"> ...
</div>
The isScrollable is not updating the value after it changes.
You can fix this, using a get and returning the value when ngAfterViewInit was already executed:
export class CustomComponent implements AfterViewInit {
private afterViewInitExecuted = false;
#ViewChild('textDiv') element: ElementRef;
public get isScrollable() {
if(this.afterViewInitExecuted) {
return this.element.scrollHeight > this.element.clientHeight;
}
return false;
}
ngAfterViewInit() {
this.afterViewInitExecuted = true;
}
}
Then in your html:
<div ngIf="!loading">
<div id="{{someID}}" [ngClass]="{'border-class': isScrollable() }"> ...
</div>
With that, it should work.
I am trying to do the following: when clicking on one element, I open another one. I need to calculate the height of that element I just opened.
I don't know how to do it because I have no event on the element I am opening, I will provide a small example code just so you get the idea what I want to do.
<div class="parent">
<div class="left">
<div (click)="openRight = !openRight" class="click-element"></div>
</div>
<div *ngIf="openRight" class="right">
</div>
</div>
The goal is to dynamically set the height of left element based on the height of right element. Right element will have position absolute, that is the reason I need to get the height.
Thanks in advance!
You can access the DOM element in angular using #. Then when you get the click event you can access the right element and get it's height.
HTML
<div #rightElement class="right">
</div>
TS
import { ElementRef, ViewChild } from '#angular/core';
#ViewChild('rightElement') rightElement: ElementRef;
// get the height
this.rightElement.nativeElement.offsetHeight;
#EDIT
Why you have an undefined child is because you are using *ngIf condition. I think you are doing something like :
clickEventFunction($event) {
...
this.openRight = true;
...
// Use of the #rightElement
this.rightElement.nativeElement.offsetHeight
...
}
The problem is that angular will only see that you modified openRight after the execution of clickEventFunction, so #rightElement do not exist and ... UNDEFINED!
What you can do, is to say to angular that you did a change, so it will create the right element and then you could use of #rightElement.
Example :
import { ChangeDetectorRef } from 'angular2/core';
constructor(protected chRef: ChangeDetectorRef){
...
}
clickEventFunction($event) {
...
this.openRight = true;
// Tell angular to look at openRight
this.chRef.detectChanges();
...
// Use of the #rightElement
this.rightElement.nativeElement.offsetHeight
...
}
you can try below code :
<div #mainScreen></div>
in component file
import { ElementRef, ViewChild } from '#angular/core';
export class viewApp{
#ViewChild('mainScreen') elementView: ElementRef;
viewHeight: number;
clickMe(){
this.viewHeight = this.elementView.nativeElement.offsetHeight;
}
}
or
<div *ngFor="let item of items" (click)="clickMe($event.currentTarget)"></div>
clickMe(dom){
let viewHeight=dom.clientHeight;
}
I'm using Redux in my app, inside a Component I want to scroll to an specific div tag when a change in the store happens.
I have the Redux part working so it triggers the componentDidUpdate() method (I routed to this compoennt view already).
The problem as far as I can tell, is that the method scrollIntoView() doesn't work properly cos componentDidUpdate() has a default behavior that scrolls to the top overwriting the scrollIntoView().
To work-around it I wrapped the function calling scrollIntoView() in a setTimeout to ensure that happens afeterwards.
What I would like to do is to call a preventDefault() or any other more elegant solution but I can't find where to get the event triggering the 'scrollTop'
I looked through the Doc here: https://facebook.github.io/react/docs/react-component.html#componentdidupdate
and the params passed in this function are componentDidUpdate(prevProps, prevState) ,since there is no event I don't know how to call preventDefault()
I've followd this Docs: https://facebook.github.io/react/docs/refs-and-the-dom.html
And tried different approaches people suggested here: How can I scroll a div to be visible in ReactJS?
Nothing worked though
Here is my code if anyone has any tip for me, thanks
class PhotoContainer extends React.Component {
componentDidUpdate(){
setTimeout(() => {
this.focusDiv();
}, 500);
}
focusDiv(){
var scrolling = this.theDiv;
scrolling.scrollIntoView();
}
render() {
const totalList = [];
for(let i = 0; i < 300; i += 1) {
totalList.push(
<div key={i}>{`hello ${i}`}</div>
);
}
return (
<div >
{totalList}
<div ref={(el) => this.theDiv = el}>this is the div I'm trying to scroll to</div>
</div>
)
};
}
Ok it's been a while but I got it working in another project without the setTimeOut function so I wanted to answer this question.
Since Redux pass the new updates through props, I used the componentWillRecieveProps() method instead of componentDidUpdate() , this allowes you a better control over the updated properties and works as expected with the scrollIntoView() function.
class PhotoContainer extends React.Component {
componentWillReceiveProps(newProps) {
if (
this.props.navigation.sectionSelected !==
newProps.navigation.sectionSelected &&
newProps.navigation.sectionSelected !== ""
) {
this.focusDiv(newProps.navigation.sectionSelected);
}
}
focusDiv(section){
var scrolling = this[section]; //section would be 'theDiv' in this example
scrolling.scrollIntoView({ block: "start", behavior: "smooth" });//corrected typo
}
render() {
const totalList = [];
for(let i = 0; i < 300; i += 1) {
totalList.push(
<div key={i}>{`hello ${i}`}</div>
);
}
return (
<div >
{totalList}
<div ref={(el) => this.theDiv = el}>
this is the div I am trying to scroll to
</div>
</div>
)
};
}
I also struggled with scrolling to the bottom of a list in react that's responding to a change in a redux store and I happened upon this and a few other stackoverflow articles related to scrolling. In case you also land on this question as well there are a few ways this could be a problem. My scenario was that I wanted a 'loading' spinner screen while the list was rendering. Here are a few wrong ways to do this:
When loading = true, render spinner, otherwise render list.
{loading ?
<Spinner />
:
<List />
}
as stated above this doesn't work because the list you might want to scroll to the bottom of isn't rendered yet.
When loading set the display to block for the spinner and none for the list. When done loading, reverse the display.
<div style={{display: loading ? 'block' : 'none'>
<Spinner />
</div>
<div style={{display: loading ? 'none' : 'block'>
<List />
</div>
This doesn't work either since the list you want to scroll to the bottom of isn't actually being displayed likely when you call the scroll.
The better approach for the above scenario is to use a loading that acts as an overlay to the component. This way both the spinner and list are rendered and displayed, the scroll happens, and when the loading is complete, the spinner can be de-rendered or set to be invisible.
I've been messing around with aurelia-dialog trying to get a modal dynamically populated with some information. I have some stuff working but the modal is the incorrect size for the data its displaying.
welcome.js
import {DialogService} from 'aurelia-dialog';
import {CmdModal} from './cmd-modal';
export class Welcome {
static inject = [DialogService];
constructor(dialogService) {
this.dialogService = dialogService;
}
OpenCmd(intName, opName, opDescription, parameters){
var cmd = { "CmdName" : opName, "Description" : opDescription, "Params" : parameters};
this.dialogService.open({ viewModel: CmdModal, model: cmd}).then(response => {
if (!response.wasCancelled) {
console.log('good - ', response.output);
} else {
console.log('bad');
}
console.log(response.output);
});
}
cmd-modal.html
<template>
<ai-dialog>
<ai-dialog-header>
<h2>${cmd.CmdName}</h2>
</ai-dialog-header>
<ai-dialog-body>
<p>${cmd.Description}</p>
<b>Parameters</b>
<div repeat.for="param of cmd.Params">
<p class="col-md-6">${param.Key}</p>
<p class="col-md-6">${param.Value}</p>
</div>
</ai-dialog-body>
<ai-dialog-footer>
<button click.trigger="controller.cancel()">Cancel</button>
<button click.trigger="controller.ok(person)">Ok</button>
</ai-dialog-footer>
</ai-dialog>
</template>
cmd-modal.js
import {DialogController} from 'aurelia-dialog';
export class CmdModal {
static inject = [DialogController];
constructor(controller){
this.controller = controller;
}
activate(cmd){
this.cmd = cmd;
}
}
When a link is clicked, a modal like the following is displayed:
As the image shows, the modal is the wrong size for the body and some of the text spills over the side. I think this is because cmd-modal.html is being rendered before the data for the repeater has been inserted.
Does anybody know how I could resize the modal to be the correct size for the body or delay the modal display until cmd-modal.htmlhas been correctly evaluated?
You can add style for width and height to the ai-dialog tag like this:
<ai-dialog style="width:600px; height: 350px;">
I think I found something similar to this when trying to add items of varying width to the dialog. The widths weren't know until after the dialog had been rendered. Well I think that is why!
In the end I added a CSS class on the ai-dialog element which included a general width setting and a media query.
...
width: 90vw;
#media (min-width: 46em) {
width: 44em;
}
....
I know I mixed vw and em measurements and there's probably better ways - but it works well in this app. I'm sure there's probably a "correct" Aurelia way to get the dialog to re-render but this is ample for our situation.
FWIW I also added a "margin-top: 4em !important" so that the dialog would appear just below the fixed header bar that Bootstrap was providing us.
In AngularJS 1.2, if I use a parent animation, the child animation doesn't work.
If I comment out app.animation('.parent', function () { .. }, then the child animation starts correctly.
How to get both parent and child animations working at the same time?
Plunker of my code
HTML:
<button ng-click="anim.toggleParent()">reveal parent</button>
<button ng-click="anim.toggleChild()">reveal child</button>
<div class="parent" ng-if="!anim.showParent">
<div class="child" ng-if="!anim.showChild">
</div>
</div>
JS:
app.animation('.parent', function () {
return {
// ...
};
});
// this doesn't work with parent animation =(
app.animation('.child', function () {
return {
// ...
};
});
Just insert ng-animate-children to the parent (Angular 1.2+).
<button ng-click="anim.toggleParent()">reveal parent</button>
<button ng-click="anim.toggleChild()">reveal child</button>
<div class="parent" ng-if="!anim.showParent" ng-animate-children>
<div class="child" ng-if="!anim.showChild">
</div>
</div>
Check the ngAnimate documentation:
Keep in mind that, by default, if an animation is running, any child elements cannot be animated until the parent element's animation has completed. This blocking feature can be overridden by placing the ng-animate-children attribute on a parent container tag.
<div class="slide-animation" ng-if="on" ng-animate-children>
<div class="fade-animation" ng-if="on">
<div class="explode-animation" ng-if="on">
...
</div>
</div>
</div>When the on expression value changes and an animation is triggered then each of the elements within will all animate without the block being applied to child elements.
Not sure whether this thread is closed. If so recommendation would be very helpful.
Facing the same issue here.
Angular animate has the below lines indicating that the child animations will not be triggered if parent has animation.
Not sure whether this is an issue or works as expected.
//skip the animation if animations are disabled, a parent is already being animated,
//the element is not currently attached to the document body or then completely close
//the animation if any matching animations are not found at all.
//NOTE: IE8 + IE9 should close properly (run closeAnimation()) in case a NO animation is not found.
if (animationsDisabled(element, parentElement) || matches.length === 0) {
domOperation();
closeAnimation();
return;
}
Have raised a thread in Angular google group referenced the issue back here.
Also not sure if this thread is closed, but you could always edit the angular-animate.js file. Function animationsDisabled is where angular looks for the parent element to see if it will allow the child to animate. At the top of this function I added a check to see if the parent element has a class of animation-override (can be whatever you define). This way you can override the default functionality when needed.
function animationsDisabled(element, parentElement) {
if (parentElement[0].classList.contains('animation-override')) return false;
if (rootAnimateState.disabled) return true;
if(isMatchingElement(element, $rootElement)) {
return rootAnimateState.disabled || rootAnimateState.running;
}
do {
//the element did not reach the root element which means that it
//is not apart of the DOM. Therefore there is no reason to do
//any animations on it
if(parentElement.length === 0) break;
var isRoot = isMatchingElement(parentElement, $rootElement);
var state = isRoot ? rootAnimateState : parentElement.data(NG_ANIMATE_STATE);
var result = state && (!!state.disabled || !!state.running);
if(isRoot || result) {
return result;
}
if(isRoot) return true;
}
while(parentElement = parentElement.parent());
return true;
}
}]);