Retrofitting "alert()" on delete buttons - angularjs

I have an angular application which contains several delete buttons somewhat like this:
<button class="btn btn-default" ng-click="delete($index)">x</button>
Now as we are nearing production deployment and I want delete buttons to behave nicely and "alert()" the user before actually deleting the object.
How can I retro-fit this feature in all the delete buttons through a directive. I would like to have a directive called "ask":
<button ask class="btn btn-default btn-xs" ng-click="delete($index)">x</button>
which I can use to affect the behaviour of any button.
Somehow I am not able to think through this
app.directive("ask", function() {
return function(scope, elems, attrs) {
// what to do here:
// 1. shall I change the elems.attr("ng-click")
// 2. or scope.attrs("ngClick")
????
}
});
Please guide me with the best practice and some code samples. Also note that the ng-click callback of all the delete buttons are different and the application already makes extensive use of isolate scope and child scope directives.

Try this implementation
angular
.module('test', [])
.directive('ask', function() {
return {
restrict: 'A',
scope: {ask: '#', onAsk: '&'},
link: function(scope, elem, attrs) {
elem.on('click', function(e) {
if(confirm(attrs.ask)) {
scope.onAsk();
scope.$apply();
}
});
}
};
})
.controller('ItemsCtrl', function($scope) {
$scope.items = [1,2,3,4,5];
$scope["delete"] = function(index) {
$scope.items.splice(index, 1);
};
})
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="test" ng-controller="ItemsCtrl">
<ul><li ng-repeat="item in items track by $index">{{item}}
<button ask="Are you sure you want to delete item?" on-ask="delete($index)">x</button>
</li>
</ul>
</div>

You can build a directive for this and in template of directive have button element and the ng-click event.
Pass index and delete function as inputs to this directive.
app.directive("deleteFruit", function(){
return{
// You can also pass one more binding if you want to have a specific alert message
scope:{
index: '#', delete:'&'
},
template:'<button class="btn btn-default" ng-click="deleteFruit()">x</button>',
link: function(scope){
scope.deleteFruit = function(){
// When user clicks, first call the alert function then use the promise returned by dialog and check whether user wants to delete or not
//modal.dialog().then(function(userSelectedOption){
// If user wants to delete then go a head and call delete function on main controller
// if(userSelectedOption){
// scope.delete(scope.index);
// }
//})
scope.delete(scope.index);
}
}
}
})
MainCtrl HTML
<delete-fruit delete="delete(index)" index={{$index}}></delete-fruit>
Plunker.

Related

AngularJs toggle template with directive to create notification like stackoverflow

