I am trying to create a custom directive which extends the functionality of an existing element. I would like to detect if a certain attribute exists and if not then add it (e.g. ng-class).
I have tried to achieve this during pre-compilation but angular does not react to the addition of the new attribute.
I created a plunker with a simple example using ng-hide.
<input hide type="submit" value="Submit"/>
app.directive('hide', function() {
return {
restrict: 'A',
compile: function(){
return {
pre: function(scope, element, attributes, controller, transcludeFn){
attributes.$set("ng-hide", true);
},
post: function(scope, element, attributes, controller, transcludeFn){
}
}
},
};
});
If I add ng-hide="true" in the html then the submit button is hidden correctly. If I leave it to the directive then I can see that the DOM has the element set up correctly but the element is not hidden:
<input hide="" type="submit" value="Submit" ng-hide="true">
Any help appreciated!
You can do this in the linking function by
Setting the directive's priority high, so it runs before all others.
Set it to terminal, so others don't run after it.
Recompile the element after you make changes to it (such as adding attributes)
For example:
app.directive('hide', function($compile) {
return {
restrict: 'A',
priority: 10000,
terminal: true,
link: function(scope, element, attrs) {
attrs.$set('ngHide', true);
attrs.$set('hide', null);
$compile(element)(scope);
}
};
});
As can be seen on http://plnkr.co/edit/tHNvCxVn2wURO38UtI0n?p=preview
Related
I have a very simple directive (named cooperate-with-ng-change) which requires ng-model and I'd like it to cooperate with ng-change.
Restated, I'd like to be able to do <cooperate-with-ng-change ng-model="model" ng-change="changeHandler()"></cooperate-with-ng-change>
However I'm noticing when changeHandler() is fired, model is the old value and not the new one.
Here's the directive definition object:
return {
restrict: "E",
require: ["ngModel"],
scope: {
"value": "=ngModel"
},
template: "<label>Cooperate with NgChange: <input type='text' ng-model='value' ng-model-options='{debounce: 500}' /></label>",
link: function(scope, element, attrs, ctrls) {
var inputNgModelCtrl = element.find("input").controller("ngModel");
var parentNgModelCtrl = ctrls[0];
Array.prototype.push.apply(inputNgModelCtrl.$viewChangeListeners, parentNgModelCtrl.$viewChangeListeners)
//if I wrap all the parentNgModelCtrl.$viewChangeListeners in a timeout and digest in changeHandler
//then message matches afterDebounce
/*Array.prototype.push.apply(inputNgModelCtrl.$viewChangeListeners,
parentNgModelCtrl.$viewChangeListeners.map(
function(listener) {
return function() {
setTimeout(listener, 1)
}
}))
*/
}
}
Here's a plunker link if you want to play with it
My guess is $viewChangeListeners gets called before the model value is set, however I can't find what might correspond for after the model value is set.
I have the following ngRepeat code:
<div ng-repeat="drug in drugs | orderBy:'drugName'">
<input id="{{drug.drugName}}"
class="drugCheckbox"
name="{{drug.drugName}}"
type="checkbox" value="{{drug.drugName}}"
ng-model="foobar"
validate-foo
/>
{{drug.drugName}}
<!-- this is the error message, one per each repeat element -->
<span style="color:red" ng-show="myForm.{{drug.drugName}}.$error.summary">
Fill in the summary
</span>
<input type="text"
name="summary-{{drug.drugName}}"
id="summary-{{drug.drugName}}"
placeholder="Summary"/>
</div>
My validateFoo directive:
app.directive('validateFoo', function(){
return{
restrict: 'A',
require: 'ngModel',
link: function(scope, elem, attr, ctrl) {
ctrl.$parsers.unshift(function (viewValue) {
var id= attr.id;
//if the checkbox is checked, see if the text field has been filled in
if(viewValue) {
var val= document.getElementById('summary-' + id).value;
if(val.length == 0) {
ctrl.$setValidity("summary", false);
return undefined;
}
else {
ctrl.$setValidity(id, true);
return viewValue;
}
}
});
}
}
});
I cannot get the validation to work for any of the checkboxes, I am not clear on what to use for "ng-show" in the error message span element.
First of all that's because all fields will be registered in your formController under name {{drug.drugName}}.
Second thing is that there is no myForm.{{drug.drugName}}.$invalid in scope. You probably tried to do myForm[drug.drugName].$invalid. But it will not work anyway - look at 1.
I think that there still is no proper way to set name field dynamically in ng-repeat. Instead you need to create tour own directive, that will build repeated list elements as String, then append it to your HTML and $compile it place.
EDIT
I have developed one solution:
http://jsfiddle.net/ulfryk/gejfeLp7/
EDIT 2:
I've developed even simpler solution ( http://jsfiddle.net/ulfryk/5wq7539r/ ):
app.directive('fixRepeatedModelName', function () {
return {
link: function (scope, element, attrs, ngModelCtrl) {
if (!attrs.name) return;
ngModelCtrl.$name = attrs.name;
},
priority: '-100',
require: 'ngModel'
};
});
And add fix-repeated-model-name attribute to any ng-model with dynamic ({{ }} dependent) name placed inside ng-repeat. And it now works :)
I have just started learning Angular and have set up Angular routing for my first app.
I have a shell page and some sub pages, for example products.html, help.html etc
the sub pages contains forms with bootstrap switches and validation, which for some reason stops working (the switch is rendered as a regular checkbox and validation does not run - please note that I am not using Angular JS).
However, if a place the same code directly in the shell page it works fine.
How do I get the sub pages to behave exactly like they the code in the shell page?
Basically, if I have the following code in a form in one of my subpages help.html:
<div class="form-group">
<label for="active">My checkbox<br />
<div class="switch">
<input type="checkbox" value="1" id="active" name="active">
</div>
</div>
...the switch does not render correctly, but if I move the code directly to the shell page it renders correctly.
So what is the difference in what happens in the sub page (which is shown in a on the shell page) or somw code that is placed directly in the shell page HTML.
I am assuming you are using this Bootstrap Switch plugin.
I am also assuming that you are initialising the switches that work on the shell page by doing something like:
$(function () {
$('input[type="checkbox"]').bootstrapSwitch();
});
The problem with this is that it will only apply the bootstrap switch plugin to the checkboxes that it finds on the first page load, not after you change pages within an ng-view element.
What I recommend you do instead is to create a simple AngularJS directive to apply the bootstrap switch plugin to the checkboxes. The reason this will work is that Angular will compile the contents of a view every time you change pages and the link() functions of all the directives found will be run. Thus, your checkboxes, if they use this new directive, will always have the plugin applied correctly.
The directive could be as simple as:
app.directive('bsSwitch', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ngModelCtrl) {
$(element).bootstrapSwitch({
onSwitchChange: function(event, state) {
scope.$apply(function() {
ngModelCtrl.$setViewValue(state);
});
}
});
}
}
});
Then change your markup in the views to be:
<div class="form-group">
<label for="active">My checkbox</label>
<br />
<div class="switch">
<input type="checkbox" value="1" id="active" name="active" bs-switch>
</div>
</div>
EDIT: If you wish to apply the bootstrap switch to all checkboxes on the application without the need for additional attributes, you could instead create a directive that will apply to all <input>s, and then just check if they are checkboxes.
Like so:
app.directive('input', function() {
return {
restrict: 'E',
require: 'ngModel',
link: function(scope, element, attrs, ngModelCtrl) {
if (attrs.type === 'checkbox')
$(element).bootstrapSwitch({
onSwitchChange: function(event, state) {
scope.$apply(function() {
ngModelCtrl.$setViewValue(state);
});
}
});
}
}
});
And then you can omit the bs-switch attribute in your markup.
See my working Plunkr example (Plunkr is a better tool than jsFiddle for Angular examples, and allows you to create multiple HTML, CSS and JS files).
EDIT 2: As Mark pointed out, it was broken if the checkbox was initially checked. Here is a fixed version of the directive (and an updated Plunkr):
// As an element directive that will apply to all checkbox inputs:
// <input type="checkbox" ng-model="blah">
app.directive('input', function() {
return {
restrict: 'E',
require: 'ngModel',
link: function(scope, element, attrs, ngModelCtrl) {
if (attrs.type === 'checkbox' && !Object.hasOwnProperty(attrs, 'bsSwitch')) {
$(element).bootstrapSwitch({
onSwitchChange: function(event, state) {
scope.$apply(function() {
ngModelCtrl.$setViewValue(state);
});
}
});
var dereg = scope.$watch(function() {
return ngModelCtrl.$modelValue;
}, function(newVal) {
$(element).bootstrapSwitch('state', !! newVal, true);
dereg();
});
}
}
}
});
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'.
I am a angularjs newbie, and I must miss something simple.
What I am trying to do is to create tooltip by using angular ui. I create a customer directive, in which it will add 3 angular directives to the attribute of the element based on the placeholder value:
myApp.directive('ngTooltip', function () {
return{
restrict: 'A',
link: function (scope, element, attrs) {
attrs.$set('tooltip', attrs['placeholder']);
attrs.$set('tooltip-placement', 'bottom');
attrs.$set('tooltip-trigger', 'focus');
}
}
});
In my markup, I have
and it got rendered as expected:
<input name="test" placeholder="this is test" tooltip="this is test" tooltip-placement="bottom" tooltip-trigger="focuse" />
However, the tooltip does not work. If I directly apply the 3 tooltip attributes to the markup, the tooltip works.
Looks like the 3 directives added by the custom directive do not get evaluated by angularjs.
Any ideas?
You can't dynamically add directives without recompiling the element with $compile, which will cause an infinite loop unless you resort to some workaround. There is an easier way to take care of this: declare a directive template and AngularJS will handle the directives properly.
myApp.directive('ngTooltip', function () {
return{
restrict: 'A',
template: '<input tooltip tooltip-placement="bottom" tooltip-trigger="focus">',
replace: true,
link: function (scope, element, attrs) {
attrs.$set('tooltip', attrs['placeholder']);
}
}
});
Working example: plunker.