AngularJS ngIf prevents finding element inside directive - javascript

I have an AngularJS directive that includes an ngIf and I would like to modify some of the DOM inside the ngIf in the directive link function. Unfortunately it seems that ngIf prevents me from finding DOM elements within it in the link function.
Here is the code for the directive:
directive('column', function () {
return {
templateUrl: 'views/column.html',
restrict: 'E',
scope: {
column: '='
},
controller: ['$scope', function ($scope) {
$scope.editing = true;
$scope.toggleEditing = function () {
$scope.editing = !$scope.editing;
};
}],
link: function postLink(scope, element) {
var select = element.find('select');
console.log(select); // See if it can find the select element
// var types = scope.column.types();
// add types as options to the select element
}
};
});
And here is the simplified html of the directive:
<div class="column">
<div>{{ column.title }}</div>
<form name="columnForm" role="form" ng-if="editing">
<select></select>
</form>
</div>
Here is the link to the jsFiddle example http://jsfiddle.net/dedalusj/Y49Xx/1/
The element.find call in the link function returns an empty array but as soon as I remove the ngIf from the form it returns the proper select DOM element. I have the feeling that I'm doing this the wrong way.
UPDATE
Thanks for the answers but I found another solution. I simply created another directive that encapsulate the form, added it to the column directive template with ng-if="editing".
The form directive doesn't have it's own scope so it effectively operates out of the column directive scope and has always access to the select element because it's inside its DOM tree. I pay the cost of an extra directive but I don't have to use the $timeout hack. I created a new jsFiddle to illustrate the solution http://jsfiddle.net/dedalusj/nx3vX/1/
Thanks #Michael but I can't simply use the ng-option because the types array comes from an XML file and its elements are other angular.element objects which cannot be inserted easily with ng-option.

The ngIf directive works by using Angular's transclusion feature. What happens during the compile/link cycle is:
The content inside the ngIf is removed from the DOM when it is compiled
Angular runs the link functions. The ngIf's link function is run before
the link function of the directive using it. When ngIf's link function
runs, it uses $scope.$watch() to watch the value of the ng-if
attribute.
Your directive's link function runs, at this point the content of the ngIf is not part of the DOM
The watch set up in step (2) is called, and ngIf will then call the $transclude function to insert the contents of the ngIf into the DOM if the ng-if attribute value is truthy.
Any watch functions, $timeout calls or use of $scope.$evalAsync that you registered in your directive's link function will run.
So if you want to access elements inside the ngIf's content, the code needs to run after step 4 above. This means that any functions registered with $scope.$watch, $timeout or $scope.$evalAsync in your directive's link function will work. For a one-time piece of setup code, I would probably opt for $scope.$evalAsync:
angular.directive('yourDirective', function () {
return {
...
link: function(scope, elem) {
scope.$evalAsync(function () {
// code that runs after conditional content
// with ng-if has been added to DOM, if the ng-if
// is enabled
});
}
};
});

As #moderndegree has said, ngIf removes the element it's applied to from the DOM, so you won't be able to find it when it's not there. But, you could write your directive in a way to workaround that:
controller: function ($scope, $element, $timeout) {
$scope.toggleEditing = function () {
$scope.editing = !$scope.editing;
$timeout(function() {
var select = $element.find('select');
select.append('<option>Value 1</option>')
.append('<option>Value 2</option>')
.append('<option>Value 3</option>');
});
};
}
Updated jsFiddle here.
The trick here is to delay the find() call by using $timeout with a 0 interval in order to wait for Angular to update the DOM.
UPDATE
After giving some more thought to your code, I realize that perhaps you can let Angular do the hard work for you:
Javascript
directive('column', function () {
return {
templateUrl: 'views/column.html',
restrict: 'E',
scope: {
column: '='
},
controller: ['$scope', function ($scope) {
$scope.editing = true;
$scope.toggleEditing = function () {
$scope.editing = !$scope.editing;
};
}],
};
});
HTML
<div class="column">
<div>{{ column.title }}</div>
<form name="columnForm" role="form" ng-if="editing">
<select ng-model="type" ng-options="type for type in column.types"></select>
</form>
</div>
jsFiddle
Now you don't need to worry about finding the select element at the right time and populating it. Angular does all of that for you. :)

