Primeng: OverlayPanel breaks on multiple clicks

Created on 12 May 2020  路  18Comments  路  Source: primefaces/primeng

I'm submitting a ... (check one with "x")

[x] bug report => Search github for a similar issue or PR before submitting
[ ] feature request => Please check if request is not on the roadmap already https://github.com/primefaces/primeng/wiki/Roadmap
[ ] support request => Please do not submit support request here, instead see http://forum.primefaces.org/viewforum.php?f=35

Plunkr Case (Bug Reports)
https://stackblitz.com/edit/github-6dh6gj-rwjkmq

Current behavior

  1. mouseover / mouseleave case
    When moving over button and leaving it few times in a row (can be quite fast) there are errors thrown in console.
  2. onclick case
    Click multiple times on button in example here https://www.primefaces.org/primeng/showcase/#/overlaypanel
    Check console for errors

However component still works but errors are thrown which is problematic in application with dedicated error handling.
Thrown errors:
ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'ngIf: true'. Current value: 'ngIf: false'.
ERROR TypeError: Cannot read property 'offsetHeight' of null

Expected behavior
No errors are thrown in console

Minimal reproduction of the problem with instructions
Case 1.

  1. Go to https://stackblitz.com/edit/github-6dh6gj-rwjkmq
  2. Open browser console
  3. Move mouse over the button with overlay panel few times (in and out)
  4. Errors are thrown

Case 2.

  1. Go to https://www.primefaces.org/primeng/showcase/#/overlaypanel
  2. Open browser console
  3. Click fast multiple times on button to show tooltip
  4. Errors are thrown

What is the motivation / use case for changing the behavior?
Usage ofoverlay panel shouldn't produce errors

  • Angular version: 9.1.4

  • PrimeNG version: 9.0.6

  • Browser: [all]

Most helpful comment

Issue reproduction
Angular 11 PrimeNg 11.4.0

The link show the behavior. Open the console and hover the text. Should see:

Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'ngIf: true'. Current value: 'ngIf: false'.
at viewDebugError (vendor.js:46667)
at expressionChangedAfterItHasBeenCheckedError (vendor.js:46655)
at checkBindingNoChanges (vendor.js:46865)
at checkNoChangesNodeInline (vendor.js:55242)
at checkNoChangesNode (vendor.js:55231)
at debugCheckNoChangesNode (vendor.js:55828)
at debugCheckDirectivesFn (vendor.js:55760)
at Object.updateDirectives (default~pages-_searchs-interview-test-interview-test-module-ngfactory~pages-member-member-module-ngfactory.js:1317)
at Object.debugUpdateDirectives [as updateDirectives] (vendor.js:55753)
at checkNoChangesView (vendor.js:55130)

All 18 comments

Found fix editing node_module files. The problem seems to be that this.render = false is called in a wrong place. When new cycle begins, the old one is not finished yet then old cycle changes this.render value when new cycle began with different value which causes angular crash with ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'ngIf: true'. Current value: 'ngIf: false'

    onAnimationEnd(event) {
        switch (event.toState) {
            case 'void':
                if (this.destroyCallback) {
                    this.destroyCallback();
                    this.destroyCallback = null;
                }
                break;
            case 'close':
                this.onContainerDestroy();
                this.onHide.emit({});
                // REMOVED: this.render =false;
                break;
        }
    }
    hide() {
        // ADDED
        this.render = false;
        this.overlayVisible = false;
    }

In case anyone face this problem i have temporary solved it by injecting service to module constructor where i use OverlayPanel. Service looks like:

@Injectable()
export class PrimeNGCorrectionService {

  init() {
    this.installOverlayPanelFix();
  }
  private installOverlayPanelFix() {
    OverlayPanel.prototype.hide = function (this: OverlayPanel) {
      this.render = false;
      this.overlayVisible = false;
    };

    const onAnimationEndSource: Function = OverlayPanel.prototype.onAnimationEnd;
    OverlayPanel.prototype.onAnimationEnd = function (this: OverlayPanel, event: any) {
      onAnimationEndSource.call(this, event);
      if (event.toState === "close") {
        this.render = true;
      }
    };
  }
}

