calling a function when AngularUI Bootstrap modal has been dismissed and animation has finished executing - angularjs

I'm using the Angular UI bootstrap modal and I ran into a bit of a problem.
I want to call a function when the bootstrap modal dismiss animation is finished. The code block below will call the cancel() function as soon as the modal starts to be dismissed - and NOT when the modal dismiss animation has finished.
Angular UI does not use events, so there is no 'hidden.bs.modal' event being fired (at least, not to my knowledge).
var instance = $modal.open({...});
instance.result.then(function(data) {
return success(data);
}, function() {
return cancel();
})
The cancel() block immediately runs when the modal starts to close. I need code to execute when the closing animation for the Bootstrap modal finishes.
How can I achieve this with angular UI?
Component for reference:
https://angular-ui.github.io/bootstrap/#/modal
Thanks!

A little late but hope it still helps! You can hijack the uib-modal-window directive and check when its scope gets destroyed (it is an isolated scope directive). The scope is destroyed when the modal is finally removed from the document. I would also use a service to encapsulate the functionality:
Service
app.service('Modals', function ($uibModal, $q) {
var service = this,
// Unique class prefix
WINDOW_CLASS_PREFIX = 'modal-window-interceptor-',
// Map to save created modal instances (key is unique class)
openedWindows = {};
this.open = function (options) {
// create unique class
var windowClass = _.uniqueId(WINDOW_CLASS_PREFIX);
// check if we already have a defined class
if (options.windowClass) {
options.windowClass += ' ' + windowClass;
} else {
options.windowClass = windowClass;
}
// create new modal instance
var instance = $uibModal.open(options);
// attach a new promise which will be resolved when the modal is removed
var removedDeferred = $q.defer();
instance.removed = removedDeferred.promise;
// remember instance in internal map
openedWindows[windowClass] = {
instance: instance,
removedDeferred: removedDeferred
};
return instance;
};
this.afterRemove = function (modalElement) {
// get the unique window class assigned to the modal
var windowClass = _.find(_.keys(openedWindows), function (windowClass) {
return modalElement.hasClass(windowClass);
});
// check if we have found a valid class
if (!windowClass || !openedWindows[windowClass]) {
return;
}
// get the deferred object, resolve and clean up
var removedDeferred = openedWindows[windowClass].removedDeferred;
removedDeferred.resolve();
delete openedWindows[windowClass];
};
return this;
});
Directive
app.directive('uibModalWindow', function (Modals) {
return {
link: function (scope, element) {
scope.$on('$destroy', function () {
Modals.afterRemove(element);
});
}
}
});
And use it in your controller as follows:
app.controller('MainCtrl', function ($scope, Modals) {
$scope.openModal = function () {
var instance = Modals.open({
template: '<div class="modal-body">Close Me</div>' +
'<div class="modal-footer"><a class="btn btn-default" ng-click="$close()">Close</a></div>'
});
instance.result.finally(function () {
alert('result');
});
instance.removed.then(function () {
alert('closed');
});
};
});
I also wrote a blog post about it here.

Related

Widget toggle functionality with $compile

