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.
Related
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.
I've got a line of text right now, and when that line of text is being overflown something gets set to true which let's me load in a tooltip! Code below:
Template
<div>
<p #tooltip [tooltip]="/* ShowToolTipSomeHow? ? name : null */" delay="300">{{name}}</p>
</div>
This is where it should check if it should show the tooltip or not. As you can see it should somehow detect if the tooltip should be shown or not, I have no idea how and that's my question right now.
Component
#ViewChildren('tooltip') private tooltips!: QueryList<ElementRef>;
ngAfterViewInit(): void {
this.tooltips.changes.subscribe((tts: QueryList<ElementRef>) => {
tts.forEach((tooltip, index) => {
this.checkTooltipTruncated(tooltip);
});
});
}
private checkTooltipTruncated(tooltip: ElementRef) {
// Checks if the text has overflown
const truncated = this.isTextTruncated(tooltip);
if (truncated) {
// Change the ShowToolTipSomehow? value?
}
}
In the component it somehow changes some value that the tooltip can detect so that it can update itself to hide the tooltip. The additional problem is that it's not 1 tooltip to change, but infinite tooltips (so basically 1 or more).
My question is, how would I do this because I'm pretty stuck.
Create an array that holds boolean values and using the tooltip elements index set the value in the array.
ts:
#ViewChildren('tooltip') private tooltips!: QueryList<ElementRef>;
tooltipsVisible: boolean[];
ngAfterViewInit(): void {
if (!this.tooltipsVisible) {
this.tooltipsVisible = new Array(this.tooltips.length).fill(false);
}
this.tooltips.changes.subscribe((tts: QueryList<ElementRef>) => {
tts.forEach((tooltip, index) => {
this.checkTooltipTruncated(tooltip, index);
});
});
}
private checkTooltipTruncated(tooltip: ElementRef, index) {
// Checks if the text has overflown
const truncated = this.isTextTruncated(tooltip);
if (truncated) {
this.tooltipsVisible[index] = true;
// Change the ShowToolTipSomehow? value?
}
}
html:
<div>
<p #tooltip [tooltip]="tooltipsVisible[i] ? ...." delay="300">{{name}}</p>
</div>
You should create your p elements using *ngFor you can have the index available in your template...
my problem is that i cant acces to a particular DOM element and their properties when the element is a children of a *ngIf container.
My case is: I a have a mat-table inside a div, the div have the *ngIf directive, and then i try to call mytable.renderRows() when my datasource changed, but i got an undefined value. I see this problem happens when the element is inside the ngIf directive, in other case i can access without problem.
<div *ngIf="!hasPermission" >
<table mat-table #myTable [dataSource]="myDataSource">
and i have this on the .ts file:
export class MyComponent {
hasPermission = true
#ViewChild('myTable',{static:true}) myTable: MatTable<any>;
constructor(){
if(checkSomething == true){
this.hasPermission = false
this.myFunctionIfNotHavePermsissions()
}
}
myFunctionIfNotHavePermsissions(){
this.myTable.renderRows();
// console.log(this.myTable); *NOTE: This output: undefined*
}
}
For the moment, I fixed this problem, hidding the div using css, but i think this not the best solution, thanks in advance for your comments.
<div *ngIf="!hasPermission" >
to
<div [ngClass]="{ 'nodisplay': !hasPermission}" >
.nodisplay{display:none!important;}
I may don't know the actual reason behind it, but I think angular need a little time to first render whatever inside the ngIf element and then make available to DOM.
You can fix your issue by changing static to false here
#ViewChild('myTable', {static: false}) myTable: MatTable<any>;
and calling this.myFunctionIfNotHavePermsissions() inside a setTimeout
constructor(){
if(checkSomething == true){
this.hasPermission = false
setTimeout(()=> this.myFunctionIfNotHavePermsissions());
}
}
in constructor you the template is not ready and mat-table is not rendered.
add your logic in ngOnInit
export class MyComponent implements OnInit {
hasPermission = true
#ViewChild('myTable',{static:true}) myTable: MatTable<any>;
constructor() {}
ngOnInit() {
if(checkSomething == true){
this.hasPermission = false
this.myFunctionIfNotHavePermsissions()
}
}
myFunctionIfNotHavePermsissions(){
this.myTable.renderRows();
// console.log(this.myTable); *NOTE: This output: undefined*
}
}
js
I have a method to enable and disable print option which has element id printReport_AId, so the existence of element is dynamic based on selection.
I have a code to get
document.getElementById('printReport_AId')
this returning null everytime , i guess we need something like windows onload or interval method , not sure how to implement in vue.js
I have attached the code below
<template>
<div id = "intro" style = "text-align:center;">
<div class="printIconSection" v-for="report in reports" :key="report.property" >
<div class="Icon" id="printReport_AId" v-if="isOverview">
<font-awesome-icon :icon="report.icon" #click="printWindow()"/>
</div>
</div>
<div class="logo"v-bind:class="{ 'non-print-css' : noprint }">
</div>
</div>
</template>
<script type = "text/javascript">
var vue_det = new Vue({
el: '#intro',
data: {
timestamp: ''
},
created() {
},
methods: {
printWindow() { },
mounted() {
// window.addEventListener('print', this.noprint);
},
computed(){
noprint() {
const printicon = document.getElementById('printReport_AId');
if (printicon != 'null') {
return true;
}
return false;
},
},
}
}
});
</script>
<style>
#media print {
.non-print-css {
display: none;
}
}
</style>
i just tried window.addEventListener that didnt worked for computed .
I need to get the element id dynamically.
whenever i enable the print element , the element id should not be null. similarly,
whenever i dont enable the print element , the element id should be null
From your code, it seems that you're inserting the ID in each iteration of the v-for loop. This means that the ID will not be unique in the event that there is more than one entry in your reports array. Based on your code, this is what I understand that you want to achieve:
each report will contain an isOverview property. It is probably a boolean, so it has a value of true or false
when isOverview is true, the print icon will be shown
when isOverview is false, the print icon will not be shown
based on whether the print icon(s) are shown, you want to toggle the .logo class
This comes the tricky part, as you have multiple isOverview to evaluate. Do you want to toggle the .logo element when:
all reports has isOverview property set to true, or
one or more reports has isOverview property set to true?
If you want to show the .logo element when all reports has isOverview set to true, then you can do this:
computed() {
noprint() {
return this.reports.every(report => report.isOverview);
}
}
Otherwise, if you only want one or more report to have isOverview as true, you can do this instead:
computed() {
noprint() {
return this.reports.some(report => report.isOverview);
}
}
computed would not watch for document methods, because it is not wrapped with vue's setters and getters.
What you can do is mark your div with ref and then access it via this.$refs
<div class="printIconSection" v-for="(report, index) in reports" :key="report.property" >
<div class="Icon" :ref="'printReport_AId' + index" v-if="isOverview">
<font-awesome-icon :icon="report.icon" #click="printWindow()"/>
</div>
</div>
methods(){
noprint(index) {
const printicon = this.$refs['printReport_AId' + index];
if (printicon != null) {
return true;
}
return false;
},
}
When you need to achieve noprint you just call noprint(index)
For each element of array noprint would be different
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;
}
}]);