I'm trying to create a notification system in AngularJs just like the notification used here. When there is a new comment, answer, etc.. The archive icon shows a red sign with the number of activities, and when I click on it, it opens up a box with the last notifications.
To do this, I built this simple directive to dynamic loads a templateUrl:
html:
<li test-alert ref="msg">
<i class="fa fa-envelope-o"></i>
</li>
<li test-alert ref="bell">
<i class="fa fa-bell-o"></i>
</li>
directive:
angular
.module('agApp')
.directive('testAlert', testAlert)
;
/* #ngInject */
function testAlert() {
var templateA = '<div>Test template A</div>';
var templateB = '<div>Test template B</div>';
return{
restrict: 'A',
scope: {
ref: '#'
},
link: function(scope,element,attrs,controller){
scope.showAlert = false;
element.on("click", function() {
if (scope.ref == 'bell') {
scope.showAlert = true;
element.append(templateA);
scope.$apply();
} else {
scope.showAlert = true;
element.append(templateB);
scope.$apply();
};
console.log(scope.ref);
});
element.addEventListener("keyup", function(e) {
if (e.keyCode === 27) {
scope.showAlert = false;
}
});
}
};
}; //end test alert
But I'm with some problems..
If i click on the icon to open the template it will open, but every time i click on it, it will append another template. Id' like it to change (if it's the other template) or do nothing.
When it is opened, I can't make it close. I can use a 'close' button, but I'd like to close/remove the template when the user click on the document or press esc;
The code I tried to use to close on 'Esc' key, doesn't work.
My main objective is to create a notification system just like the one in stackOverflow, so is this the best way to do it? Should I use a controller instead?
Edit:
Close mechanism I'm using at the momment. It's working, but maybe it can be improved.
run.js
angular
.module('agApp')
.run(runApp);
/* #ngInject */
function runApp($rootScope) {
document.addEventListener("keyup", function(e) {
if (e.keyCode === 27) {
$rootScope.$broadcast("escapePressed", e.target);
};
});
document.addEventListener("click", function(e) {
$rootScope.$broadcast("documentClicked", e.target);
});
}; //end run
controller.js
$rootScope.$on("documentClicked", _close);
$rootScope.$on("escapePressed", _close);
function _close() {
$scope.$apply(function() {
vm.closeAlert();
});
};
Since I wasn't able to use it as a directive, I moved the open/close function inside a controller. But it can be used in any other way, as long as it works, there is no problem.
First off, key events only fire on the document and elements that may receive focus.
Directives are really nice for things you need to use multiple times. But even if you implement your notification system as a directive and only use it once - you will have it isolated, which is often good.
Hard to give the best solution without knowing more but here is one example that implements the messages and the notifications as one directive:
app.directive('notifications',
function() {
return {
restrict: 'E',
templateUrl: 'template.html',
scope: {},
link: function(scope, element, attrs, controller) {
scope.viewModel = {
showTemplateA: false,
showTemplateB: false
};
scope.toggleTemplateA = function() {
scope.viewModel.showTemplateA = !scope.viewModel.showTemplateA;
scope.viewModel.showTemplateB = false;
};
scope.toggleTemplateB = function() {
scope.viewModel.showTemplateB = !scope.viewModel.showTemplateB;
scope.viewModel.showTemplateA = false;
};
}
};
});
It simply contains logic for showing and hiding the templates. The directive uses a template that looks like this:
<div>
<i class="fa fa-envelope-o" ng-click="toggleTemplateA()"></i>
<div ng-show="viewModel.showTemplateA">
Template A
</div>
<br>
<i class="fa fa-bell-o" ng-click="toggleTemplateB()"></i>
<div ng-show="viewModel.showTemplateB">
Template B</div>
</div>
The template uses ng-show and ng-click to bind to our scope functions. This way we let Angular do the job and don't have to mess around with element.append etc.
Usage:
<notifications></notifications>
Demo: http://plnkr.co/edit/8M1D5uENjpDoIbb1ZuMR?p=preview
To implement your closing mechanism you can add the following to the directive:
var close = function () {
scope.$apply(function () {
scope.viewModel.showTemplateA = false;
scope.viewModel.showTemplateB = false;
});
};
$document.on('click', close);
$document.on('keyup', function (e) {
if (e.keyCode === 27) {
close();
}
});
scope.$on('$destroy', function () {
$document.off('click', close);
$document.off('keyup', close);
});
Note that you now have to inject $document into the directive:
app.directive('notifications', ['$document',
function($document) {
In the toggle functions you can call stopPropagation() to prevent the global closing handler to execute when you click the icons (might not be needed in this example, but good to know. Might want it on the actual templates in the future?):
scope.toggleTemplateA = function($event) {
$event.stopPropagation();
And:
ng-click="toggleTemplateA($event)"
Demo: http://plnkr.co/edit/LHS4RBE7qtY4yNyEdR16?p=preview

Can angularjs ng-click process events during the capturing phase?

Is it possible to have angularjs ng-click process events during the capturing phase instead of bubbling phase? I want to aggregate data from each of the parent elements in order starting from the parent and ending with the element that was clicked on.
Lets see the source code of ng-click at ngEventDirs.js#L50
As you can see the ng-click and all other event directives using .on().
So, the answer is No, it is not possible.
If you really need it, you could write a custom directive for that. For example, modify the code of ng-click a bit:
.directive('captureClick', function($parse) {
return {
restrict: 'A',
compile: function(element, attrs) {
var fn = $parse(attrs.captureClick);
return function(scope, element) {
element[0].addEventListener('click', function(event) {
scope.$apply(function() {
fn(scope, {
$event: event
});
});
}, true);
};
}
}
});
and use it like this:
<div title="A" ng-click="onBubbled($event)" capture-click="onCaptured($event)">
<div title="B" ng-click="onBubbled($event)" capture-click="onCaptured($event)">
<div title="C" ng-click="onBubbled($event)" capture-click="onCaptured($event)">
Yo!
</div>
</div>
</div>
Example Plunker: http://plnkr.co/edit/SVPv0fCNRQX4JXHeL47X?p=preview

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 ;)

How to trigger a directive when updating a model in AngularJS?

I found a good solution for inline editing content in angular js that is created by running ng-repeat on a model: https://stackoverflow.com/a/16739227/2228613
To expand on that solution I added a button to the page that has a ng-click directive as so:
<button ng-click="addCategory()" class="btn btn-large btn-primary" type="button">
<i class="icon-white icon-plus"></i> Add Category
</button>
The addCategory function is defined in my controller:
$scope.addCategory = function(){
var newCategory = {id:0, name:"Category Name"};
$scope.categories.unshift(newCategory);
}
The goal here is to allow the user to add a new record and automatically trigger the inline-edit directive once the view is updated with the new row. How can I trigger the inline-edit directive in such a manner?
One technique that i've used is to have a boolean change values and have a $watch on it inside the directive that needs to be triggered.
myApp.directive('myDirective', function () {
return function (scope, element, attr) {
scope.$watch('someValue', function (val) {
if (val)
// allow edit
else
// hide edit
});
}
});
Then in your controller you'd set $scope.someValue = true; in your ng-click for the button.
plunker: http://plnkr.co/edit/aK0HDY?p=preview
UPDATE
I've gone a bit further with the above answer. I've made something more along the lines with what you're after.
Here's the plunk for it: http://plnkr.co/edit/y7iZpb?p=preview
This is the new directive:
.directive('editCar', function ($compile) {
return {
restrict: 'E',
link: function (scope, element, attr) {
var template = '<span class="car-edit">'+
'<input type="text" ng-model="car.name" />' +
'<button ng-click="someValue = false" class="btn btn-primary">Save</button></span>';
scope.$watch('someValue', function (val) {
if (val) {
$(element).html(template).show();
$compile($('.car-edit'))(scope);
}
else
$(element).hide();
});
}
}
})
It replaces the <edit-car></edit-car> element with the above template. The save button adds the values to an array called editedCars. I've left in some dummy code for submitting the entire thing using $http.post()
I have one possible solution for you: http://plnkr.co/edit/uzuKki (I worked on the original plunk as you mentioned.)
My idea is
Add "editMode" property to TODO model
Instead of passing in just todo.title to directive's scope, passing in the whole TODO object, which is inline-edit="todo" in index.html
In inline-edit.html, change every editMode to model.editMode (and every model to model.title to display title correctly)
In your controller's add method, create new object with editMode = true, e.g.
var newTodo = {id:0, name:"TODO Name", editMode: true};
$scope.todos.unshift(newTodo);

Using ng-click vs bind within link function of Angular Directive

In the link function, is there a more "Angular" way to bind a function to a click event?
Right now, I'm doing...
myApp.directive('clickme', function() {
return function(scope, element, attrs) {
scope.clickingCallback = function() {alert('clicked!')};
element.bind('click', scope.clickingCallback);
} });
Is this the Angular way of doing it or is it an ugly hack? Perhaps I shouldn't be so concerned, but I'm new to this framework and would like to know the "correct" way of doing things, especially as the framework moves forward.
You may use a controller in directive:
angular.module('app', [])
.directive('appClick', function(){
return {
restrict: 'A',
scope: true,
template: '<button ng-click="click()">Click me</button> Clicked {{clicked}} times',
controller: function($scope, $element){
$scope.clicked = 0;
$scope.click = function(){
$scope.clicked++
}
}
}
});
Demo on plunkr
More about directives in Angular guide. And very helpfull for me was videos from official Angular blog post About those directives.
I think it is fine because I've seen many people doing this way.
If you are just defining the event handler within the directive,
you do not have to define it on the scope, though.
Following would be fine.
myApp.directive('clickme', function() {
return function(scope, element, attrs) {
var clickingCallback = function() {
alert('clicked!')
};
element.bind('click', clickingCallback);
}
});
Shouldn't it simply be:
<button ng-click="clickingCallback()">Click me<button>
Why do you want to write a new directive just to map your click event to a callback on your scope ? ng-click already does that for you.
You should use the controller in the directive and ng-click in the template html, as suggested previous responses. However, if you need to do DOM manipulation upon the event(click), such as on click of the button, you want to change the color of the button or so, then use the Link function and use the element to manipulate the dom.
If all you want to do is show some value on an HTML element or any such non-dom manipulative task, then you may not need a directive, and can directly use the controller.
In this case, no need for a directive. This does the job :
<button ng-click="count = count + 1" ng-init="count=0">
Increment
</button>
<span>
count: {{count}}
</span>
Source: https://docs.angularjs.org/api/ng/directive/ngClick
myApp.directive("clickme",function(){
return function(scope,element,attrs){
element.bind("mousedown",function(){
<<call the Controller function>>
scope.loadEditfrm(attrs.edtbtn);
});
};
});
this will act as onclick events on the attribute clickme

Resources