$event.stopPropagation doesn't play nice with Bootstrap data-toggle - angularjs

I have a list of elements, each containing a ng-click. Nested inside each element is a div that should toggle a Bootstrap modal.
I have added the $event.stopPropagation to the nested div because i don't want the ng-click event from the parent element to fire, but this causes the modal to not begin displayed.
<div id="segment{{segment.Id}}" class="message" ng-class="{activeSegment: segment.Selected}" ng-click="!segmentIsLoaded || selectSegment(segment)">
<div>
<div class="pull-left">
<span>ID: {{segment.Id}}</span>
<span>{{segment.Name}} </span>
<i class="fa fa-info-circle fa-lg" data-toggle="tooltip" data-original-title="{{segment.Description}}" tooltip></i><br />
<small>{{formatJsonDate(segment.Updated)}}</small>
</div>
<div class="btn btn-danger pull-right" data-toggle="modal" data-target="#deleteSegmentModal" ng-click="$event.stopPropagation();">
<span><i class="fa fa-trash-o" aria-hidden="true"></i></span>
</div>
</div>
</div>
Any known work-arounds for this?
I should perhaps mention that the stopPropagation() works as far as preventing the click event from firing. My issue is with the bootstrap modal not being activated through data-toggle attribute

Have you tried using a directive
<a ng-click='expression' stop-event='click'>
.directive('stopEvent', function () {
return {
restrict: 'A',
link: function (scope, element, attr) {
if(attr && attr.stopEvent)
element.bind(attr.stopEvent, function (e) {
e.stopPropagation();
});
}
};
});
check SO link How can I make an AngularJS directive to stopPropagation?

Related

Data attribute to call function when modal dialog is closed

