Can ng-show directive be used with a delay - angularjs

I have a spinner this is shown with ng-show="loading>0"
Is there a way I can display this spinner with a delay (say 1 second)?
I can't use a timeout because with multiple requests de loading counter will get out of sync.
What I need is a delay on the ng-show via css transition or similar

My suspicion is that you are looking for a general purpose spinner that includes a delay. The standard, show after 200ms or something like that.
This is a perfect candidate for a directive, and actually pretty easy to accomplish.
I know this is a long code example, but the primary piece is the directive. It's pretty simple.
Listen to a few scope variables and shows after some configurable delay. If the operation takes longer than the delay, it will just get canceled and never show up.
(function() {
'use strict';
function SpinnerDirective($timeout) {
return {
restrict: 'E',
template: '<i class="fa fa-cog fa-spin"></i>',
scope: {
show: '=',
delay: '#'
},
link: function(scope, elem, attrs) {
var showTimer;
//This is where all the magic happens!
// Whenever the scope variable updates we simply
// show if it evaluates to 'true' and hide if 'false'
scope.$watch('show', function(newVal){
newVal ? showSpinner() : hideSpinner();
});
function showSpinner() {
//If showing is already in progress just wait
if (showTimer) return;
//Set up a timeout based on our configured delay to show
// the element (our spinner)
showTimer = $timeout(showElement.bind(this, true), getDelay());
}
function hideSpinner() {
//This is important. If the timer is in progress
// we need to cancel it to ensure everything stays
// in sync.
if (showTimer) {
$timeout.cancel(showTimer);
}
showTimer = null;
showElement(false);
}
function showElement(show) {
show ? elem.css({display:''}) : elem.css({display:'none'});
}
function getDelay() {
var delay = parseInt(scope.delay);
return angular.isNumber(delay) ? delay : 200;
}
}
};
}
function FakeService($timeout) {
var svc = this,
numCalls = 0;
svc.fakeCall = function(delay) {
numCalls += 1;
return $timeout(function() {
return {
callNumber: numCalls
};
}, delay || 50);
};
}
function MainCtrl(fakeService) {
var vm = this;
vm.makeCall = function(delay) {
vm.isBusy = true;
fakeService.fakeCall(delay)
.then(function(result) {
vm.result = result;
}).finally(function() {
vm.isBusy = false;
});
}
}
angular.module('spinner', [])
.service('fakeService', FakeService)
.controller('mainCtrl', MainCtrl)
.directive('spinner', SpinnerDirective);
}());
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css" rel="stylesheet" />
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.8/angular.min.js"></script>
<div class="container" ng-app="spinner">
<div class="row" ng-controller="mainCtrl as ctrl">
<div class="col-sm-12">
<h2>{{ctrl.result | json}}
<spinner show="ctrl.isBusy" delay="200"></spinner>
</h2>
<button type="button"
class="btn btn-primary"
ng-click="ctrl.makeCall(2000)"
ng-disabled="ctrl.isBusy">Slow Call
</button>
<button type="button"
class="btn btn-default"
ng-click="ctrl.makeCall()"
ng-disabled="ctrl.isBusy">Fast Call
</button>
</div>
</div>
</div>

Here's a simpler approach that worked for my needs. Depending on what your action is, you would tie function setDelay() to the element. For example, in my case I tied setDelay() to a select input.
Trigger HTML:
<select class="first-option"
ng-change="setDelay()"
ng-options="o.label for o in download.options"
ng-model="optionModel" required>
</select>
In your controller, add a simple function setDelay that will change the flag $scope.delay:
$scope.setDelay = function(){
$scope.delay = true;
$timeout(function(){
$scope.delay = false;
}, 200);
};
Then, you can simply use $scope.delay as a flag in ng-show:
<div class="loading-div" ng-show="delay">
<img src="loading_spinner.gif">
</div>
And show content after done loading:
<div ng-show="!delay">
Content is loaded.
</div>
Now, every time the user selects a new value in the dropdown menu, it will trigger$scope.delay to be set totrue causing the spinner to show, and when it reaches 200, it will be set to false causing the spinner to hide.

