Extend ng-if or implement similar directive - javascript

I would like to include or exclude content based on a boolean value.
The boolean value comes from a service that provides feature toggles. I don't want to inject the directive in a lot of places or use $root in a condition for ng-if.
So I would like to create an AngularJS attribute directive that is similar to ng-if but has the service injected and includes/excludes the content based on the boolean value, instead of getting a condition expression from the attribute value in the template.
Is it possible to somehow extend the existing ng-if directive? If not, what would be the easiest/best way to implement similar behavior?

Here is a simple directive embedding a service which when it executes the linking function will decide whether or not to display the DOM element based on the value returned from the service. The directive relies on the currentUser service to return the value which would normally be part of the ng-if expression. It evaluates this in the linking function so the assumption is that this value is static and does not need to be re-evaluated. In fact, it cannot be re-evaluated as we totally remove the element from the DOM.
<!DOCTYPE html>
<html>
<head>
<script data-require="angular.js#1.4.8" data-semver="1.4.8" src="https://code.angularjs.org/1.4.8/angular.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body>
<button is-admin>ADMIN BUTTON</button>
<button>REGULAR BUTTON</button>
<script>
var app=angular.module("app",[]);
app.service("currentUser",function(){
return {
isAuthorized : function(){return false;}
}
});
app.directive("isAdmin",["currentUser", function(currentUser){
var linkFx = function(scope,elem,attrs){
debugger;
if(!currentUser.isAuthorized()){
elem[0].remove();
}
}
return {
link: linkFx,
restrict: "A"
}
}]);
angular.bootstrap(document,[app.name]);
</script>
</body>
</html>

I would suggest a different approach perhaps, and that is have an object in any parent controller, and use that object to do whatever.
So if your first controller is MainCtrl you can:
$scope.serviceDataWrapper = function() {
return MyService.getValue();
}
and use the regular ng-if anywhere, even if it involves different controllers:
<div ng-if="serviceDataWrapper()"></div>

Related

Unable to properly load external script without jquery

I am trying to lazy load a directive within another directive based on a condition. I have a main directive and within it I have a <script> tag. The included directive loads properly only when jquery is present, the problem is that not all of my pages load jquery. There is also a second warning...
synchronous XMLHttpRequest on the main thread is deprecated.
Which I should be able to suppress if I find a pure angularJs way of loading the script.
Here is my setup
<my-directive>
<script ng-if="expression" src="../path" type="application/javascript"></script>
</my-directive>
very simple, but it only works if jquery is there to load it. I tried to load it via $http.get() request and then eval() (which I know the dangers) but that did not work.
Here is a way which leverages the $http approach. You can create a directive which augments your <script> tag for this functionality, checking for a defined type attribute. You could of course modify this check however you'd like. Observe the following...
app.directive('script', function($http) {
return {
restrict: 'E',
scope: false,
link: function(scope, elem, attrs) {
if (attrs.type === 'text/javascript-lazy') {
$http.get(attrs.src).then(function(response) {
var code = response.data;
var f = new Function(code);
f();
});
}
}
};
});
<!-- retrieved -->
<script type="text/javascript-lazy" ng-if="true" src="script.js"></script>
<!-- not retrieved -->
<script type="text/javascript-lazy" ng-if="false" src="script.js"></script>
This will work inside an element wrapped directive as well.
Plunker - working demo

Using multiple AngularJS directives in one div tag

