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

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.

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

Issue with element.html() in Angular directive

I am new to Angular. I have a directive and in the linkFunction I am able to set the scope to a certain value by using attributes["attribute-value"]. I was expecting element.html() to provide the inner html of the directive.
.directive("sfGroupbar", function () {
var linkFunction = function (scope, element, attributes) {
scope.text = element.html();
scope.attr = attributes["text"];
};
return {
restrict: 'E',
templateUrl: "controls/groupbar.html",
link: linkFunction,
scope: {}
};
In my view, I am using the directive like so...
<sf-groupbar warning="50" error="80" text="Web Server">Some Text</sf-groupbar>
In groupbar.html, I am using the code like so...
<div ng-controller="groupbarController">
{{text}} <br />
{{attr}}
</div>
I was expecting to see "Some Text" and "Web Server" as output. I am getting only Web Server as output, and instead of "Some Text", I am getting the following as output...
<div ng-controller="groupbarController">
{{text}} <br />
{{attr}}
</div>
You have to set transclude property of the directive to true and have to include ng-transclude attribute inside the template or templateurl 's HTML element to make innerHTML of the directive to render.
Here is the working plunker based on your code,
http://embed.plnkr.co/sXoLPxeFA21fxzzeAcVs/preview
Hope this helps!!!!
You need to include text and the other attributes in your scope definition like so
scope { text : '=' } and also you might wanna add transclude option to true to try and get the text inside your directive.
I'm sure you'll be interested to look at this part of the documentation
Directive to manipulate the DOM

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 ngIf prevents finding element inside directive

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.

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