I think a pure CSS solution is the best way to do it.
Here is a plunker showing how to do it. Using ng-animate classes for transition and applying a transition delay with a transition of 10ms (0s transition is not working with css).
Relevant part of the code :
.your-element-class.ng-hide {
opacity: 0;
}
.your-element-class.ng-hide-add,
.your-element-class.ng-hide-remove {
transition: all linear 0.01s 1s;
}
The only reason to use a custom directive for it would be using this tons of times in your code with different delays value. A custom directive allow more flexibility with the delay timing.

Related

Start/Stop/Resume all in one button with Angular Timer

Using the pre-built "Angular Timer" directives available here (scroll down to progress bar example), I'm trying to build a progress bar with a Start, Stop and Resume button all-in-one.
Examples on their websites are made of two buttons and I would like to merge them.
One solution could be to use innerHTML to get the current state ("Start" or "Stop") and a if condition in the controller.js but i'd like the button to be a icon-only-button switching from play to pause icon.
An example from simple timer implementation is available here
Here is my html
<button class="button button-icon icon" ng-class="{'icon ion-play': !play, 'icon ion-pause': play}" ng-click="stopResumeTimer(this)"></button>
<timer interval="1000" autostart="false"><div class="progress"> <div class="bar" style="width: {{seconds}}%;"></div> </div></timer>
And the controller.js code with the if condition
$scope.stopResumeTimer = function(btn) {
if (not yet started) {
$scope.startTimer();
}
else if (already started) {
$scope.stopTimer();
}
else {
$scope.resumeTimer();
}
}
I am discouraged by the repeated failure on this, any help would be great! Thanks
In the controller where you are setting the ng-click function, you can set flags to determine whether the click function should start, stop, or resume. Those flags can also set the text for the button to be either "Start", "Stop", or "Resume".
JS:
(function(angular) {
function controller($scope) {
var isStart = false;
var isStop = false;
$scope.timerBtnText = "Start";
$scope.timer = function() {
if (isStart) {
$scope.startTimer();
isStart = false;
isStop = true;
$scope.timerBtnText = "Stop";
return;
}
if (isStop) {
$scope.stopTimer();
isStop = false;
$scope.timerBtnText = "Resume";
} else {
$scope.resumeTimer();
isStop = true;
$scope.timerBtnText = "Stop";
}
};
}
angular.module("app", []).controller("controller", ["$scope", controller]);
})(angular);
HTML:
<button ng-click="timer()" ng-bind="timerText"></butotn>
Here is a working proof of concept: http://plnkr.co/edit/NlQFPysVH1M2EyjogQQv
This is how I would solve it (Plunker)
Controller:
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
$scope.operation = 'start';
$scope.startOrStop = function(){
document.getElementById('first')[$scope.operation]();
$scope.operation = ($scope.operation === 'start' || $scope.operation === 'resume') ? 'stop' : 'resume';
}
});
Declare it like this:
<button ng-click="startOrStop()" ng-class="{start:operation==='start', stop:operation==='stop', resume:operation==='resume'}"></button>
<timer id="first" interval="1000" autostart="false"><div class="progress"><div class="progress-bar" role="progressbar" style="width: {{seconds}}%;"></div></div></timer>
Css:
.start::before{
content:"Start";
}
.stop::before{
content:"Stop";
}
.resume::before{
content:"Resume";
}

ng-scrollbar is not working with ng-repeat