I am unable to use multiple directives in the same div tag. The following html does not display the product names that the module supplies
<!DOCTYPE html>
<html ng-app="gemStore">
<head>
<title></title>
<link rel="stylesheet" type="text/css" href="css/bootstrap.min.css" />
</head>
<body>
<div ng-controller="StoreController as store" ng-repeat="pro in store.products">
<h1>{{pro.name}}</h1>
</div>
<script src='scripts/angular.min.js'></script>
<script type="text/javascript" src="scripts/app.js"></script>
</body>
</html>
But if I add the ng-controller in a different div tag(see below) I am able to see the products. What is the reason behind this.
<!DOCTYPE html>
<html ng-app="gemStore">
<head>
<title></title>
<link rel="stylesheet" type="text/css" href="css/bootstrap.min.css" />
</head>
<body>
<div ng-controller="StoreController as store" >
<div ng-repeat="pro in store.products">
<h1>{{pro.name}}</h1>
</div>
</div>
<script src='scripts/angular.min.js'></script>
<script type="text/javascript" src="scripts/app.js"></script>
</body>
</html>
I did search about using multiple directives in a single div tag but was unsuccessful in finding any information.
Is this a restriction imposed by AngularJS?
Following is the contents of app.js
(function() {
var app = angular.module('gemStore', []);
app.controller('StoreController', function() {
this.products = gems;
});
var gems = [
{ name: 'Alwin', price: 200, description: "Checking my name" },
{ name: 'Alwin', price: 200, description: "Checking my name" },
{ name: 'Alwin', price: 200, description: "Checking my name" },
];
})();
You can use multiple directives on the same DOM node.
The issue you are running into is related to $compile priority and what ng-repeat does.
First of all you must understand that there's a clear algorithm that defines the way in which directives on the same DOM node are compiled. There's a priority parameter that defaults to 0. Directives are compiled based on this parameter, highest to lowest. ng-repeat has a priority of 1000 while ng-controller has a priority of 500. So ng-repeat is compiled first.
Second you need to know how ng-repeat works. If you read the sources you see that it uses the terminal parameter. That parameter tells AngularJS that it should not compile any directives with lower priorities.
Thus, in your code, ng-controller is never compiled ("executed").
Further, if ng-controller is not compiled then ng-repeat does not have a $scope to work on, the $digest loop won't have anything to do.
You'd be tempted to think store is undefined, and so is store.products, but you'd be wrong. If that were the case you'd see a lot of "Trying to access property 'products' on undefined object 'store'", but you don't because the code in ng-repeat is never executed.
What you are trying to do is not possible. ng-repeat would normaly create several instance of the specific tag. In your example it would create several divs, each one creating a seperate controller. I have no source for this, but I guess that $scope is only valid inbetween the controller tags. Therefore your ng-repeat argument does not point to you products list, but onto a higher scope. Due to the fact that there is no entry in this scope ng-repeat hast nothing to show.
Putting multiple directives onto a div is not a problem. This is a snippet of code from an app of mine, using both ng-click and ng-class:
<a href="" class="listItems" ng-click="selectPost($index)" ng-class="{disabledLink: !post.available}">
Your problem is you did it with the Controller, I'm not sure exactly how the ng-controller directive works, but you were essentially declaring a different instance of the controller, for each row in a list from the first controller. How this didn't end up in some crazy infinite loop, I don't know. But essentially, don't declare your controller in an ng-repeat. unless your looping through a list of controllers!
Edit:
Daniel has hit the route of the problem much better than I did. The ng-repeat creates a html element for each item in the list. However the controller hadn't been initialised, so the was no scope to get the list from, so there were no elements to draw, and no controllers got created. By putting the ng-controller 1 element further up, the list was created, and there was a scope, containing a list, which the ng-repeat could use to draw your items.
Directives are compiled and linked in priority order with the higher priority directives executed before lower priority directives.
ng-controller has a priority of 0 and ng-repeat has a priority of 1000, so ng-repeat is compiled and linked first.
If both were compiled, this would not cause the issue. However, ng-repeat is also defined with a terminal attribute.
According to angular documentation:
terminal:
If set to true then the current priority will be the last set of
directives which will execute (any directives at the current priority
will still execute as the order of execution on same priority is undefined).
This means that once ng-repeat is compiled, no other directives with a lower priority (such as ng-controller) will be executed.
[EDIT]
Apparently, there is a discrepancy between the source code and documentation as to the correct priority of ng-controller. The code uses a priority of 500, but the documentation mentions it is defined with a priority of 0.

AngularJS databinding query