You can put your code from the link function inside $timeout.
$timeout(function(){
var select = element.find('select');
console.log(select);
});
Don't forget to inject $timeout in your directive
directive('column', function ($timeout) {

I was facing this same issue and i was able to resolve it using ng-show, this prevents this issue because ngIf removes the element it's applied to the DOM, so you won't be able to find it when it's not there.
so in your case:
<div class="column">
<div>{{ column.title }}</div>
<form name="columnForm" role="form" ng-show="editing">
<select></select>
</form>
will work OK.
Cheers.

Related

AngularJS - Setting a controller's $scope variable in a custom directive that accesses the DOM

I just answered a question here
How i send value when i click button to server and popup directive
His code looked like this
<div ng-controller="IndexController">
<label id="number" >5</label>
<input type="button" ng-click="getUserDetails()" >
</div>
He was trying to access the value of the label element with id #number.
I understand that in Angular, DOM manipulation and querying should be done in a custom directive, and not inside a controller. I therefore gave this solution.
Since you want to access the value of a number, you are interacting with the DOM. The best solution for this is creating custom directives. Here is an example.
angular.module('myApp', [])
.directive('getValue', function({
restrict: 'A',
link: function(scope, elem, attrs) {
elem.bind("click", function() {
scope.value = elem.prev().text();
});
}
});
I know that in a directive's link function, you pass in scope, element and attrs as a param. In this line, I'm trying to set a scope variable to the label's value.
scope.value = elem.prev().text();
But I have a suspicion that I'm not doing it correctly. Since directives are the way to go, how can I do this.
I also checked this as a reference
Easiest way to pass an AngularJS scope variable from directive to controller?
I've read about the controller function inside directives, but I need the link function for getting the value from the DOM.
There's no need for a custom directive for that. ng-click can work:
$scope.getUserDetails = function(e){
$scope.value = e.target.previousElementSibling.innerHTML;
}
HTML
<div ng-controller="IndexController">
<label id="number" >5</label>
<input type="button" ng-click="getUserDetails($event)" >
</div>
$event

Adding an element with directives in Angular

I am adding an element dynamically in Angular. The code is as follows.
myelement.after('<input ng-model="phone" id="phone">');
The element gets added and all works fine. However I think I'm missing a step and Angular does not recognize the new element because when I collect data later, the value of the dynamically added element is not defined.
The reason for adding the element on fly is that the number of inputs is not known from the beginning and user can add as many of them as they want. The actual code is like:
myelement.after('<input ng-model="phone"' + counter + ' id="phone' + counter + '">');
I'm sorry, I should have provided complete sample from the beginning. Please see the following jsfiddle, add some phones (with values) and list them: http://jsfiddle.net/kn47mLn6/4/
And please note that I'm not creating any new directive. I'm only using Angular standard directives, and I prefer not to create a custom directive for this work (unless required).
Assuming that you are adding element to the DOM using directive. You can use built in angular service $compile.
Firstly inject the $compile service to your directive. Then in your link function of the directive, get your myElement after which you want to append your element. Then create your element using angular.element(). Then compile it using $compile service and pass the scope in which you are in now.(Here this scope is the directive scope). After getting the compiled dom element you can just append it after your myElement.
here is an example about how you can add element dynamically from directive:
var elementToBeAdded = angular.element('<input ng-model="phone" id="phone">');
elementToBeAddedCompiled = $compile(elementToBeAdded)(scope);
myElement.append(elementToBeAddedCompiled);
If you add your element using $compile service in your directive, angular will recognize your dynamically added element.
I was trying to accomplish this without using directives, but it seems the best way (and probably the only proper way) of adding multiple elements to DOM in Angular is by defining a custom directive.
I found a very nice example here http://jsfiddle.net/ftfish/KyEr3/
HTML
<section ng-app="myApp" ng-controller="MainCtrl">
<addphone></addphone>
<div id="space-for-phones"></section>
</section>
JavaScript
var myApp = angular.module('myApp', []);
function MainCtrl($scope) {
$scope.count = 0;
}
//Directive that returns an element which adds buttons on click which show an alert on click
myApp.directive("addbuttonsbutton", function(){
return {
restrict: "E",
template: "<button addbuttons>Click to add buttons</button>"
}
});
//Directive for adding buttons on click that show an alert on click
myApp.directive("addbuttons", function($compile){
return function(scope, element, attrs){
element.bind("click", function(){
scope.count++;
angular.element(document.getElementById('space-for-buttons')).append($compile("<div><button class='btn btn-default' data-alert="+scope.count+">Show alert #"+scope.count+"</button></div>")(scope));
});
};
});
//Directive for showing an alert on click
myApp.directive("alert", function(){
return function(scope, element, attrs){
element.bind("click", function(){
console.log(attrs);
alert("This is alert #"+attrs.alert);
});
};
});
in the angular mind you should manipulate the dom from the JS code.
and use ng-* directive
So i don't know you code but i think you just need to do something like :
View
<button ng-click="showAction()">Show the phone input</button>
<input ng-show="showPhone" ng-model="phone" id="phone">
Controller
app.controller('ctrl', [function(){
$scope.showPhone = false;
$scope.showAction = function() {
$scope.showPhone = true;
};
}]);
Outside of Angular you would need to use the event handler to recognize new elements that are added dynamically. I don't see enough of your code to test, but here is an article that talks about this with $.on():
Jquery event handler not working on dynamic content
Here is a good article that will help with directives and may solve your problem by creating your own directive:
http://ruoyusun.com/2013/05/25/things-i-wish-i-were-told-about-angular-js.html#when-you-manipulate-dom-in-controller-write-directives

AngularJS attribute directive. How to add another attribute directive on 'compile'

I would like create an attribute directive which add an icon on a button when it's disabled.
Like this Fiddle
However, I would also add the ng-disabled directive on compile (with the same disabled-button value)
What is the best way ?
If I add the attribute ng-disabled on compile function, it never compile.
So, if I re-compile my element on link function, I have to remove ng-tranclude directive due to an error. More, my events, like ng-click, are triggered twice.
Bonus question: Is it possible to restrict my attribute directive to html elements like <a> or <button> ?
Thx
I'm afraid you cannot add directives dynamically to the element that contains your directive. The reason is that your compile function will be called after Angular has processed the directive's element and determined what the directives are attached to it. Adding another attribute at this point is too late, discovery has already taken place.
There may be ways to do it that I don't know of (and I would be interested in seeing any stable, non-hackish one).
I can suggest an alternative that may suit you: manually place ng-disabled on the button, but for brevity and consistency let the expression of ng-disabled drive your directive, i.e.:
<button ng-click="ctrl.click()" ng-disabled="ctrl.disabled" disabled-button>
Directive code:
.directive('disabledButton', function($parse) {
return {
restrict: 'A',
transclude: true,
scope: {
},
template: '<span ng-show="disabled">X</span><span ng-transclude></span>',
link: function (scope, elem, attrs) {
var disabled = $parse(attrs.ngDisabled);
scope.disabled = false;
scope.$watch(
function() {
return disabled(scope.$parent);
},
function(newval) {
scope.disabled = newval;
}
);
}
};
})
Fiddle: http://jsfiddle.net/3orwupo5/1/
Or you can manually set the disabled property of the button: http://jsfiddle.net/y5ezvj5L/

Angularjs - ng-if - bypass / work around the scope logic

I am aware that use of ng-if dictates that ng-if destroys the scope on the element unlike the use of ng-show or ng-hide.. I however need to use ng-if (ng-show / hide is not an option) because I actually need the element to not render on the page when ng-if is falsely..
I use ng-if as part of a directive template..
My directive
app.directive("myDirective", function ($parse) {
return {
restrict: "E",
scope:{},
controller: function ($scope, $element, $attrs) {
// controller code
},
templateUrl: "template.html",
compile: function(elm, attrs){
var expFn = $parse(attrs.atr1 + '.' + attrs.atr2);
return function(scope,elm,attrs){
scope.$parent.$watch(expFn, function(val){
scope.exp = val;
})
scope.$watch('exp', function(val){
expFn.assign(scope.$parent, val)
})
}
}
}
})
My template
<div ng-click="view = !view">{{exp}}</div>
<div ng-if="view">
<input type="text" ng-model="exp"><br/>
<div class="btn btn-default" ng-click="Submit()">Submit</div>
</div>
Is there a way to "bypass" the ng-if behavior,,, to KEEP the scope,, or recreate it?
I created the attach-if directive for this purpose.
The code is available at https://github.com/Sparks-Creative-Limited/angular-attach-if and, if you use bower, it's registered as a downloadable component with the following details:
angular-attach-if 0.1.0
There is also a demo page to show how it works.
If you are using ng-if it won't create the element at all period. So the directive does not exist in that instance. I know you mentioned ng-hide/show is not an option but I think its the only option. What is your reason for not wanting it to render and keep its scope? which by the way defeats the purpose of the directive. If you are doing some logic in the directive that doesn't require the view, it should belong somewhere else.
If the scope has single/few variable, ng-init="obj=$parent.obj" on the same tag where ng-if is, would help.

How to use ng-bind on html that is inserted with innerHTML inside a directive

I made a custom directive that had two ng-repeats inside of it. One ng-repeat was nested inside the other one. I got the code to work, and it performs well in chrome but on the iPad and iPhone it is sluggish.
There are 10 sections with 5 rows each and it needs to be very fast when it comes to scrolling and changing the bindings. I think the slowdown comes from all the loops through the bindings but only one array needs to be changed on user input. The rest of the bindings never change after the page loads.
So I am trying to figure out a way to load nested unordered lists while only binding one variable. This is pseudo code for my directive.
.directive('myDirective', function($compile) {
return {
restrict: 'A'
link: function(scope, elm, attrs) {
outerList = '<ul><li>statically generated content that does not change'
outerList += '<ul><li ng-bind="I only need to bind one thing"><li></ul>'
outerList += < /ul>'
elm[0].innerHTML = outerList
}
}
});
As you can see I am trying to generate the html content, and then insert it with innerHTML. The problem is the ng-bind isn't working when I do it this way. I tried to $compile it again but that didn't change anything.
Does anyone have a better way? I don't care how hideous the solution is I just really need this part of the app to be super fast. The main thing is I don't want ng-repeat unless there is a way to make it do its thing on load and then never loop through anything again.
I would like to do this in the most Angular way possible but I realize I might have to do something that goes completely against Angular philosophy
Here is an example of how to modify your code in order to bind some variable in a directive from a scope outside of it. I've used $compile to ensure that your directive DOM manipulation has its own directives compiled. I've used replaceWith to replace the directive element with your compiled DOM:
HTML
<div ng-app="myApp">
<div ng-controller="ctrlMain">
<div my-directive="bindMe"></div>
</div>
</div>
JavaScript
var app = angular.module('myApp',[]);
app.controller('ctrlMain',function($scope){
$scope.bindMe = {id:1,myvar:"test"};
});
app.directive('myDirective', function($compile){
return{
restrict: 'A',
scope: {
varToBind: '=myDirective'
},
link: function(scope, elm, attrs){
outerList = '<ul><li>statically generated content that does not change'
outerList += '<ul><li ng-bind="varToBind.myvar"><li></ul>'
outerList += '</ul>';
outerList = $compile(outerList)(scope);
elm.replaceWith(outerList);
}
}
});
Here is a demo

Categories