I'm trying to take a form button that says Save to change to Saving... when it's busy. It'd be awesome if this could detect that there's an ng-click directive here and only trigger that directive if busy is false. Would I need to create a new directive for this, or is there a way to just tap into ng-click's functionality?
HTML:
<button-large color="green" ng-click="createWorkstation()" busy="disableSave()" busyLabel="Saving...">Save</button-large>
JS:
directive('buttonLarge', function () {
return {
scope: {
busy: '&'
},
replace: true,
restrict: 'E',
transclude: true,
template: '<button type="checkbox" class="buttonL" ng-transclude/>',
link: function (scope, element, attrs) {
var config = {
color: "Default"
};
angular.extend(config, attrs);
element.addClass("b"+capitalize(config.color));
//when the button is busy, disable the button
scope.$watch(attrs.busy, function () {
console.log('changed', scope.busy);
});
//capitalize first letter of string
function capitalize(s) {
return s[0].toUpperCase() + s.slice(1);
}
}
}
})
I'd just do it like this:
<button ng-click="createWorkstation()">{{isBusy && "Saving" || "Save"}} </button>
Where isBusy can just be a boolean that you are changing in your scope (or a function I guess) while you are processing/etc. This doesn't require a directive and keeps the wording in the markup. You could probably extend this to having a service or constant for the strings/etc but that just depends on how far you want to take it.
** update **
If you want to bind html per one of the comments you'd use the ng-bind-html-unsafe directive:
<button ng-click="go()" ng-bind-html-unsafe="isBusy && '<span class=icol-refresh></span><span>Saving...</span>' || 'Save'">Process</button>
Related
Assume I have a directive my-button with an optional attribute disabled. People could use this directive like:
<my-button>Button text</my-button>
or
<my-button disabled="variable">Button Text</my-button>
Should I be watching for a disabled attribute? Could these usages somehow transition from one to the other?
In response to JB Nizet's request for the code in question, here's a clean version of the directive function:
function ButtonDirective() {
var directive = {
link: link,
restrict: 'E',
scope: {
click: '&',
disabled: '=?',
},
template: '<a class="my-button" ' +
'data-ng-class="{\'my-button-disabled\': disabled}" ' +
'data-ng-click="disabled || click()" ng-transclude />',
transclude: true
};
function link(scope) {
if (typeof scope.disabled == 'undefined') scope.disabled = false;
}
return directive;
}
The directive creates an anchor tag styled as a button. It accepts two properties/parameters: click and disabled. The latter is optional. When disabled, the click event should fire when clicked, otherwise the the click event should fire when clicked.
To reiterate: Should I worry about someone somehow adding, removing, or modifying the disabled attribute after the fact? If so, how should I go about it?
After hashing things out with JB Nizet, he counseled me to not worry about the HTML attribute changing.
How do you get the value of the binding based on an angular js directive restrict: 'A'?
<span directiverestrict> {{binding}} </span>
I tried using elem[0].innerText but it returns the exact binding '{{binding}}' not the value of the binding
.directive('directiverestrict',function() {
return {
restrict:'A',
link: function(scope, elem, attr) {
// I want to get the value of the binding enclosed in the elements directive without ngModels
console.log(elem[0].textContent) //----> returns '{{binding}}'
}
};
});
You can use the $interpolate service, eg
.directive('logContent', function($log, $interpolate) {
return {
restrict: 'A',
link: function postLink(scope, element) {
$log.debug($interpolate(element.text())(scope));
}
};
});
Plunker
<span directiverestrict bind-value="binding"> {{binding}} </span>
SCRIPT
directive("directiverestrict", function () {
return {
restrict : "A",
scope : {
value : '=bindValue'
},
link : function (scope,ele,attr) {
alert(scope.value);
}
}
});
During the link phase the inner bindings are not evaluated, the easiest hack here would be to use $timeout service to delay evaluation of inner content to next digest cycle, such as
$timeout(function() {
console.log(elem[0].textContent);
},0);
Try ng-transclude. Be sure to set transclude: true on the directive as well. I was under the impression this was only needed to render the text on the page. I was wrong. This was needed for me to be able to get the value into my link function as well.
I was simply trying to add functionality to an existing directive so I can track any changes that occur. I have a toggle control which is just used to toggle a boolean value. Here is my directive
app.directive('skToggle', function ($timeout) {
return {
replace: true,
restrict: 'A',
require: 'ngModel',
scope: {
ngModel: '=',
skcallback: '&callback',
disabled: '=',
emit: '#',
positive: '#',
negative: '#',
skTouched:'=?' //THIS IS NEW
},
template: '<div class="toggle" ng-class="{ disabled: disabled }" ng-click="disabled || toggle($event)">\
<div class="off">\
<span>{{neg}}</span>\
</div>\
<div class="on" ng-class="{ active: ngModel }">\
<span>{{pos}}</span>\
<div class="control"></div>\
</div>\
</div>',
link: function (scope, elem, attrs, ctrl) {
var hasCallback = angular.isDefined(attrs.callback);
scope.pos = scope.positive || "YES"
scope.neg = scope.negative || "NO";
scope.toggle = function (e) {
if (hasCallback) {
scope.skcallback({ event: e });
} else {
ctrl.$setViewValue(!ctrl.$viewValue);
}
if (scope.emit === 'true') {
$timeout(function () {
scope.$emit('toggle');
});
}
}
// THIS IS ALSO NEW
scope.$watch('ngModel', function(newVal, oldVal){
if(scope.skTouched && oldVal !== undefined && newVal !== oldVal){
scope.skTouched.UI.$dirty = true;
}
});
}
}
});
I have commented on which parts of the directive are new.. all I did was add a two-way binding on my directive that takes an object and updates the UI.$dirty property on it. The problem is when I print out the object on the screen, the UI object never gets updated on the parent $scope. I don't know if I'm just spacing on something easy or if I am doing something wrong but, my directive (child) scope is not updating the parent scope like it should be.
<div sk-toggle ng-model="feature.enabled" sk-touched="feature"></div>
So I realized the problem was with using $dirty as my variable. Angular must reserve $dirty for only use with form controllers, which explains why it wasn't showing up when I printed the entire object out on the page. Simply changing the variable to dirty made it work as expected
ngModel is a core angular directive. It may be that there is a conflict with your using ngModel as an attribute to use two-way databinding on.
You may be able to get around this by setting the priority of your directive to be higher than that of ngModel. Add a priority of something > 1 in your directive definition. See the docs for more info on priority.
LIVE DEMO
Consider the following myButton directive:
angular.module("Demo", []).directive("myButton", function() {
return {
restrict : "E",
replace: true,
scope: {
disabled: "="
},
transclude: true,
template: "<div class='my-button' ng-class='{ \"my-button-disabled\": disabled }' ng-transclude></div>",
};
});
which can be used like this:
<my-button disabled="buttonIsDisabled"
ng-click="showSomething = !showSomething">
Toggle Something
</my-button>
How could I stop ng-click from executing when buttonIsDisabled is true?
PLAYGROUND HERE
You could use capture (addEventListener's optional third parameter) and stop the propagation of the event (using stopPropagation).
"Capture" allows you to catch the event before it reaches the "bubble" phase (when the triggering of "normal" event-listeners happens) and "stopPropagation" will...stop the propagation of the event (so it never reaches the bubbling phase).
element[0].addEventListener('click', function (evt) {
if (scope.disabled) {
console.log('Stopped ng-click here');
evt.preventDefault();
evt.stopPropagation();
}
}, true);
See, also, this short demo.
Why not use the actual button for your button. You could change your directive to:
angular.module("Demo", []).directive("myButton", function() {
return {
restrict : "E",
replace: true,
scope: {
disabled: "="
},
transclude: true,
template: "<button class='my-button' ng-class='{ \"my-button-disabled\": disabled }' ng-disabled='disabled' type='button' ng-transclude></button>"
};
});
Then style it to look like your div. See the Short Example I've made.
Try this in your link function:
link: function(scope, element, attrs) {
var clickHandlers = $._data(element[0]).events.click;
clickHandlers.reverse(); //reverse the click event handlers list
element.on('click', function(event) {
if (scope.disabled) {
event.stopImmediatePropagation(); //use stopImmediatePropagation() instead of stopPropagation()
}
});
clickHandlers.reverse(); //reverse the list again to make our function at the head of the list
}
DEMO
This solution uses jQuery to deal with cross browser problems. The idea here is to attach our event handler at the head of the click handlers list and use stopImmediatePropagation() to stop current handlers of the same event and bubbling event.
Also take a look at this: jquery: stopPropagation vs stopImmediatePropagation
<my-button disabled="buttonIsDisabled"
ng-click="showSomething = buttonIsDisabled ? showSomething : !showSomething">
or
<my-button disabled="buttonIsDisabled"
ng-click="showSomething = buttonIsDisabled ? function(){} : !showSomething">
Is this too simple?
Some actions in my Angular app require the user to be registered. If the user is not registered we want to show a "Register modal" and prevent the original action.
Those actions can be triggered via ng-click or any other "click binding" directive (for example the 'modal-toggle' one).
So I found this solution: https://stackoverflow.com/a/16211108/2719044
This is pretty cool but only works with ng-click.
I first wanted to make the "terminal" property of the directive dynamic but couldn't manage to do it.
So the idea was to set "terminal" to true and manually prevent default click action in the directive.
Here is my DOM
<!-- This can work with terminal:true and scope.$eval(attrs.ngClick) (see example above) -->
<div user-needed ng-click="myAction()">Do it !</div>
<!-- This doesn't work. I can't manage to prevent the modal-toggle to be executed -->
<div user-needed modal-toggle="my-modal-id-yey">Show yourself modal !</div>
And my directive(s) (which don't work...)
// First try (with terminal:true)
app.directive('userNeeded', function() {
return {
priority: -100,
terminal: true,
restrict: 'A',
link: function(scope, element, attrs) {
element.bind('click', function(e) {
if(isRegistered()) {
// Here we do the action like scope.$eval or something
}
});
}
};
});
// Second try (with stopPropagation)
app.directive('userNeeded', function() {
return {
priority: -100
restrict: 'A',
link: function(scope, element, attrs) {
element.bind('click', function(e) {
if(!isRegistered()) {
e.stopPropagation();
}
});
}
};
});
...And that's why I'm here. Any idea ?
Thanks a lot.
You were extremely close. Instead of stopPropagation you needed stopImmediatePropagation. The difference between the two is summarized in this StackOverflow answer by #Dave:
stopPropagation will prevent any parent handlers from being
executed while stopImmediatePropagation will do the same but
also prevent other handlers from executing.
So to fix the code, all we have to do is swap out that method and VoilĂ :
app.directive('userNeeded', function() {
return {
priority: -100
restrict: 'A',
link: function(scope, element, attrs) {
element.bind('click', function(e) {
if(!isRegistered()) {
e.stopImmediatePropagation();
}
});
}
};
});
Here is an example Plunker of the working code. In the example I modified the directive slightly to allow specific events to be specified (such as user-needed="submit") by passing the value directly to the element.bind function; however, it defaults to 'click'.