This is a nooob question - as in I started learning Angular today. I was following the tutorial at Angular JS in 30 mins
The idea is to setup basic databinding in Angular. The code displays an input box, and shows(updates) whatever is typed in the box adjacent to it. Coming from a Java world and Spring MVC background, it makes perfect sense as long as the code is as follows:
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="ISO-8859-1">
<title>Test Angular</title>
<script type="text/javascript" src="angular.min.js"></script>
<script type="text/javascript" src="app.js"></script>
<script type="text/javascript" src="maincontroller.js"></script>
</head>
<body>
<div id="content" ng-app="SampleAngular" ng-controller="MainController">
<input type="text" ng-model="inputVal">{{ inputVal }}
</div>
</body>
</html>
app.js
var app = angular.module('SampleAngular', []);
maincontroller.js
app.controller("MainController", function($scope){
$scope.inputVal = "";
}
);
But, the same thing still works if I have a blank body for the controller i.e.:
maincontroller.js
app.controller("MainController", function($scope){
}
);
I understand that I will not be able to get the value of inputVal in the controller but
a) why does it still work in the view?
b) Clearly, I don't have a corresponding model 'inputVal' as defined in ng-model directive, and there are no errors/warnings - which IMHO - is equivalent to failing silently. Is this how Angular is designed? Is this a potential issue in large apps and will make debugging a nightmare?
Thanks in advance.
The following is from the api documentation for ngModel:
Note: ngModel will try to bind to the property given by evaluating the expression on the current scope. If the property doesn't already exist on this scope, it will be created implicitly and added to the scope.
Whenever a ng-model is used in the view, corresponding model value is created on the scope of the controller. Angular won't be throwing any error in this case, as this is its default behaviour.
$scope.inputVal = "";
is used more like initialisation of the variable being used.

Is there a callback for ng-bind-html-unsafe in AngularJS

I would like to remove some of the elements that are brought into the DOM by this...
<div ng-bind-html-unsafe="whatever"></div>
I wrote a function that will remove the elements, but I need a way to trigger the function after ng-bind-html-unsafe is complete. Is there a callback for ng-bind-html-unsafe or a better approach?
ng-bind-html-unsafe has been removed from the current version (1.2+) of angular. I would recommend using the $sanitize service in the new version of angular. You'll need to include the sanitize library and add it to your module.
That way you can do whatever you want once the sanitize operation is complete and not worry about a callback.
A quick and dirty implementation:
<!doctype html>
<html lang="en">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.11/angular.js"></script>
<script src="libs/angular/angular-sanitize.js"></script>
</head>
<body ng-app="myApp">
<div ng-controller="myController">
<div>{{sanitized}}</div>
</div>
<script>
angular.module('myApp', ['ngSanitize'])
.controller('myController', ['$scope', '$sanitize', function($scope, $sanitize) {
$scope.sanitized = $sanitize('someTextFromSomeSource');
// Do whatever you want here with the cleaned up text
}])
</script>
</body>
</html>
I would move the html in your demo to a directive:
<div ng-bind-html-unsafe="whatever"></div>
in the directive I would manipulate the html based on my needs, I am here basing this on an assumption since I am not sure how often or how whatever variable is being updated so the generic solution will be a $watch listener over this variable like this:
$scope.$watch('whatever',function(newValue,oldValue, scope){
//do something
});
All this code in my new directive, probably you will want to use the postLink function
Here is the Documentation from Angular about this.
Post-linking function
Executed after the child elements are linked. It is safe to do DOM transformation in the post-linking function.

How to register existing elements with Angular?

Fairly new to Angular and working inside of an existing code base.
Basically, there's an element that exists within the root document (index.html) that already exists in the html before the Angular library loads. Because of this, the ng-click directive isn't registered.
Is there an easy way that I can pass Angular a reference to the element in question and have it register that as one of its own?
Sample code (obviously missing parts, just to illustrate Angular loads after):
<html>
<body ng-app="allMyCookiesApp">
<a ng-click="giveGeuisACookie()">GIMME</a>
<script src="angular.js"></script>
</body>
</html>
I'd like to get a cookie when I click GIMME.
ng-app will bootstrap everything inside it once angular loads. This includes compiling and linking the ng-click in your example. So I think the real problem may be elsewhere.
The biggest omission from this example is any controller. I expect you are missing a controller that can place the giveGeuisACookie method on the correct scope to be used by ng-click. For example
angular.module('allMyCookiesApp', [])
.controller('geuisCtrl', function($scope) {
$scope.giveGeuisACookie = function() {
// Cookie time
};
});
would define your module for ng-app and register a controller for it. This controller will add the giveGeuisACookie function to its scope.
<html>
<body ng-app="allMyCookiesApp" ng-controller="geuisCtrl">
<a ng-click="giveGeuisACookie()">GIMME</a>
<script src="angular.js"></script>
</body>
</html>
tells angular to use the controller so that ng-click will have access to the correct method.
If this is not the problem it may be worth adding a jsfiddle with a working (or not) example of what you are doing.

Categories