Just to add to this a more complete stacktrace:

image

core.js:6228 ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'true'. Current value: 'false'.
    at throwErrorIfNoChangesMode (core.js:8147)
    at bindingUpdated (core.js:20136)
    at Module.傻傻property (core.js:21163)
    at OverlayPanel_Template (primeng-overlaypanel.js:221)
    at executeTemplate (core.js:12156)
    at refreshView (core.js:11995)
    at refreshComponent (core.js:13445)
    at refreshChildComponents (core.js:11716)
    at refreshView (core.js:12051)
    at refreshEmbeddedViews (core.js:13391)
defaultErrorLogger @ core.js:6228
handleError @ core.js:6281
(anonymous) @ core.js:43145
invoke @ zone-evergreen.js:364
run @ zone-evergreen.js:123
runOutsideAngular @ core.js:41488
tick @ core.js:43142
(anonymous) @ core.js:42975
invoke @ zone-evergreen.js:364
onInvoke @ core.js:41654
invoke @ zone-evergreen.js:363
run @ zone-evergreen.js:123
run @ core.js:41427
next @ core.js:42971
schedulerFn @ core.js:37119
__tryOrUnsub @ Subscriber.js:183
next @ Subscriber.js:122
_next @ Subscriber.js:72
next @ Subscriber.js:49
next @ Subject.js:39
emit @ core.js:37079
checkStable @ core.js:41566
onLeave @ core.js:41729
onInvokeTask @ core.js:41638
invokeTask @ zone-evergreen.js:398
runTask @ zone-evergreen.js:167
invokeTask @ zone-evergreen.js:480
invokeTask @ zone-evergreen.js:1621
globalZoneAwareCallback @ zone-evergreen.js:1647
core.js:6228 ERROR TypeError: Cannot read property 'offsetHeight' of null
    at Function.absolutePosition (primeng-dom.js:113)
    at OverlayPanel.align (primeng-overlaypanel.js:143)
    at OverlayPanel.onAnimationStart (primeng-overlaypanel.js:157)
    at OverlayPanel_div_0_Template_div_animation_animation_start_0_listener (primeng-overlaypanel.js:28)
    at executeListenerWithErrorHandling (core.js:21806)
    at wrapListenerIn_markDirtyAndPreventDefault (core.js:21848)
    at animations.js:394
    at Array.forEach (<anonymous>)
    at animations.js:388
    at ZoneDelegate.invoke (zone-evergreen.js:364)

same issue

In case anyone face this problem i have temporary solved it by injecting service to module constructor where i use OverlayPanel. Service looks like:

@Injectable()
export class PrimeNGCorrectionService {

  init() {
    this.installOverlayPanelFix();
  }
  private installOverlayPanelFix() {
    OverlayPanel.prototype.hide = function (this: OverlayPanel) {
      this.render = false;
      this.overlayVisible = false;
    };

    const onAnimationEndSource: Function = OverlayPanel.prototype.onAnimationEnd;
    OverlayPanel.prototype.onAnimationEnd = function (this: OverlayPanel, event: any) {
      onAnimationEndSource.call(this, event);
      if (event.toState === "close") {
        this.render = true;
      }
    };
  }
}

Thank you for your fix.
Even with it, I was still getting crashes but it gives me a good starting point. Please find below what I added to make it works for me.

`/* modules */
import { Injectable } from '@angular/core';

/* feature modules */
import { OverlayPanel } from 'primeng';

@Injectable({
providedIn: 'root',
})
export class PrimeNgOverlayPanelFixService {

constructor() {
    this.installOverlayPanelFix();
}

private installOverlayPanelFix() {

    const onAlignSource: Function = OverlayPanel.prototype.align;
    OverlayPanel.prototype.align = function (this: OverlayPanel) {
        var _this = this;
        if (_this.container != null && _this.target != null) {
            onAlignSource.call(_this);
       }
    };

    const onBindDocumentClickListenerSource: Function = OverlayPanel.prototype.bindDocumentClickListener;
    OverlayPanel.prototype.bindDocumentClickListener = function (this: OverlayPanel) {
        var _this = this;
        if (_this.container != null && _this.target != null) {
            onBindDocumentClickListenerSource.call(_this);
        }
    };

    OverlayPanel.prototype.hide = function (this: OverlayPanel) {
        var _this = this;
        _this.render = false;
        _this.overlayVisible = false;
    };

    const onAnimationEndSource: Function = OverlayPanel.prototype.onAnimationEnd;
    OverlayPanel.prototype.onAnimationEnd = function (this: OverlayPanel, event: any) {
        var _this = this;
        onAnimationEndSource.call(_this, event);
        if (event.toState === "close") {
            _this.render = true;
        }
    };
}

}`