In my app I want to use a custom scrollbar for a div. So I used ng-scrollbar, it is working fine with static data. But whenever I get the data using ng-repeat it is not working. Please help me in this regard. Thanks in advance.
myFile.html
<style>
.scrollme {
max-height: 300px;
}
</style>
<div ng-app="myapp">
<div class="container" ng-controller="myctrl">
<button class="btn btn-info" ng-click="add();">add</button>
<button class="btn btn-warning" ng-click="remove();">remove</button>
<div class="well" >
<div class="scrollme" ng-scrollbar bottom rebuild-on="rebuild:me">
<h1>Scroll me down!</h1>
<p ng-repeat="mi in me">{{mi.name}}</p>
</div>
</div>
</div>
</div>
myCtrl.js
var myapp = angular.module('myapp', ["ngScrollbar"]);
myapp.controller('myctrl', function ($scope) {
$scope.me = [];
for(var i=1;i<=20;i++){
$scope.me.push({"name":i});
}
var a = $scope.me.length;
$scope.add = function(){
$scope.me.push({"name":$scope.me.length+1});
$scope.$broadcast('rebuild:me');
}
$scope.remove = function(){
$scope.me.pop();
}
});
Try adding the broadcast call to the end of your controller so it fires on controller load. If that doesn't work, try adding:
$timeout(function () {
$scope.$broadcast('rebuild:me');
}, 0);
// 0 optional, without it the time is assumed 0 which means next digest loop.
at the end of your controller code, not inside the add function. If this works but the previous approach doesn't then that means ngRepeat didn't finish rendering it's dynamic content in time for the ngScrollbar to properly update.
UPDATE: in general, you might have to wrap the broadcast inside of the add() function in a timeout as well. The reason I say this is that I suspect what's going on is that you add data to the scope variable and then broadcast all in the same function call. What might be happening is that the broadcast event is caught and scrollbar recalculates before ngRepeat sees the updated scope data and adds its extra DOM elements. Btw, if you want to recalculate the scrollbar on add(), then you also want to do this on remove() as well.
So your add function would become:
$scope.add = function(){
$scope.me.push({"name":$scope.me.length+1});
// wait until next digest loop to send event, this way ngRepeat has enough time to update(?)
$timeout(function () {
$scope.$broadcast('rebuild:me');
});
}
please try ng-scroll... another plugin, but without need of manual adjust.
mentioned on:
AngularJS with ng-scroll and ng-repeat
If you use jQuery, you can try jQuery Scrollbar - it has more options and fully CSS customizable.
Example with ng-repeat is here
JavaScript
var demoApp = angular.module('demoApp', ['jQueryScrollbar']);
demoApp.controller('SimpleController', function($scope){
$scope.me = [];
for(var i=1;i<=20;i++){
$scope.me.push({"name":i});
}
$scope.add = function(){
$scope.me.push({"name":$scope.me.length+1});
}
$scope.remove = function(){
$scope.me.pop();
}
$scope.jqueryScrollbarOptions = {
"onUpdate":function(container){
setTimeout(function(){
// scroll to bottom. timeout required as scrollbar restores
// init scroll positions after calculations
container.scrollTop(container.prop("scrollHeight"));
}, 10);
}
};
});
HTML
<div data-ng-app="demoApp">
<div data-ng-controller="SimpleController">
<button class="btn btn-info" ng-click="add();">add</button>
<button class="btn btn-warning" ng-click="remove();">remove</button>
<div class="scrollbar-dynamic" data-jquery-scrollbar="jqueryScrollbarOptions">
<h1>Scroll me down!</h1>
<p ng-repeat="mi in me">{{mi.name}}</p>
</div>
</div>
</div>
CSS
.scrollbar-dynamic {
border: 1px solid #FCC;
max-height: 300px;
overflow: auto;
}
This might be a bit late.
The problem is even though you have added the content to scope variable, angular has not finished adding p tags to your DOM. If you try a simple console log like
console.log($('.well').find('p').length);
After pushing content to $scope.me, you will understand what is happening. (Need jQuery at least to debug)
The solution is far more complicated than you can imagine.
STEP 1:
Add a ng-controller to your ng-repeat (Yes. It is allowed)
<p ng-repeat="mi in me" ng-controller="loopController">{{mi.name}}</p>
STEP 2: Define loopController
demoApp.controller('loopController', function($scope) {
$scope.$watch('$last', function(new_val) {
new_val && $scope.$emit('loopLoaded', $scope.$index);
});
});
This controller function is triggered whenever ng-repeat manipulates DOM. I'm watching $last which is a scope variable for ng-repeat. This will be set to true whenever, ng-repeat loads last element in DOM. When $last is set to true I emit one event loopLoaded. Since you are pushing values into $scope.me using a loop, this event will be triggered for every push.
STEP 3: Event handling
In your SimpleController (not simple anymore, eh?)
$scope.$on('loopLoaded', function(evt, index) {
if (index == $scope.me.length-1) {
$scope.$broadcast('rebuild:me');
}
});
Once all the p elements are loaded, index sent to event will be equal to $scope.me.length-1. So you call scroll rebuild. That's it.
Here's a reference I used - AngularJS - Manipulating the DOM after ng-repeat is finished