I need to implement toggle functionality for the widget. When the user clicks on the minimization button then widget should shrink and expand when click on maximize button respectively.
I'm trying to achieve this functionality with below piece of code.
Functionality working as expected but it is registering the event multiple times(I'm emitting the event and catching in the filterTemplate directive).
How can we stop registering the event multiple times ?
Or
Is there anyway to like compiling once and on toggle button bind the template/directive to DOM and to make it work rest of the functionality .
So could you please help me to fix this.
function bindFilterTemplate(minimize) {
if ($scope.item && !minimize) {
if ($scope.item.filterTemplate) { // filter template is custom
// directive like this
// "<widget></widget>"
$timeout(function () {
var filterElement = angular.element($scope.item.filterTemplate);
var filterBody = element.find('.cls-filter-body');
filterElement.appendTo(filterBody);
$compile(filterElement)($scope); // Compiling with
// current scope on every time when user click on
// the minimization button.
});
}
} else {
$timeout(function () {
element.find('.cls-filter-body').empty();
});
}
}
bindFilterTemplate();
// Directive
app.directive('widget', function () {
return {
restrict: 'E',
controller: 'widgetController',
link: function ($scope, elem) {
// Some code
}
};
});
// Controller
app.controller('widgetController', function ($scope) {
// This event emitting from parent directive
// On every compile, the event is registering with scope.
// So it is triggering multiple times.
$scope.$on('evt.filer', function ($evt) {
// Server call
});
});
I fixed this issue by creating new scope with $scope.$new().
When user minimizes the widget destroying the scope.
Please let me know if you have any other solution to fix this.
function bindFilterTemplate(minimize) {
// Creating the new scope.
$scope.newChildScope = $scope.$new();
if ($scope.item && !minimize) {
if ($scope.item.filterTemplate) {
$timeout(function () {
var filterElement = angular.element($scope.item.filterTemplate);
var filterBody = element.find('.cls-filter-body');
filterElement.appendTo(filterBody);
$compile(filterElement)($scope.newChildScope);
});
}
} else {
$timeout(function () {
if ($scope.newChildScope) {
// Destroying the new scope
$scope.newChildScope.$destroy();
}
element.find('.cls-filter-body').empty();
});
}
}

Implementing component require property in Angular 1.5 components

I am having no joy with implementing require: {} property as part of an angular component. Allow me to demonstrate with an example I have.
This is the component/directive that supposed to fetch a list of judgements. Nothing very fancy, just a simple factory call.
// judgements.component.js
function JudgementsController(GetJudgements) {
var ctrl = this;
ctrl.Get = function () {
GetJudgements.get().$promise.then(
function (data) {
ctrl.Judgements = data.judgements;
}, function (error) {
// show error message
});
}
ctrl.$onInit = function () {
ctrl.Get();
};
}
angular
.module('App')
//.component('cJudgements', {
// controller: JudgementsController,
//});
.directive('cJudgements', function () {
return {
scope: true,
controller: 'JudgementsController',
//bindToController: true,
};
});
I am trying to implement component require property to give me access to ctrl.Judgements from the above component/directive as follows:
// list.component.js
function ListController(GetList, GetJudgements) {
var ctrl = this;
ctrl.list = [];
ctrl.Get = function () {
GetList.get().$promise.then(
function (data) {
ctrl.list = data.list;
}, function (error) {
// show error message
});
};
//ctrl.GetJudgements = function () {
// GetJudgements.get().$promise.then(
// function (data) {
// ctrl.Judgements = data.judgements;
// }, function (error) {
// // show error message
// });
//}
ctrl.$onInit = function () {
ctrl.Get();
//ctrl.GetJudgements();
};
}
angular
.module('App')
.component('cTheList', {
bindings: {
listid: '<',
},
controller: ListController,
controllerAs: 'ctrl',
require: {
jCtrl: 'cJudgements',
},
template: `
<c-list-item ng-repeat="item in ctrl.list"
item="item"
judgements="ctrl.Judgements"></c-list-item>
<!--
obviously the reference to judgements here needs to change
or even better to be moved into require of cListItem component
-->
`,
});
Nice and simple no magic involved. A keen reader probably noticed GetJudgement service call in the ListController. This is what I am trying to remove from TheList component using require property.
The reason? Is actually simple. I want to stop database being hammered by Judgement requests as much as possible. It's a static list and there is really no need to request it more than once per instance of the app.
So far I have only been successful with receiving the following error message:
Error: $compile:ctreq
Missing Required Controller
Controller 'cJudgements', required by directive 'cTheList', can't be found!
Can anyone see what I am doing wrong?
PS: I am using angular 1.5
PSS: I do not mind which way cJudgement is implemented (directive or component).
PSSS: If someone wonders I have tried using jCtrl: '^cJudgements'.
PSSSS: And multiple ^s for that matter just in case.
Edit
#Kindzoku posted a link to the article that I have read before posting the question. I hope this also helps someone in understanding $onInit and require in Angular 1.5+.
Plunker
Due to popular demand I made a plunker example.
You should use required components in this.$onInit = function(){}
Here is a good article https://toddmotto.com/on-init-require-object-syntax-angular-component/
The $onInit in your case should be written like this:
ctrl.$onInit = function () {
ctrl.jCtrl.Get();
};
#iiminov has the right answer. No parent HTML c-judgements was defined.
Working plunker.

How to properly unit test directives with DOM manipulation?

Before asking my real question, I have a different one... Does it make sense to unit test DOM manipulation in Angular directives?
For instance, here's my complete linking function:
function linkFn(scope, element) {
var ribbon = element[0];
var nav = ribbon.children[0];
scope.ctrl.ribbonItemClick = function (index) {
var itemOffsetLeft;
var itemOffsetRight;
var item;
if (scope.ctrl.model.selectedIndex === index) {
return;
}
scope.ctrl.model.selectedIndex = index;
item = nav.querySelectorAll('.item')[index];
itemOffsetLeft = item.offsetLeft - ribbon.offsetLeft;
itemOffsetRight = itemOffsetLeft + item.clientWidth;
if (itemOffsetLeft < nav.scrollLeft) {
nav.scrollLeft = itemOffsetLeft - MAGIC_PADDING;
}
if(itemOffsetRight > nav.clientWidth + nav.scrollLeft) {
nav.scrollLeft = itemOffsetRight - nav.clientWidth + MAGIC_PADDING;
}
this.itemClick({
item: scope.ctrl.model.items[index],
index: index
});
$location.path(scope.ctrl.model.items[index].href);
};
$timeout(function $timeout() {
var item = nav.querySelector('.item.selected');
nav.scrollLeft = item.offsetLeft - ribbon.offsetLeft - MAGIC_PADDING;
});
}
This is for a scrollable tabbed component and I have no idea how to test the 3 instances of nav.scrollLeft = x.
The first two if statements happen when an item - which is only partially visible - is clicked. The left/right (each if) item will be snapped to the left/right border of the component.
The third one, is to place the selected item in view if it's not visible when the component is loaded.
How do I unit test this with Karma/Jasmine. does it even make sense to do it or should I do functional tests with Protractor instead?
When testing directives, look for things that set or return explicit values. These are generally easy to assert and it makes sense to unit test them with Jasmine and Karma.
Take a look at Angular's tests for ng-src. Here, they test that the directive works by asserting that the src attribute on the element gets set to the right values. It is explicit: either the src attribute has a specific value or it does not.
it('should not result empty string in img src', inject(function($rootScope, $compile) {
$rootScope.image = {};
element = $compile('<img ng-src="{{image.url}}">')($rootScope);
$rootScope.$digest();
expect(element.attr('src')).not.toBe('');
expect(element.attr('src')).toBe(undefined);
}));
The same with ng-bind. Here, they pass a string of HTML with to the $compiler and then assert that the return value has had its HTML populated with actual scope values. Again, it is explicit.
it('should set text', inject(function($rootScope, $compile) {
element = $compile('<div ng-bind="a"></div>')($rootScope);
expect(element.text()).toEqual('');
$rootScope.a = 'misko';
$rootScope.$digest();
expect(element.hasClass('ng-binding')).toEqual(true);
expect(element.text()).toEqual('misko');
}));
When you get into more complicated scenarios like testing against viewport visibility or testing whether specific elements are positioned in the right places on the page, you could try to test that CSS and style attributes get set properly, but that gets fiddly real quick and is not recommended. At this point you should be looking at Protractor or a similar e2e testing tool.
I would 100% want to test all the paths of your directive even if it isn't the easiest thing. But there are approaches you can take to make this process simpler.
Break Complicated Logic into Service
The first thing that stands out to me is the complicated piece of logic about setting the nav scrollLeft. Why not break this into a separate service that can be unit tested on its own?
app.factory('AutoNavScroller', function() {
var MAGIC_PADDING;
MAGIC_PADDING = 25;
return function(extraOffsetLeft) {
this.getScrollPosition = function(item, nav) {
var itemOffsetLeft, itemOffsetRight;
itemOffsetLeft = item.offsetLeft - extraOffsetLeft;
itemOffsetRight = itemOffsetLeft + item.clientWidth;
if ( !!nav && itemOffsetRight > nav.clientWidth + nav.scrollLeft) {
return itemOffsetRight - nav.clientWidth + MAGIC_PADDING;
} else {
return itemOffsetLeft - MAGIC_PADDING;
}
};
}
});
This makes it much easier to test all the paths and refactor (which you can see I was able to do above. The tests can be seen below:
describe('AutoNavScroller', function() {
var AutoNavScroller;
beforeEach(module('app'));
beforeEach(inject(function(_AutoNavScroller_) {
AutoNavScroller = _AutoNavScroller_;
}));
describe('#getScrollPosition', function() {
var scroller, item;
function getScrollPosition(nav) {
return scroller.getScrollPosition(item, nav);
}
beforeEach(function() {
scroller = new AutoNavScroller(50);
item = {
offsetLeft: 100
};
})
describe('with setting initial position', function() {
it('gets the initial scroll position', function() {
expect(getScrollPosition()).toEqual(25);
});
});
describe('with item offset left of the nav scroll left', function() {
it('gets the scroll position', function() {
expect(getScrollPosition({
scrollLeft: 100
})).toEqual(25);
});
});
describe('with item offset right of the nav width and scroll left', function() {
beforeEach(function() {
item.clientWidth = 300;
});
it('gets the scroll position', function() {
expect(getScrollPosition({
scrollLeft: 25,
clientWidth: 50
})).toEqual(325);
});
});
});
});
Test That Directive is Calling Service
Now that we've broken up our directive, we can just inject the service and make sure it is called correctly.
app.directive('ribbonNav', function(AutoNavScroller, $timeout) {
return {
link: function(scope, element) {
var navScroller;
var ribbon = element[0];
var nav = ribbon.children[0];
// Assuming ribbon offsetLeft remains the same
navScroller = new AutoNavScroller(ribbon.offsetLeft);
scope.ctrl.ribbonItemClick = function (index) {
if (scope.ctrl.model.selectedIndex === index) {
return;
}
scope.ctrl.model.selectedIndex = index;
item = nav.querySelectorAll('.item')[index];
nav.scrollLeft = navScroller.getScrollLeft(item, nav);
// ...rest of directive
};
$timeout(function $timeout() {
var item = nav.querySelector('.item.selected');
// Sets initial nav scroll left
nav.scrollLeft = navScroller.getScrollLeft(item);
});
}
}
});
The easiest way to make sure our directive keeps using the service, is to just spy on the methods that it will call and make sure they are receiving the correct parameters:
describe('ribbonNav', function() {
var $compile, $el, $scope, AutoNavScroller;
function createRibbonNav() {
$el = $compile($el)($scope);
angular.element(document)
$scope.$digest();
document.body.appendChild($el[0]);
}
beforeEach(module('app'));
beforeEach(module(function ($provide) {
AutoNavScroller = jasmine.createSpy();
AutoNavScroller.prototype.getScrollLeft = function(item, nav) {
return !nav ? 50 : 100;
};
spyOn(AutoNavScroller.prototype, 'getScrollLeft').and.callThrough();
$provide.provider('AutoNavScroller', function () {
this.$get = function () {
return AutoNavScroller;
}
});
}));
beforeEach(inject(function(_$compile_, $rootScope) {
$compile = _$compile_;
$el = "<div id='ribbon_nav' ribbon-nav><div style='width:50px;overflow:scroll;float:left;'><div class='item selected' style='height:100px;width:200px;float:left;'>An Item</div><div class='item' style='height:100px;width:200px;float:left;'>An Item</div></div></div>";
$scope = $rootScope.$new()
$scope.ctrl = {
model: {
selectedIndex: 0
}
};
createRibbonNav();
}));
afterEach(function() {
document.getElementById('ribbon_nav').remove();
});
describe('on link', function() {
it('calls AutoNavScroller with selected item', inject(function($timeout) {
expect(AutoNavScroller).toHaveBeenCalledWith(0);
}));
it('calls AutoNavScroller with selected item', inject(function($timeout) {
$timeout.flush();
expect(AutoNavScroller.prototype.getScrollLeft)
.toHaveBeenCalledWith($el[0].children[0].children[0]);
}));
it('sets the initial nav scrollLeft', inject(function($timeout) {
$timeout.flush();
expect($el[0].children[0].scrollLeft).toEqual(50);
}));
});
describe('ribbonItemClick', function() {
beforeEach(function() {
$scope.ctrl.ribbonItemClick(1);
});
it('calls AutoNavScroller with item', inject(function($timeout) {
expect(AutoNavScroller.prototype.getScrollLeft)
.toHaveBeenCalledWith($el[0].children[0].children[1], $el[0].children[0]);
}));
it('sets the nav scrollLeft', function() {
expect($el[0].children[0].scrollLeft).toEqual(100);
});
});
});
Now, obviously these specs can be refactored a 100 ways but you can see that a higher coverage is much easier to achieve once we started breaking out the complicated logic. There are some risks around mocking objects too much because it can make your tests brittle but I believe the tradeoff is worth it here. Plus I can definitely see that AutoNavScroller being generalized and reused elsewhere. That would not have been possible if the code existed in the directive before.
Conclusion
Anyways, the reason why I believe Angular is great is the ability to test these directives and how they interact with the DOM. These jasmine specs can be run in any browser and will quickly surface inconsistencies or regressions.
Also, here is a plunkr so you can see all the moving pieces and experiment: http://plnkr.co/edit/wvj4TmmJtxTG0KW7v9rn?p=preview

Underscore 'after' function

I have code similar to this
(function(exports, $, Backbone, document){
"use strict";
var modals = {
"Alerts.alert_delete" : (function() {
var self = this;
return {
template : {
count : this.collection.length,
models : this.collection.models
},
events : {
"click .confirm" : function(event) {
event.preventDefault();
var modal = this,
finished = _.after(modal.collection.length, self.reDraw);
// Models are succesfully delete, but finished is not completed
this.collection.each(function(model) {
modal.collection.sync('delete', model, { success : finished });
});
}
}
};
})
};
exports.app.modal = function(name, context) {
return modals[name].call(context);
};
}(this, jQuery, Backbone, document));
Please ignore the abstraction, this is to allow me to use one generic view for all my modals whilst keeping unique logic abstracted. I am at a loss as to why the _.after function is not completing when it has been called the correct number of times. self in this instance is a reference to the parent view.
Can someone shed some light on this for me?

Access Element Style from Angular directive

I'm sure this is going to be a "dont do that!" but I am trying to display the style on an angular element.
<div ng-repeat="x in ['blue', 'green']" class="{{x}}">
<h3 insert-style>{{theStyle['background-color']}}</h3>
</div>
Result would be
<div class='blue'><h3>blue(psudeo code hex code)</h3></div>
<div class='green'><h3>green(psudeo code hex code)</h3></div>
I basically need to get the style attributes and display them.
Directive Code...
directives.insertStyle = [ function(){
return {
link: function(scope, element, attrs) {
scope.theStyle = window.getComputedStyle(element[0], null);
}
}
}];
Fiddle example: http://jsfiddle.net/ncapito/G33PE/
My final solution (using a single prop didn't work, but when I use the whole obj it works fine)...
Markup
<div insert-style class="box blue">
<h4 > {{ theStyle['color'] | toHex}} </h4>
</div>
Directive
directives.insertStyle = [ "$window", function($window){
return {
link: function(scope, element, attrs) {
var elementStyleMap = $window.getComputedStyle(element[0], null);
scope.theStyle = elementStyleMap
}
}
}];
Eureka!
http://jsfiddle.net/G33PE/5/
var leanwxApp = angular.module('LeanwxApp', [], function () {});
var controllers = {};
var directives = {};
directives.insertStyle = [ function(){
return {
link: function(scope, element, attrs) {
scope.theStyle = window.getComputedStyle(element[0].parentElement, null)
}
}
}];
leanwxApp.controller(controllers);
leanwxApp.directive(directives);
So that just took lots of persistence and guessing. Perhaps the timeout is unnecessary but while debugging it seemed I only got the style value from the parent after the timeout occurred.
Also I'm not sure why but I had to go up to the parentElement to get the style (even though it would realistically be inherited (shrug)?)
Updated fiddle again
Did one without the timeout but just looking at the parentElement for the style and it seems to still work, so scratch the suspicions about the style not being available at all, it's just not available where I would expect it.
Also holy cow there are a lot of ways to debug in Chrome:
https://developers.google.com/chrome-developer-tools/docs/javascript-debugging
I used
debugger;
statements in the code to drop in breakpoints without having to search all the fiddle files.
One more quick update
The code below comes out of Boostrap-UI from the AngularUI team and claims to provide a means to watch the appropriate events (haven't tried this but it looks like it should help).
http://angular-ui.github.io/bootstrap/
/**
* $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete.
* #param {DOMElement} element The DOMElement that will be animated.
* #param {string|object|function} trigger The thing that will cause the transition to start:
* - As a string, it represents the css class to be added to the element.
* - As an object, it represents a hash of style attributes to be applied to the element.
* - As a function, it represents a function to be called that will cause the transition to occur.
* #return {Promise} A promise that is resolved when the transition finishes.
*/
.factory('$transition', ['$q', '$timeout', '$rootScope', function($q, $timeout, $rootScope) {
var $transition = function(element, trigger, options) {
options = options || {};
var deferred = $q.defer();
var endEventName = $transition[options.animation ? "animationEndEventName" : "transitionEndEventName"];
var transitionEndHandler = function(event) {
$rootScope.$apply(function() {
element.unbind(endEventName, transitionEndHandler);
deferred.resolve(element);
});
};
if (endEventName) {
element.bind(endEventName, transitionEndHandler);
}
// Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur
$timeout(function() {
if ( angular.isString(trigger) ) {
element.addClass(trigger);
} else if ( angular.isFunction(trigger) ) {
trigger(element);
} else if ( angular.isObject(trigger) ) {
element.css(trigger);
}
//If browser does not support transitions, instantly resolve
if ( !endEventName ) {
deferred.resolve(element);
}
});
// Add our custom cancel function to the promise that is returned
// We can call this if we are about to run a new transition, which we know will prevent this transition from ending,
// i.e. it will therefore never raise a transitionEnd event for that transition
deferred.promise.cancel = function() {
if ( endEventName ) {
element.unbind(endEventName, transitionEndHandler);
}
deferred.reject('Transition cancelled');
};
return deferred.promise;
};
// Work out the name of the transitionEnd event
var transElement = document.createElement('trans');
var transitionEndEventNames = {
'WebkitTransition': 'webkitTransitionEnd',
'MozTransition': 'transitionend',
'OTransition': 'oTransitionEnd',
'transition': 'transitionend'
};
var animationEndEventNames = {
'WebkitTransition': 'webkitAnimationEnd',
'MozTransition': 'animationend',
'OTransition': 'oAnimationEnd',
'transition': 'animationend'
};
function findEndEventName(endEventNames) {
for (var name in endEventNames){
if (transElement.style[name] !== undefined) {
return endEventNames[name];
}
}
}
$transition.transitionEndEventName = findEndEventName(transitionEndEventNames);
$transition.animationEndEventName = findEndEventName(animationEndEventNames);
return $transition;
}]);
The issue you'll face is that getComputedStyle is considered a very slow running method, so you will run into performance issues if using that, especially if you want angularjs to update the view whenever getComputedStyle changes.
Also, getComputedStyle will resolve every single style declaration possible, which i think will not be very useful. So i think a method to reduce the number of possible style is needed.
Definitely consider this an anti-pattern, but if you still insist in this foolishness:
module.directive('getStyleProperty', function($window){
return {
//Child scope so properties are not leaked to parent
scope : true,
link : function(scope, element, attr){
//A map of styles you are interested in
var styleProperties = ['text', 'border'];
scope.$watch(function(){
//A watch function to get the styles
//Since this runs every single time there is an angularjs loop, this would not be a very performant way to do this
var obj = {};
var computedStyle = $window.getComputedStyle(element[0]);
angular.forEach(styleProperties, function(value){
obj[value] = computedStyle.getPropertyValue(value);
});
return obj;
}, function(newValue){
scope.theStyle = newValue;
});
}
}
});
This solution works if you don't HAVE to have the directive on the child element. If you just place the declaration on the ng-repeat element itself, your solution works:
<div insert-style ng-repeat="x in ['blue', 'green']" class="{{x}}">
Fiddle

Resources