The Problem

In AngularJS and Angular, updating the UI based on external events (like document.click) can be tricky when those events aren’t part of the framework’s internal lifecycle. This is particularly relevant when integrating third-party libraries or manually binding DOM events via jQuery or vanilla JavaScript.

Scenario

$('html').click(function(e) {
     scope.close();
   });

   scope.close = function() {
     scope.isOpen = false;
   };

   <div ng-show="isOpen">My Div</div>

In this scenario, the close() method sets isOpen to false. However, AngularJS doesn’t update the view because the change occurs outside its digest cycle.

Why Doesn’t This Work?

AngularJS relies on its digest cycle to detect and propagate changes in the scope. This cycle is triggered by Angular-aware mechanisms like ng-click, $http, $timeout, etc.

When you use a raw jQuery event handler ($(‘html’).click(…)), AngularJS isn’t aware of any model changes made in the handler, so the view doesn’t update even though the model has.

The Correct Approach

You must notify AngularJS (or Angular) that a change has occurred outside its normal detection mechanisms.

AngularJS

Use $scope.$apply() or $scope.$evalAsync() to manually trigger the digest cycle:

$('html').click(function(e) {
     scope.$apply(function() {
       scope.close();
     });
   });

Angular (2+)

Use @HostListener or NgZone to ensure changes are registered by Angular’s change detection system.

Solution Examples

AngularJS Example

angular.module('myApp', [])
     .controller('MainCtrl', function($scope) {
       $scope.isOpen = false;
    
       $scope.close = function() {
         $scope.isOpen = false;
       };
    
       $('html').click(function() {
         $scope.$apply(function() {
           $scope.close();
         });
       });
     });
  
   <div ng-app="myApp" ng-controller="MainCtrl">
     <div ng-show="isOpen">My Div</div>
   </div>

Angular (2+) Example Using NgZone

import { Component, NgZone } from '@angular/core';

   @Component({
     selector: 'app-root',
     template: `<div *ngIf="isOpen">My Div</div>`
   })
   export class AppComponent {
     isOpen = true;

     constructor(private ngZone: NgZone) {
       document.addEventListener('click', () => {
         this.ngZone.run(() => {
           this.isOpen = false;
         });
       });
     }
   }

Note: Direct DOM access like document.addEventListener is not recommended in Angular. See the section below for a better approach using @HostListener.

Angular (2+) Best Practice: Using @HostListener

This is the preferred Angular-native way to listen for global events like clicks on the document.

import { Component, HostListener } from '@angular/core';
  
   @Component({
     selector: 'app-root',
     template: `
       <div *ngIf="isOpen" class="popup">
         My Div
       </div>
     `,
     styles: [`.popup { border: 1px solid black; padding: 10px; }`]
   })
   export class AppComponent {
     isOpen = true;
  
     close() {
       this.isOpen = false;
     }
  
     @HostListener('document:click', ['$event'])
     onDocumentClick(event: MouseEvent) {
       this.close();
     }
   }

Why This Works

  • @HostListener(‘document:click’) tells Angular to bind to the global document click event.
  • Changes made in the onDocumentClick() method are automatically detected by Angular’s change detection system—no need to manually invoke NgZone.run() or ChangeDetectorRef.detectChanges().

Need Help With Angular Development?

Work with our skilled Angular developers to accelerate your project and boost its performance.

Hire Angular Developers

Support On Demand!

Related Q&A