AngularJS - Why is the DOM not updating

So, what I am trying to do is add a "stack" - a basic JS object - into an array called cardStacks, declared the stackCtrl function. I've read that the controller in AngularJS is not supposed to do very much, nor do you want the manipulation of DOM elements done the controller. So what I have done is create a "save" directive which, as you could probably guess, adds a new "stack" into $scope.cardStacks (if the object is not being edited)
Everything seems to work ok, up until when the template is supposed to update. console.log() reveals that objects are going into an array, but because the template is not being updated, I can only guess it is not $scope.cardStacks.
Can somebody give the following a look and tell me why the template is not listing the "stacks" in the template?
Consider the following:
Template:
<div ng-controller="stackCtrl">
<stackeditor></stackeditor>
<p>Stacks:</p>
<ul class="stack-list" ng-repeat="stack in cardStacks">
<li>{{stack.title}}
<span class="stack-opts"> (Edit | Delete)</span>
</li>
</ul>
</div>
Template for the stackeditor tag/directive:
<div>
<span class="button" ng-click="show=!show">+ New</span>
<form ng-show="show" novalidate>
<input type="text" ng-model="stackTitle" required/>
<button class="button" save>Save</button>
<button class="button" reset>Cancel</button>
<div ng-show="error"><p>{{error}}</p></div>
</form>
</div>
Controller:
function stackCtrl($scope) {
$scope.cardStacks = [];
$scope.stackTitle = "";
$scope.addStack = function(title) {
var newStack = {};
newStack.title = title;
$scope.cardStacks.push(newStack);
}
$scope.removeStack = function($index) {
console.log("removing Stack...");
}
$scope.editStack = function(element) {
console.log("editing Stack...");
}
}
Directive:
fcApp.directive('save', function() {
var linkFn = function($scope, element, attrs) {
element.bind("click", function() {
if (typeof $scope.stackTitle !== 'undefined' && $scope.stackTitle.length > 0) {
if ($scope.edit) {
$scope.editStack(element);
} else {
$scope.addStack($scope.stackTitle);
}
} else {
$scope.error = "Your card stack needs a title!";
}
});
});
return {
restrict: "A",
link: linkFn
}
}
});
Try using $apply:
$scope.cardStacks.push(newStack);
$scope.$apply(function(){
$scope.cardStacks;
}
Rendering might be the problem..... Hope it helps.
The save function would be better in stackCtrl and then use ng-click in the template to call it.
You are right that manipulating the DOM in the controller is bad practice, but you are just updating an object in the controller - angular is sorting out the DOM which is fine.

Angular ui-router - expose a portion of the layout to a child state/template

Taking the following mockup screenshot as an example, I need to be able to put some custom actions on the top right of the view. What the user sees there will depend on the current view that's being displayed (e.g. share button).
I'm using ui-router, and nested states. I may be approaching it wrong, so my question is more generic than specific. What is the best way to go about exposing a portion of the layout to a child state that may be a few layers deep?
<div>
<header id="menu">
<a class="fa fa-bars"></a>
<div>Title</div>
<div class="actions">
<!-- I want a child state, possibly multiple levels deep in the
state hierarchy, to be able to insert content here -->
</div>
</header>
<div id="content">
<div ui-view></div>
</div>
</div>
I can see a couple of approaches, pick one that feels the best to you:
1) The actions view could be implemented as a directive that watches $state.current and based on the current state, update its view accordingly. If you want the ability for each individual state to specify their own actions, then perhaps setting the data property when using $stateProvider.state, and using that information to determine what actions there are could work.
2) Implement some kind of cross transclude directive. So something like this (untested and not fully implemented, be sure to test for memory leaks):
angular.module('app',[]).directive('crossTransclude', function(){
return {
require : '^crossTranscludeController',
transclude: true,
link: function($scope, $el, $attr, ctrl, transclude){
ctrl.setTranscludeFn(transclude);
$scope.$on('$destroy', function(){
ctrl.unsetTranscludeFn(transclude);
});
}
};
})
.directive('crossTranscludeController', function(){
return {
controller : function CrossTranscludeController(){
this.transcludeFn = null;
this.transcludeTargetEl = null;
this.setTranscludeFn = function(func){
this.transcludeFn = func;
this._crossTransclude();
}
this.unsetTranscludeFn = function(){
this.transcludeFn = null;
this._crossTransclude();
}
this.registerTranscludeTarget = function($element){
this.transcludeTargetEl = $element;
this._crossTransclude();
}
this.deregisterTranscludeTarget = function(){
this.transcludeTargetEl = null;
this._crossTransclude();
}
this._crossTransclude = function(){
if(this.transcludedClone)
if(this.transcludeFn && this.transcludeTargetEl){
this.transcludeFn(function(clone){
this.transcludeTargetEl.empty();
this.transcludeTargetEl.append(clone);
});
} else {
if(this.transcludeTargetEl){
this.transcludeTargetEl.empty();
}
}
}
}
};
})
.directive('crossTranscludeTarget', function(){
return {
require: '^crossTranscludeController',
link: function($scope, $el, $attr, ctrl){
ctrl.registerTranscludeTarget($el);
$scope.$on('$destroy', function(){
ctrl.deregisterTranscludeTarget();
});
}
};
});
Which is used like:
index.html
<div cross-transclude-controller>
<div cross-transclude-target>
</div>
<div ui-view>
</div>
</div>
subview.html
<div>
<div cross-transclude>
This content should end up in 'cross-transclude-target', bound to the subview's scope
</div>
</div>

