Press anywhere outside the container to conceal it along with the button

Utilizing an Angular directive to hide div elements when the user interacts outside of them has been effective. However, there is a specific issue that arises when clicking outside of a div on a button that toggles the visibility of the div.

The 'angular-off-click' callback successfully hides the container as intended but triggers the toggle function associated with the button shortly after, reopening the div. This conflicting behavior led to the removal of the 'off-click-filter', which provided exceptions using CSS selectors to prevent this interaction, ultimately avoiding unnecessary clutter in the HTML markup.

The desired outcome is for the toggle button to not activate its handler when clicking outside of the container.

Update: The problem primarily occurs on touch devices due to the default 300ms delay. When clicking outside the div, the callback initiates to hide the container and then, after the delay, the toggle function executes, causing the container to reopen. Conversely, on desktops with mouse clicks, the toggle function activates before the callback runs.

// Angular App Code
var app = angular.module('myApp', ['offClick']);

app.controller('myAppController', ['$scope', '$timeout', function($scope,$timeout) {
  $scope.showContainer = false;

  $scope.toggleContainer = function() {
    $timeout(function() {
      $scope.showContainer = !$scope.showContainer;
    }, 300);
    
  };
  
  $scope.hideContainer = function(scope, event, p) {
    $scope.showContainer = false;
    console.log('event: ', event);
    console.log('scope: ', scope);
    console.log(p);
  };
}]);