I'm using Bootstrap in conjunction with AngularJS to open modal dialogs. To activate a modal without writing JavaScript code, I use the data attributes as described in the documentation. This is a very convenient way, since I do not need to show/hide the dialog manually.
<button type="button" data-toggle="modal" data-target="#myModal">Launch modal</button>
Now I would like to call a method when the modal dialog is closed. With an explicit close button, this is no problem. However, when the user clicks outside of the dialog or presses the Esc key, I cannot trigger any function explicitly.
I know that I can use jQuery or Angular's $uibModal to listen for a dismiss event, but this makes the entire project more complex. I'd rather have it all in one place. I do not want to mix things up, so using jQuery within my AngularJS project is not an option. The solution I'm stuck with right now, is using $uibModal to open() the dialog manually and catching the result to handle user-invoked dismiss.
My question:
How can I call a function when a modal dialog is closed without introducing too much clutter?
What I have in mind looks like this (imaginary data-dismiss-callback):
<button type="button" data-toggle="modal"
data-target="#myModal"
data-dismiss-callback="handleCloseEvent()">Launch modal</button>
As we want to attach a specified behavior (custom callback) to the target modal using the button that opens it, then directive is the best candidate who can help us with achieving this.
We will be listening to show.bs.modal and hide.bs.modal/hidden.bs.modal events: the first one will help us to determine if the modal was opened using the corresponding button and the second one is the place where we want to call the passed callback function.
Here is a working example of modalDismissCallback directive (due to normalization, we can't name it dataDismissCallback):
angular.module('myDemoApp', [])
.controller('myCtrl', [function () {
var ctrl = this;
ctrl.testVar = 2;
ctrl.onModalDismiss = onModalDismiss;
function onModalDismiss(a, e) {
console.log(arguments);
}
return ctrl;
}])
.directive('modalDismissCallback', [function modalDismissCallback() {
return {
restrict: 'A',
scope: {
modalDismissCallback: '&'
},
link: function (scope, element) {
var modal = angular.element(element.data('target'));
modal.on('show.bs.modal', onShow);
modal.on('hide.bs.modal', onHide);
scope.$on('$destroy', function () {
modal.off('show.bs.modal', onShow);
modal.off('hide.bs.modal', onHide);
});
var shouldCall = false;
function onShow(e) {
shouldCall = e.relatedTarget === element[0];
}
function onHide(e) {
if (angular.isFunction(scope.modalDismissCallback) && shouldCall) {
scope.$event = e;
scope.$applyAsync(function () {
scope.modalDismissCallback.apply(this, arguments);
});
}
}
}
}
}]);
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta.3/css/bootstrap.min.css">
<body ng-app="myDemoApp">
<div ng-controller="myCtrl as $ctrl">
<button type="button" class="btn btn-default"
data-toggle="modal"
data-target="#myModal"
modal-dismiss-callback="$ctrl.onModalDismiss($ctrl.testVar, $event)">Launch modal
</button>
<button type="button" class="btn btn-default"
data-toggle="modal"
data-target="#myModal">Launch modal wo callback
</button>
<div id="myModal" class="modal fade bd-example-modal-sm" tabindex="-1" role="dialog"
aria-labelledby="mySmallModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="myModalLabel">Modal title</h4>
</div>
<div class="modal-body">
<div ng-include="'template.html'"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
<script type="text/ng-template" id="template.html"><h5>Hello from ng-template!</h5></script>
</body>
<script type="text/javascript" src="//code.jquery.com/jquery-3.1.1.slim.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.6.6/angular.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta.3/js/bootstrap.min.js"></script>

AngularJS - $emit fire before $on

I am working with AngularJS framework. I wrote a few directives with and without their own controller and they sometimes include each other.
For every course, I use my own directive to print its informations
<li ng-repeat="course in courses" on-courses-loaded="fetch_subscribed">
<course-info course="course"></course-info>
</li>
The 'on-courses-loaded' attribute is looking for the last element, I wrote it to do things when ng-repeat is over : a few data are prepared and signal is emitted for every function register as $rootScope.$on('courses_available', function() {}) (scope < actual scope)
angular.module('app').directive('onCoursesLoaded', [
'$rootScope', function ($rootScope) {
return {
restrict: 'A',
link: function (scope, element, attr) {
if (scope.$last === true)
{
element.ready(function() {
// Prepare things, when done return promise and then ->
$rootScope.$broadcast('courses_available');
});
}
}
}
}]);
In course info directive, there is a call to another directive
<subscribe code="course.code" triggered="true"></subscribe>
With this code
<div ng-show="available">
<button type="button" class="btn btn-success" ng-hide="subscribed" ng-click="subscribe()">
<span class="glyphicon glyphicon-ok"></span> Subscribe
</button>
<button type="button" class="btn btn-danger" ng-show="subscribed" ng-click="unsubscribe()">
<span class="glyphicon glyphicon-remove"></span> Unsubscribe
</button>
</div>
And THIS link function
if (scope.triggered === true)
{
console.log('wait for emit');
$rootScope.$on('courses_available', function() {
internals.getSubscribed();
});
}
else
{
console.log('do not wait for emit');
scope.$watch('code', function(value) {
internals.getSubscribed();
});
}
The biggest issue is that signal is emitted before subscribe directive has set its $rootScope.$on('courses_available', ..)
Here you can find HTML pseudo code expanded
<li class="list-group-item" ng-repeat="course in courses | filter: search" on-courses-loaded="fetch_subscribed">
<!--<course-info course="course"></course-info> IS NEXT DIV-->
<div>
<!--<subscribe code="course.code" triggered="true"></subscribe> IS NEXT DIV-->
<div ng-show="available">
<button type="button" class="btn btn-success" ng-hide="subscribed" ng-click="subscribe()">
<span class="glyphicon glyphicon-ok"></span> Subscribe
</button>
<button type="button" class="btn btn-danger" ng-show="subscribed" ng-click="unsubscribe()">
<span class="glyphicon glyphicon-remove"></span> Unsubscribe
</button>
</div>
</div>
I need to prepare data and emit signal after ng-repeat
While 1., every content from ng-repeat must subscribe to $rootScope.$on() and before emit.
If you have any tips, thank you all.
You can detect finish event of ng-repeat as below
if (scope.$last){
window.alert("im the last!");
}
I have done something like this , so I will post my code here maybe it can help you. First I created custom directive
'use strict';
angular.module('myApp')
.directive('onFinishRender',['$timeout', function ($timeout) {
return {
restrict: 'A',
link: function (scope, element, attr) {
if (scope.$last === true) {
$timeout(function () {
scope.$emit('tilesCreated');
});
}
}
}
}]);
Then used this directive with ng-repeat
<div on-finish-render="" ng-repeat="tile in tiles" ></div>
Then I listen for this event inside another directive
scope.$on('tilesCreated', function() {
// Do something here
});
P.S. Notice that in here I do all things on the same scope. But it depends on you if you want you can broadcast events on $rootScope. I just wanted to give you base idea of how to do this. I hope it will help.

Can't create a popover with custom template

First I tried using angular-ui
<span popover-template="removePopover.html" popover-title="Remove?" class="glyphicon glyphicon-remove cursor-select"></span>
here the template is not included and no errors are provided in console. As I undestood from previous questions this capability is still in development (using v0.13.0).
Then I tried using bootstrap's popover
<span delete-popover row-index={{$index}} data-placement="left" class="glyphicon glyphicon-remove cursor-select"></span>
This is included to popover
<div id="removePopover" style="display: none">
<button id="remove" type="button" ng-click="removeElement()" class="btn btn-danger">Remove</button>
<button type="button" ng-click="cancelElement()" class="btn btn-warning">Cancel</button>
</div>
This is the managing directive
app.directive('deletePopover', function(){
return{
link: function(scope, element, attrs) {
$(element).popover({
html : true,
container : element,
content: function() {
return $('#removePopover').html();
}
});
scope.removeElement = function(){
console.log("remove"); //does not get here
}
scope.cancelElement = function(){
console.log("cancel"); //does not get here
}
}
};
});
In case of bootstrap's popover the scope is messed up. cancelElement() call does not arrive in directive neither the parent controller.
If anyone could help me get atleast on of these working it would be great.

Why does this only work the first time the dialog is displayed?

Using ui-bootstrap with this attribute attached to the ok/save button on the dialog.
The first time my dialog is created, it focuses on the button just as expected.
Every subsequent time it has no effect.
.directive('autoFocus', function($timeout) {
return {
restrict: 'AC',
link: function(_scope, _element) {
$timeout(function(){
_element[0].focus();
}, 0);
}
};
});
The modal template looks like this (This comes from Michael Conroy's angular-dialog-service):
<div class="modal" ng-enter="" id="errorModal" role="dialog" aria-Labelledby="errorModalLabel">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header dialog-header-error">
<button type="button" class="close" ng-click="close()">×
</button>
<h4 class="modal-title text-danger"><span class="glyphicon glyphicon-warning-sign"></span> Error</h4>
</div>
<div class="modal-body text-danger" ng-bind-html="msg"></div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" autoFocus ng-click="close()">Close</button>
</div>
</div>
</div>
</div>
The first time the focus moves to the close button no problem. After that the focus stays where it was.
I'm trying to deal with an enter key keypress that is launching this error dialog repeatedly, and I really need the focus to move away from the from underneath the dialog.
Turns out that autofocus is a really bad choice for a directive. I renamed it takefocus and now it works every time without any change. Why does autofocus not work? Beats me. There are override directives for and other tags that are in angular and work, but overriding autofocus with a directive does not.
It happens because the directive autoFocus is compiled once when the element is added to stage and the link function isn't called again, if you have a variable on parent scope responsible for displaying modal like $scope.opened you can use $watcher on said variable, i.e. if ith change from false to true set focus
.directive('autoFocus', function($timeout, $watch) {
return {
restrict: 'AC',
link: function(_scope, _element) {
$watch('_scope.opened', function (newValue) {
if(newValue){
$timeout(function(){
_element[0].focus();
}, 0);
}
}
}
};
});

How to prevent an angular-bootstrap dropdown from closing (Unbind Event which was bound by a directive)

I am using the Angular-Bootstrap Dropdown. I want to prevent it from closing on click until the user closes it intentionally.
Default state is: The Dropdown closes when clicking somewhere in the Document.
I identified the relevant lines of code: (Line 12, dropdown.js)
this.open = function( dropdownScope ) {
if ( !openScope ) {
$document.bind('click', closeDropdown); // line to unbind
$document.bind('keydown', escapeKeyBind);
}
}
You can find the full code here: Link to Github
I don't want to change the original sources of angular-bootstrap to keep my project open for updates.
My question:
How can i unbind an event bound by a Directive to the document in an Angular Controller?
I solved this by adding the following to my drop down-menu. This prevents the drop down from closing unless you click on the tag that opens it
<ul class="dropdown-menu" ng-click="$event.stopPropagation()">
For those who are using Angular UI-Bootstrap 0.13.0 or later version, here is the cleaner way that states on the UI-Bootstrap documentation.
By default the dropdown will automatically close if any of its elements is clicked, you can change this behavior by setting the auto-close option as follows:
always - (Default) automatically closes the dropdown when any of its
elements is clicked.
outsideClick - closes the dropdown automatically only when the user
clicks any element outside the dropdown.
disabled - disables the auto close. You can then control the
open/close status of the dropdown manually, by using is-open. Please
notice that the dropdown will still close if the toggle is clicked,
the esc key is pressed or another dropdown is open. The dropdown will
no longer close on $locationChangeSuccess events.
Here is the link to the documentation:
https://angular-ui.github.io/bootstrap/#/dropdown
You can stop event from bubbling up in DOM Tree in angular 2 and above by adding event propagation.
Ex: (click)="$event.stopPropagation()"
This is another hack, but you could add a directive to stop the toggle event from propagating. For example something like this worked for my specific use case:
<div>
<div class="btn-group" dropdown is-open="status.isopen" ng-controller="DropDownCtrl">
<button type="button" class="btn btn-primary dropdown-toggle" ng-disabled="disabled">
Button dropdown <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li ng-click="goToPage('Action')">Action</li>
<li disable-auto-close>Don't Dismiss</li>
<li ng-click="goToPage('SomethingElse')">Something else here</li>
</ul>
</div>
Adding this directive to an element should disable the auto close behavior:
angular.module('plunker', ['ui.bootstrap'])
.controller('DropDownCtrl', ['$scope', '$location',
function($scope, $location) {
// Controller logic here
$scope.goToPage = function(page) {
console.log("Going to " + page + ". Dropdown should close");
$location.path(page);
};
}])
.directive('disableAutoClose', function() {
// directive for disabling the default
// close on 'click' behavior
return {
link: function($scope, $element) {
$element.on('click', function($event) {
console.log("Dropdown should not close");
$event.stopPropagation();
});
}
};
});
Plunker Example Here
This is a crude way of overriding it. You need to control the is-open attribute manually and hijack the on-toggle event, example:
<div class="btn-group" dropdown is-open="ctrl.isOpen" on-toggle="toggled(open)">
<button type="button" class="btn btn-primary dropdown-toggle">
Button dropdown <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li>Action</li>
<li>Another action</li>
<li>Something else here</li>
<li class="divider"></li>
<li>Separated link</li>
</ul>
</div>
Controller:
$scope.toggled = function (open) {
$timeout(function () {
$scope.ctrl.isOpen = true;
});
};
I would ask for a property on the dropdownConfig constant (something like autoClose) for a permanent solution.
This is an even more crude way of overriding it based on Rob Jacobs answer except that it prevents the ugly flickering ulilcht commented on:
$scope.toggled = function (open) {
$scope.open = true;
var child = $scope.$$childHead;
while (child) {
if (child.focusToggleElement) {
child.isOpen = true;
break;
}
child = child.$$nextSibling;
}
};
You can also use this solution : https://gist.github.com/Xspirits/684beb66e2499c3ff0e5
Gives you a bit more of control over the dropdown, if that's ever needed.
You can decorate directives.
With this way you don't have to touch the original code and you can keep the original behaviour.
You can put a close button inside the dropdown
HTML
<div class="dropdown-menu keep-dropdown-open-on-click" role="menu">
<i class="icon-close close-dropdown-on-click"></i>
</div>
JS
angular.module('app').config(uiDropdownMenuDecorate);
uiDropdownMenuDecorate.$inject = ['$provide'];
function uiDropdownMenuDecorate($provide) {
$provide.decorator('dropdownMenuDirective', uiDropdownMenuDecorator);
uiDropdownMenuDecorator.$inject = ['$delegate'];
function uiDropdownMenuDecorator($delegate) {
var directive = $delegate[0];
var link = directive.link;
directive.compile = function () {
return function (scope, elem, attrs, ctrl) {
link.apply(this, [scope, elem, attrs, ctrl]);
elem.click(function (e) {
if (elem.hasClass('keep-dropdown-open-on-click') && !angular.element(e.target).hasClass('close-dropdown-on-click')) {
e.stopPropagation();
}
});
};
};
return $delegate;
}
}
Here is what the code looks like using the approved angular-bootstrap auto-close method. Notice the auto-close attribute goes on the top <div>.
<div class="btn-group" uib-dropdown auto-close="disabled">
<button id="single-button" type="button" class="btn btn-primary" uib-dropdown-toggle>
Button dropdown <span class="caret"></span>
</button>
<ul class="dropdown-menu" uib-dropdown-menu role="menu" aria-labelledby="single-button">
<textarea class="form-control" ng-model="description" rows="4" placeholder="Description"></textarea>
</ul>
</div>

Resources