AngularJS: Refactoring a confirmation modal directive

I need some advice on refactoring a modal directive I have. I am just getting started with directives, so any other approach to my problem is welcome.
My program needs a confirmation modal, where we can confirm or cancel the desired action. It will appear in many places and needs to be able to have a programmable button. Cancel is consistent in that it will only hide the modal, the confirmation button needs to perform whatever action required.
I am currently using $rootScope to show / hide / configure the modal. Is this a bad idea? Please tell me.
This is what I am working with right now (roughly, as I have cut out a lot of the other unnecessary code):
index.html
<!doctype html>
<html lang="en">
<head>
<title>My App</title>
</head>
<body ng-controller="MenuCtrl">
<confirmmodal ng-show="$root.confirmModal.isVisible"></confirmmodal>
<ul>
<li>Home</li>
<li>About</li>
<li>Contact</li>
</ul>
<div ng-view></div>
<!-- build:js scripts/main.js -->
<script data-main="scripts/main" src="lib/requirejs/require.js"></script>
<!-- endbuild -->
</body>
</html>
So my modal sits atop the ng-view and can be called from anywhere. It is inside a pseudo global controller, called MenuCtrl.
Here is the modal directive code:
directives.js
/* Confirm Modal */
.directive('confirmmodal', [function() {
return {
restrict: 'E',
templateUrl: 'view/templates/modal-confirm.tpl.html'
};
}])
It serves as a template for the following code:
modal-confirm.tpl.html
<!-- Confirm Modal Template -->
<div class="overlay">
<div class="overlay-content extended">
<span>{{$root.confirmModal.content}}</span>
<div class="buttons">
<button class="btn btn-default" ng-click="$root.confirmModal.secondary.action()">{{$root.confirmModal.secondary.content}}</button>
<button class="btn btn-primary" ng-click="$root.confirmModal.primary.action()">{{$root.confirmModal.primary.content}}</button>
</div>
</div>
</div>
I set a bunch of defaults in the app.run function:
app.js
app.run(['$rootScope', function ($rootScope) {
_.extend($rootScope, {
confirmModal: {
isVisible: false,
content: '',
primary: {
action: function() {
console.log('hello world');
},
content: 'Submit'
},
secondary: {
action: function() {
$rootScope.confirmModal.isVisible = false;
},
content: 'Cancel'
}
}
});
}]);
So I've also coded a modal trigger directive, the idea being that I can create different triggers that perform different actions with the modal.
directives.js
/* Resolve Event */
.directive('resolveevent', ['RequestService', '$location', function (RequestService, $location) {
return {
restrict: 'A',
scope: {
eventtype: '#',
eventid: '#',
index: '#'
},
controller: ['$scope', function($scope) {
$scope.remove = function(id) {
// remove the event from the events array
$scope.$parent.$parent.$parent.$parent.events.splice(id, 1);
},
$scope.config = function(config) {
_.extend($scope.$root.confirmModal, config);
},
$scope.isVisible = function() {
$scope.$apply(function() {
$scope.$root.confirmModal.isVisible = true;
});
}
}],
link: function( $scope, element, attrs ) {
var config = {
content: 'Are you sure you wish to resolve this event?',
primary: {
action: function() {
var config = {
url: '/Events/' + $scope.eventid,
method: 'PUT',
data: {
event_status: 'resolved'
},
cache: false
}
/* Update event with resolved status */
RequestService.makeApiRequest(config).success(function(response) {
$scope.$root.confirmModal.isVisible = false;
$scope.remove($scope.index);
});
},
content: 'Resolve Event'
}
}
element.on('click', function() {
if (!$scope.$root.confirmModal.isVisible) {
$scope.config(config);
$scope.isVisible();
}
});
}
}
}]);
And then I use a button on the view where my ng-repeat is found which is able to trigger the modal:
eventlist.html
<li ng-repeat="event in events">
<p>Event: {{ event.number }}</p>
<p>Group: {{ event.group_name }}</p>
<p>Record Date: {{ event.event_date | moment: 'MM/DD/YYYY h:mm A' }}</p>
<button resolveevent index="{{$index}}" eventid="{{ event.number }}" class="btn btn-default">Resolve</button>
</li>
This is what I've got, and it is working, however it seems like overkill, inefficient, and a nightmare to maintain. Can anyone chime in on a way to improve this? I appreciate any help, thanks in advance.
You can have a look at the bootstrap-ui project : http://angular-ui.github.io/bootstrap/
If you're using Bootstrap 3, be careful about the templates, and use the version without them. You can download bootstrap3 compliant templates here : https://github.com/angular-ui/bootstrap/tree/bootstrap3_bis2_modalPatch
A simple directive to confirm:
/**
* A generic confirmation for risky actions.
* Usage: Add attributes: ng-really-message="Really?" ng-really-click="takeAction()" function
*/
angular.module('app').directive('ngReallyClick', [function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
element.bind('click', function() {
var message = attrs.ngReallyMessage;
if (message && confirm(message)) {
scope.$apply(attrs.ngReallyClick);
}
});
}
}
}]);
My method might not be according to best practises, but I usually end up creating dedicated service that both has access to modal's scope and manipulates dom. Think of it as self injecting directive.
Here's the modal's container html (uses bootstrap's styling):
<div class="modal-backdrop"></div>
<div class="modal fade">
<div class="modal-dialog" ng-style="{width: width}">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" ng-click="close()" aria-hidden="true">×</button>
<h4 class="modal-title">{{title}}</h4>
</div>
<div class="modal-body">
</div>
<div class="modal-footer">
<button ng-repeat="(name, callback) in buttons" type="button" ng-click="callback()">{{name}}</button>
</div>
</div>
</div>
</div>
Then there's pseudo code of the DialogService:
.service('DialogService', function($compile, $http, $rootScope) {
this.open = function(options) {
//options contain various properties
//e.g. title, width, template or templateUrl, button map with callbacks
loadModalContainer()
.then(loadModalBody)
.then(init);
function init() {
modal = $('body').append(containerHtml).find('.modal');
modal.append(bodyHtml);
scope = (options.scope || $rootScope).$new();
if (options.controller) $controller(options.controller, {$scope: scope});
$compile(modal)(scope);
listenForEscKey();
}
function close() {
//clean up event listeners
//
if (options.onClose) options.onClose();
scope.$destroy();
$('body').find('.modal,.modal-backdrop').remove();
}
}
});
Of course, because of the async nature of the service, you have to implement some auto-close logic if second modal pops-up. From there is really easy, you can define concrete dialogs as separate services to abstract away the details:
.service('TermsModal', function(DialogService) {
this.open = function(acceptCallback, declineCallback, scope) {
DialogService.open({
templateUrl: '',
width: '',
buttons: {
accept: acceptCallback,
decline: declineCallback
},
scope: scope
});
}
})
Then from any controller you can open modal with an one-liner: TermsModal.open(acceptCallback, declineCallback, $scope)
There are several issues. First of all, it would be great to use transclusion, since now modal's child scope is littered with title, buttons, width properties.
Another thing is that I pass around modal body's width, but that's just my laziness (I cannot style bootstraps modal body width properly since it's hardcoded).
Also, I pass around local scopes from controllers because very often modal's body content is in one or another way related to the controller that invokes the modal. If, say, we have ItemController with item as scope property and we have an edit button to edit item's value in a modal, the child scope has to know about the model it's dealing with. So either it's passing around scope or passing needed values directly in options. I prefer scope because that gives more flexibility and with child scope intialization it is really hard to mess up orginal model.
All in all, the power and flexibility this set-up gives justifies the fact that service is messing a bit with the DOM. Your rootScope becomes free of global state (the service manages its own state without giving details to the outside world) and your main template is free of modal partials/directives/whatever that may or may not be used.
I have created a small confirmation directive which, opens a modal and executes the code you want, if the modal is confirmed:
app.html
<button type="button" class="btn btn-default"
nait-confirm-click
confirm="Do you really want to remove this record?"
confirm-if="user.disabled == true"
do="remove(user)">
Remove
</button>
script.js
angular
.module('xyz', ['ui.bootstrap'])
.directive('naitConfirmClick', function($modal, $parse) {
return {
restrict: 'EA',
link: function(scope, element, attrs) {
if (!attrs.do) {
return;
}
// register the confirmation event
var confirmButtonText = attrs.confirmButtonText ? attrs.confirmButtonText : 'OK';
var cancelButtonText = attrs.cancelButtonText ? attrs.cancelButtonText : 'Cancel';
element.click(function() {
// action that should be executed if user confirms
var doThis = $parse(attrs.do);
// condition for confirmation
if (attrs.confirmIf) {
var confirmationCondition = $parse(attrs.confirmIf);
if (!confirmationCondition(scope)) {
// if no confirmation is needed, we can execute the action and leave
doThis(scope);
scope.$apply();
return;
}
}
$modal
.open({
template: '<div class="modal-body">' + attrs.confirm + '</div>'
+ '<div class="modal-footer">'
+ '<button type="button" class="btn btn-default btn-naitsirch-confirm pull-right" ng-click="$close(\'ok\')">' + confirmButtonText + '</button>'
+ '<button type="button" class="btn btn-default btn-naitsirch-cancel pull-right" ng-click="$dismiss(\'cancel\')">' + cancelButtonText + '</button>'
+ '</div>'
})
.result.then(function() {
doThis(scope);
scope.$apply()
})
;
});
}
};
})
;
Now, if you click on the button with the nait-confirm-click it opens a modal with two buttons and the text you have passed by the confirm attribute. If you click the cancel button, nothing will happen. If you confirm by clicking "OK", the expression, which you have passed by the do attribute, will be executed.
If you pass an expression in the optional confirm-if attribute, the modal will only be opened if the expression is true. If the expression is false, the action will be executed without asking.
I hope this snippet will help someone ;)

Resources