In case anyone face this problem i have temporary solved it by injecting service to module constructor where i use OverlayPanel. Service looks like:

@Injectable()
export class PrimeNGCorrectionService {

  init() {
    this.installOverlayPanelFix();
  }
  private installOverlayPanelFix() {
    OverlayPanel.prototype.hide = function (this: OverlayPanel) {
      this.render = false;
      this.overlayVisible = false;
    };

    const onAnimationEndSource: Function = OverlayPanel.prototype.onAnimationEnd;
    OverlayPanel.prototype.onAnimationEnd = function (this: OverlayPanel, event: any) {
      onAnimationEndSource.call(this, event);
      if (event.toState === "close") {
        this.render = true;
      }
    };
  }
}

It fixes the issue but it breaks the onHide event, it doesn't get triggered anymore. In my code I have something like this:
...
(onHide)="resetAddFilterForm()">
...

@tadamczak after adding your fix "resetAddFilterForm" is not triggered anymore when the OverlayPanel is closed.

In case anyone face this problem i have temporary solved it by injecting service to module constructor where i use OverlayPanel. Service looks like:

@Injectable()
export class PrimeNGCorrectionService {

  init() {
    this.installOverlayPanelFix();
  }
  private installOverlayPanelFix() {
    OverlayPanel.prototype.hide = function (this: OverlayPanel) {
      this.render = false;
      this.overlayVisible = false;
    };

    const onAnimationEndSource: Function = OverlayPanel.prototype.onAnimationEnd;
    OverlayPanel.prototype.onAnimationEnd = function (this: OverlayPanel, event: any) {
      onAnimationEndSource.call(this, event);
      if (event.toState === "close") {
        this.render = true;
      }
    };
  }
}

It fixes the issue but it breaks the onHide event, it doesn't get triggered anymore. In my code I have something like this:
...
(onHide)="resetAddFilterForm()">
...

@tadamczak after adding your fix "resetAddFilterForm" is not triggered anymore when the OverlayPanel is closed.

You are right @tadamczak I spotted it too. I was planning to work on it after my holidays. But feel free to solve it ;)

Here what I did and it seems to solve the issue breaking the onHide event.

const onAnimationEndSource: Function = OverlayPanel.prototype.onAnimationEnd; OverlayPanel.prototype.onAnimationEnd = function (this: OverlayPanel, event: any) { const _this = this; onAnimationEndSource.call(_this, event); if (event.toState === 'close') { /* this.render = true;*/ (<any>_this).cd.detectChanges(); } };

when i use the mouseover event, it occurs the same error. in my case, I use the OverlayPanel in a list, using the mouseenter event to open, hovering the mouse over the label to open the list, when I release the mouse from the top, it occurs the ExpressionChangedAfterItHasBeenCheckedError error.

if i used the service you passed above, would it solve the problem?

image

when i use the mouseover event, it occurs the same error. in my case, I use the OverlayPanel in a list, using the mouseenter event to open, hovering the mouse over the label to open the list, when I release the mouse from the top, it occurs the ExpressionChangedAfterItHasBeenCheckedError error.

if i used the service you passed above, would it solve the problem?

image

It should. The ExpressionChangedAfterItHasBeenCheckedError error occurs because the internal variable "render" is set by the onAnimationEnd method to true value after being checked by angular with false value

Hi @tadamczak ,