// Off Click Directive Code
angular.module('offClick', [])
    .directive('offClick', ['$rootScope', '$parse', function ($rootScope, $parse) {
    var id = 0;
    var listeners = {};
    var touchMove = false;
    
    document.addEventListener("touchmove", offClickEventHandler, true);
    document.addEventListener("touchend", offClickEventHandler, true);
    document.addEventListener('click', offClickEventHandler, true);

    // Rest of the code remains the same...
/* Styles go here */

.container {
  background: blue;
  color: #fff;
  height: 300px;
  width: 300px;
}
<!DOCTYPE html>
<html>

  <head>
    <script src="https://code.angularjs.org/1.4.0/angular.js"></script>
    <link rel="stylesheet" href="style.css" />
    
    
  </head>

  <body data-ng-app="myApp">
    <h1>Hello Plunker!</h1>
    <div data-ng-controller="myAppController">
      
      <button data-ng-click="toggleContainer()">Toggle Container</button>
      
      <div class="container" data-ng-show="showContainer" data-off-click="hideContainer()" data-off-click-if="showContainer">
        This is the container
      </div>
    </div>
    
  </body>

</html>

http://jsbin.com/hibovu

Answer №1

One issue arises when the button is clicked, triggering both functions:

  1. hideContainer from the directive.
  2. toggleContainer from the click event (which displays the div again).

The solution

To resolve this problem, add event.stopPropagation(); before executing the hide callback.

How can you achieve this?

  1. Pass the event to the function
    data-off-click="hideContainer($event)"
    .
  2. Include the $event parameter in the definition of the hideContainer function within the $scope, like so:
    $scope.hideContainer = function($event)

Here's the complete code:

// Angular App Code
var app = angular.module('myApp', ['offClick']);

app.controller('myAppController', ['$scope', '$timeout', function($scope,$timeout) {
  $scope.showContainer = false;

  $scope.toggleContainer = function() {
    $timeout(function() {
      $scope.showContainer = !$scope.showContainer;
    }, 300);
  };

  $scope.hideContainer = function($event) {
    $event.stopPropagation();
    $timeout(function(){
      $scope.showContainer = false;  
    });
  };
}]);

// Off Click Directive Code
angular.module('offClick', [])
.directive('offClick', ['$rootScope', '$parse', function ($rootScope, $parse) {
  var id = 0;
  var listeners = {};
  // add variable to detect touch users moving..
  var touchMove = false;

  // Add event listeners to handle various events. Destop will ignore touch events
  document.addEventListener("touchmove", offClickEventHandler, true);
  document.addEventListener("touchend", offClickEventHandler, true);
  document.addEventListener('click', offClickEventHandler, true);

  function targetInFilter(target, elms) {
    if (!target || !elms) return false;
    var elmsLen = elms.length;
    for (var i = 0; i < elmsLen; ++i) {
      var currentElem = elms[i];
      var containsTarget = false;
      try {
        containsTarget = currentElem.contains(target);
      } catch (e) {
        // If the node is not an Element (e.g., an SVGElement) node.contains() throws Exception in IE,
        // see https://connect.microsoft.com/IE/feedback/details/780874/node-contains-is-incorrect
        // In this case we use compareDocumentPosition() instead.
        if (typeof currentElem.compareDocumentPosition !== 'undefined') {
          containsTarget = currentElem === target || Boolean(currentElem.compareDocumentPosition(target) & 16);
        }
      }

      if (containsTarget) {
        return true;
      }
    }
    return false;
  }

  function offClickEventHandler(event) {
    // If event is a touchmove adjust touchMove state
    if( event.type === 'touchmove' ){
      touchMove = true;
      // And end function
      return false;
    }
    // This will always fire on the touchend after the touchmove runs...
    if( touchMove ){
      // Reset touchmove to false
      touchMove = false;
      // And end function
      return false;
    }
    var target = event.target || event.srcElement;
    angular.forEach(listeners, function (listener, i) {
      if (!(listener.elm.contains(target) || targetInFilter(target, listener.offClickFilter))) {
        //$rootScope.$evalAsync(function () {
        listener.cb(listener.scope, {
          $event: event
        });
        //});
      }
    });
  }

  return {
    restrict: 'A',
    compile: function ($element, attr) {
      var fn = $parse(attr.offClick);
      return function (scope, element) {
        var elmId = id++;
        var offClickFilter;
        var removeWatcher;

        offClickFilter = document.querySelectorAll(scope.$eval(attr.offClickFilter));

        if (attr.offClickIf) {
          removeWatcher = $rootScope.$watch(function () {
            return $parse(attr.offClickIf)(scope);
          }, function (newVal) {
            if (newVal) {
              on();
            } else if (!newVal) {
              off();
            }
          });
        } else {
          on();
        }

        attr.$observe('offClickFilter', function (value) {
          offClickFilter = document.querySelectorAll(scope.$eval(value));
        });

        scope.$on('$destroy', function () {
          off();
          if (removeWatcher) {
            removeWatcher();
          }
          element = null;
        });

        function on() {
          listeners[elmId] = {
            elm: element[0],
            cb: fn,
            scope: scope,
            offClickFilter: offClickFilter
          };
        }

        function off() {
          listeners[elmId] = null;
          delete listeners[elmId];
        }
      };
    }
  };
}]);
.container {
  background: blue;
  color: #fff;
  height: 300px;
  width: 300px;
}
<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
  </head>
  <body data-ng-app="myApp">
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular.min.js"></script>
    <h1>Hello Plunker!</h1>
    <div data-ng-controller="myAppController">
      <button data-ng-click="toggleContainer()">Toggle Container</button>
      <div class="container" data-ng-show="showContainer" data-off-click="hideContainer($event)" data-off-click-if="showContainer">
        This is the container
      </div>
    </div>
  </body>
</html>

http://jsbin.com/hibovu/3/edit?html,css,js

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

HTML scroll bar functioning intermittently

Here is my code snippet. table#projectTable > tbody , table#productTable > tbody{ height: 300px; overflow: auto; } The scrollbar is functional when clicking the top part, but not the bottom. It's quite odd. Upon usin ...

The incorrect indentation issue occurs when the text wraps around

I am looking to automatically indent wrapped contents based on the first line. To achieve this, I implemented the following HTML and CSS code: li { padding-left: 10px; text-indent: 10px; } .Slides { width: 20em; //Display wrap-around } <div cla ...

Developing an exportable value service type in TypeScript for AngularJS

I have been working on creating a valuable service using typescript that involves a basic switch case statement based on values from the collection provided below [{ book_id: 1, year_published: 2000 }, { book_id: 2, year_publish ...

Saving data in a Spring MVC Angular JS application may involve storing null values in the database

angular js code <body data-ng-app="myApp" data-ng-controller="UserController as userBean"> <form method="post" action="register" name="myForm"> <div class="form-group col-lg-7" > <label for="username" class="control-label">First ...

Await the sorting of intervals

Looking for a solution to re-execute a hand-made for loop with a delay of 10 seconds after it finishes indefinitely. I've attempted some code, but it keeps re-executing every 10 seconds rather than waiting for the loop to finish before starting again. ...

Error encountered in NodeJS: Promise TypeError - res.json is not a function while handling pre-signed URLs. This results in

I'm having trouble generating a list of pre-signed URLs for files in my private S3 bucket. Despite researching similar issues on multiple forums, I can't seem to resolve the error with the res.json function in my code. Can someone please help me ...

Receive real-time updates on incoming messages in your inbox

I'm seeking advice on implementing live update messages in my code. Here's what I have so far: <script> function fetch_messages(){ var user_id = "1" // example id $.ajax({ url: "do_fetch.php", t ...

Scroll within the inner container

I am currently working on creating panels, each with a header and content. My goal is to allow scrolling only within the content area. Here is the CSS I have been using: .content-div{ float:left; height: 400px; overflow-x:hidden; overflow-y:hidden; ...

Reading the final element in the series with an IF statement

Something strange is happening with my code. I have a variable called racks_value that gets updated based on calculations performed on the page. Despite manually setting racks_value to 2 and confirming it with a console log, after running a series of IF st ...

Tips for looping through a JSON object?

Similar Question: How to extract a specific value from a nested JSON data structure? I am looking to loop through a two-dimensional JSON object, whereas I already know how to do so for a one-dimensional JSON object. for (var key in data) { alert(data ...

Nested routing in Nextjs is encountering issues when implemented with a specific file

I'm struggling with setting up routes in Next.js. When I create the path "/app/[locale]/admin/page.tsx," I can access http://url/admin/ without any issues. However, when I try to set up "/app/[locale]/admin/login.tsx," I encounter an error and cannot ...

Angular log out function to automatically close pop-up windows

Within my application, there is a page where users can open a popup window. When the user clicks on logout, it should close the popup window. To achieve this, I have used a static variable to store the popup window reference in the Global.ts class. public ...

Angular: Leveraging $http within a service

I've developed an angular Provider that exposes a getSession method which needs to be resolved before entering a specific route: var serviceId = 'session'; angular.module("app").provider(serviceId, sessionProvider); function sessionProvid ...

What's the best way to add row numbers within ajax requests?

I wrote a function that retrieves values from a form using jQuery's AJAX method: function getvalues(){ var sendid = $('#id').val(); $.ajax({ type: "POST", url: "ready.php", data: {sendid} }).done(function( result ) { $("#msg").html( "worked ...

Please explain the concept of the Node.js event loop

I've spent countless hours poring over guides and resources on the event loop, yet I still can't grasp its essence. It's common knowledge that the libuv library is responsible for implementing the event loop, but what exactly is this entity ...

Is there a way to dynamically switch between AngularJS modules based on a configuration change?

Understanding the inner workings of Dependency Injection with AngularJS modules has sparked an idea in my mind. I thought about leveraging this concept to switch between different modules based on the environment of the application. For instance, I aim to ...

Arrange data into columns on a website

In my project, I'm exploring the challenge of creating a square 2 x 2 grid alongside a rectangular column on the right-hand side. While attempting to implement a grid system for the 2 x 2 layout, I encountered issues with the alignment of the rectang ...

Save user sessions in a database using node.js, express, and mongoose-auth

I have a question about authentication and sessions in node.js. So, I've set up authentication using express.js and mongoose-auth with mongodb: app.use(express.cookieParser()); app.use(express.session({ secret: 'esoognom'})); app.use(auth. ...

Set up a WhatsApp web bot on the Heroku platform

I am currently in the process of developing a WhatsApp bot using the node library whatsapp-web.js. Once I finish writing the script, it appears something like this (I have provided an overview of the original script) - index.js const {Client, LocalAuth, M ...

Position the responsive carousel in the center

My carousel appears misaligned on smaller screens, but displays correctly on laptops and desktops. I've attached a screenshot and my CSS code below for reference. Thank you in advance! .MultiCarousel { margin-right:15px; margin-left: 15px;float: lef ...