I have the same error :
ERROR TypeError: Cannot read property 'offsetHeight' of null
when using the OverlayPanel on mouseenter and mouseleave event when I was switching fast between cells of my table.

I managed to avoid throwing this error by using the following code:

// in your your .ts file
showOverlayPanel(event) {
    if (this.overlayPanel.target === null || this.overlayPanel.target === undefined) {
        this.overlayPanel.show(event);
    }
}
<!-- in your your .html file -->


md5-28c60b97228b0c1b95801685e4f995fe


Hope it will help

Unable to replicate with PrimeNG 11.0.0-rc.1, if the issue persists please create a new ticket with a test case reproducing the issue e.g. stackblitz or a github repo and it will be reviewed by our team once again.

Still happens for me, seems to me the listener that checks if the click happened outside fires way too fast, and thus the second time around the dialog never opens. I copied the raw source for now and modified the onAnimationStart event to the following:

(added the setTimeout)

onAnimationStart(event: AnimationEvent) {
    if (event.toState === 'open') {
      setTimeout(() => {
        this.container = event.element;
        this.onShow.emit(null);
        this.appendContainer();
        this.align();
        this.bindDocumentClickListener();
        this.bindDocumentResizeListener();
        this.bindScrollListener();

        if (this.focusOnShow) {
          this.focus();
        }
      });
    }
  }

That made it work for me.

Just a note: still occurs (2021-1-19; with primeng 9.0.5)

Same issue version 11.4.0

Unable to replicate with PrimeNG 11.0.0-rc.1, if the issue persists please create a new ticket with a test case reproducing the issue e.g. stackblitz or a github repo and it will be reviewed by our team once again.

Please reopen this issue

Issue reproduction
Angular 11 PrimeNg 11.4.0

The link show the behavior. Open the console and hover the text. Should see:

Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'ngIf: true'. Current value: 'ngIf: false'.
at viewDebugError (vendor.js:46667)
at expressionChangedAfterItHasBeenCheckedError (vendor.js:46655)
at checkBindingNoChanges (vendor.js:46865)
at checkNoChangesNodeInline (vendor.js:55242)
at checkNoChangesNode (vendor.js:55231)
at debugCheckNoChangesNode (vendor.js:55828)
at debugCheckDirectivesFn (vendor.js:55760)
at Object.updateDirectives (default~pages-_searchs-interview-test-interview-test-module-ngfactory~pages-member-member-module-ngfactory.js:1317)
at Object.debugUpdateDirectives [as updateDirectives] (vendor.js:55753)
at checkNoChangesView (vendor.js:55130)

In case anyone face this problem i have temporary solved it by injecting service to module constructor where i use OverlayPanel. Service looks like:

@Injectable()
export class PrimeNGCorrectionService {

  init() {
    this.installOverlayPanelFix();
  }
  private installOverlayPanelFix() {
    OverlayPanel.prototype.hide = function (this: OverlayPanel) {
      this.render = false;
      this.overlayVisible = false;
    };

    const onAnimationEndSource: Function = OverlayPanel.prototype.onAnimationEnd;
    OverlayPanel.prototype.onAnimationEnd = function (this: OverlayPanel, event: any) {
      onAnimationEndSource.call(this, event);
      if (event.toState === "close") {
        this.render = true;
      }
    };
  }
}

I managed to fix the onHide event by adding the original lines of code in the rewritten hide method (primeng 11.4.0):

@Injectable()
export class PrimeNGCorrectionService {

  init() {
    this.installOverlayPanelFix();
  }
  private installOverlayPanelFix() {
    OverlayPanel.prototype.hide = function (this: OverlayPanel) {
      this.overlayVisible = false;
      this.cd.markForCheck();
      this.render = false;
      this.overlayVisible = false;
    };

    const onAnimationEndSource: Function = OverlayPanel.prototype.onAnimationEnd;
    OverlayPanel.prototype.onAnimationEnd = function (this: OverlayPanel, event: any) {
      onAnimationEndSource.call(this, event);
      if (event.toState === "close") {
        this.render = true;
      }
    };
  }
}
Was this page helpful?
0 / 5 - 0 ratings