I'm trying to create a directive for our AngularJS application. It would be used like this:
<div requires-permissions="['permission_1', ...]">
This should only show if the user has appropriate permissions.
</div>
It would function much like ng-if in that it would remove or replace the element. Without this directive I would need to write this for each such element:
<div ng-if="ctrl.User.has_permissions(['permission_1', ...])">etc.</div>
and each controller would need a reference to the User service. I have looked through the code for ng-if and I can't make heads or tails of the code.
I'm pretty sure I can remove the element using jqlite's remove function. How the heck can I replace it later?
I've implemented something similar for your needs, it calls experimentIF
export function experimentIf(ngIfDirective, experimentsManager) {
const ngIf = ngIfDirective[0];
const NAME = 'experimentIf';
return {
restrict: ngIf.restrict,
priority: ngIf.priority + 1,
terminal: ngIf.terminal,
transclude: ngIf.transclude,
link: function ($scope, $element, $attr) {
const experimentName = $attr[NAME];
$attr.ngIf = function () {
return experimentsManager.isExperimentEnabled(experimentName);
};
ngIf.link.apply(ngIf, arguments);
}
};
}
Basically, it reuses ng-if's configuration when needed and changes only the assertion part.
In my case, it was to use experimentsManager service.
The magic occurs when you are injecting the ngIfDirective.
Related
I have - ng-view - template create item functionality and same template containing one directive that load the saved items.
Now, when i do save create item and immediately, its not refreshing list of items (from directive).
Can anyone tell me how I would resolve this, so, after saving item, immediately directive is refreshed.
Note: directive link function is making call to $http and retrieving data and populate in directive template. And directive element is added in other html template.
html template: (which has separate controller and scope).
<div>.....code</div>
<div class="col-md-12">
<parts-list></parts-list>
</div>
directive code:
(function () {
angular.module("application")
.directive("partsList", function (partService) {
return {
templateUrl: 'partsListView.html',
restrict: 'E',
scope: {},
link: function ($scope) {
$scope.partList = [{}];
RetrieveParts = function () {
$scope.partList=partService.RetrieveParts();
};
}
};
});
})();
For starters, your ReceiveParts variable doesn't have proper closure. Also, are you calling this function? I'm not sure where this function gets executed.
link: function ($scope) {
$scope.partList = [{}];
RetrieveParts = function () {
$scope.partList=partService.RetrieveParts();
};
}
An easy trick I've learned that makes it trivial to execute some of the the directives linking function logic in sync with angularjs's digest cycle by simply wrapping the logic I need in sync with the $timeout service ($timeout is simply a setTimeout call followed by a $scope.$apply()). Doing this trick would make your code look like:
link: function ($scope) {
$scope.partList = [{}];
$scope.fetchedPartList = false;
$timeout(function() {
$scope.partList = partService.RetrieveParts();
$scope.fetchedPartList = true;
});
}
Additionally, you'll notice the boolean value I set after the partList has been set. In your HTML you can ng-if (or ng-show/hide) on this variable to only show the list once it's been properly resolved.
I hope this helps you.
Use isolated scope in directive:
return {
templateUrl: 'partsListView.html',
restrict: 'E',
scope: {partList: '='},
and in template:
<parts-list partList="list"></parts-list>
Where list is where ui will update with updated data.
See how isolated scope using basic Example
I am relatively new to AngularJS. While venturing into directive creation, I can across this problem: How to dynamically add / remove attributes on the children of the directive's element when these children are dynamically added with 'ng-repeat'?
First, I thought of this solution:
template
...
a.list-group-item(ng-repeat='playlist in playlists', ng-click='addToPlaylist(playlist, track)', ng-href='playlist/{{ playlist._id }})
...
*directive
link: function(scope, elm, attrs) {
var listItems = angular.element(element[0].getElementsByClassName('list-group-item')
angular.forEach(listItems, function(item, index) {
'add' in attrs ? item.removeAttr('href') : item.removeAttr('ng-click');
listItems[index] = item;
}
...
Result
It turns out, my code never enters this angular.forEach loop because listItems is empty. I suppose it's because the ng-repeat is waiting for the scope.playlists to populate with the data from a async call to a server via $resource.
temporary fix
in the directive definition, I added a boolean variable that checks for the presence of 'add' in the element's attributes: var adding = 'add' in attrs ? true : false;
And then in the template,
a.list-group-item(ng-if='adding', ng-repeat='playlist in playlists', ng-click='addToPlaylist(playlist, track)')
a.list-group-item(ng-if='!adding', ng-repeat='playlist in playlists', ng-href='playlist/{{playlist._id }}')
While it works fine, it is obviously not DRY at all. HELP!
Instead of removing attributes, change your click handler.
Add $event to the list of arguments and conditionally use preventDefault().
<a ng-click='addToPlaylist($event,playlist)' ng-href='playlist'>CLICK ME</a>
In your controller:
$scope.addToPlaylist = function(event,playlist) {
if (!$scope.adding) return;
//otherwise
event.preventDefault();
//do add operation
};
When not adding, the function returns and the href is fetched. Otherwise the default is prevented and the click handler does the add operation.
From the Docs:
$event
Directives like ngClick and ngFocus expose a $event object within the scope of that expression. The object is an instance of a jQuery Event Object when jQuery is present or a similar jqLite object.
-- AngularJS Developer Guide -- $event
The way that you are trying to do things may not be the most Angularish (Angularist? Angularyist?) way. When using angular.element() to select child elements as you are trying to do here, you can make sure the child elements are ready as follows:
link: function(scope, elm, attrs) {
elm.ready(function() {
var listItems = angular.element(element[0].getElementsByClassName('list-group-item')
angular.forEach(listItems, function(item, index) {
'add' in attrs ? item.removeAttr('href') : item.removeAttr('ng-click');
listItems[index] = item;
}
});
}
However, this is unlikely to work in your situation, as #charlietfl points out below. If you want to avoid the solution you already have (which I think is better than your first attempt), you will have to reimplement your code altogether.
I would suggest defining an additional directive that communicates with its parent directive using the require property of the directive definition object. The new directive would have access to an add property of the parent (this.add in the parent directive's controller) and could be programmed to behave accordingly. The implementation of that solution is beyond the scope of this answer.
Update:
I decided to give the implementation something of a shot. The example is highly simplified, but it does what you are trying to do: alter the template of a directive based on the attributed passed to it. See the example here.
The example uses a new feature in Angular 1: components. You can read more about injectable templates and components here. Essentially, components allow you to define templates using a function with access to your element and its attributes, like so:
app.component('playlistComponent', {
// We can define out template as a function that returns a string:
template: function($element, $attrs) {
var action = 'add' in $attrs
? 'ng-click="$ctrl.addToPlaylist(playlist, track)"'
: 'ng-href="playlist/{{playlist._id}}"';
return '<a class="list-group-item" ng-repeat="playlist in playlists" ' +
action + '></a>';
},
// Components always use controllers rather than scopes
controller: ['playlistService', function(playlists) {
this.playlists = playlists;
this.addToPlaylist = function(playlist, track) {
// Some logic
};
}]
});
I have an Angular app where I'm using ui-grid. I want to have a custom action on a cell of the grid that calls a method from my app. So basically, this means calling a method that's somewhere up in the parent hierarchy, from a directive.
This would be achieved by calling something like: $scope.$parent.$parent.$parent.$parent.foo(). But that doesn't seem too nice.
One option would be to create a recursive function that goes up the ancestry of the $scope. That's nicer, but still seems a bit weird.
Also... Is it good practice to try to achieve something like this?
You're correct that $parent.$parent.$parent is definitely not a good practice.
If the method you're calling is another directive, you can require that directive in your child directive and then, the parentDirective's controller function will be injected as the fourth parameter to your link function:
In your DDO:
return {
require : '^parentDirective',
restrict : 'E',
link : function (scope, elem, attrs, parentDirectiveController) {}
}
If what you're trying to call is on a factory/service, you can inject that factory/service into your directive, although this sometimes is a code smell, depending on what you're trying to inject.
Finally, another way to do it is to use event propagation. From your directive, you can use $scope.$emit to send information up to parent controllers:
From the directive:
$scope.$emit('directiveDidStuff', {
data : 'blah'
});
In the parent controller:
$scope.$on('directiveDidStuff', function (evt, params) {
this.data = params.data; // equals blah
});
You can achieve the same by using "&" through one of the scope variable in directive.Like this, you can bind your event to the controller method and from the method, you could do your desired things or if the original business logic which you wants to achieve on onClick of the grid is used across many modules than you can bisect it in service and make it reusable and call the service from the event method. Let me know if you do have any doubts with the approach.
Key Code of example:
Html
<my-component attribute-foo="{{foo}}" binding-foo="foo" isolated-expression- foo="updateFoo(newFoo)" >
Directive
var myModule = angular.module('myModule', [])
.directive('myComponent', function () {
return {
restrict:'E',
scope:{
/* NOTE: Normally I would set my attributes and bindings
to be the same name but I wanted to delineate between
parent and isolated scope. */
isolatedAttributeFoo:'#attributeFoo',
isolatedBindingFoo:'=bindingFoo',
isolatedExpressionFoo:'&'
}
};
})
I am using the editable-text directive from the xeditable module for AngularJS. Is there a way to disable the directive for the entire page?
I thought about using replacing editable-text with {{variable}}, where variable="editable-text" to enable and variable="somethingElse" to disable. However, this produces meaningless attributes in the html.
It is possible to remove directive with another directive. For this, new directive should have higher priority than the one being removed, then in compilation state you search for elements with required directive and remove tag/class/element wholetogether. Here's a very simple realisation of this:
.directive("disableDirective", function () {
function compile (el, attr) {
var hasDirective = el[0].querySelectorAll("["+attr.disableDirective+"]");
[].forEach.call(hasDirective, function (el) {
el.removeAttribute(attr.disableDirective);
});
}
return {
priority: 100000,
compile: compile
};
})
In the following HTML DIV will be visible, thanks to our directive:
<body disable-directive="ng-hide">
<div ng-hide="true">Hidden</div>
</body>
You'll have to set disable-directive="editable-text" for the page.
JSBin.
If you just want to turn off this directive across all app forever, I know two ways:
Create decorator for this directive and just set the compile function to empty function e.g. to angular.noop. Check this stuff for more info: https://docs.angularjs.org/api/auto/service/$provide#decorator .
Or you can also create directive with the same name and restrictions (A|E|C|M), but with higher priority and terminal property setted to true. But you should always keep in mind that this directive will turn off all directives with lower priority on the same element. (so this solution looks like antipattern for me)
I believe you can remove the restrict field using a decorator. this would disable the directive. Here is an example using ng-show
var app = angular.module("app", []);
//disable ng-show
app.config(function($provide) {
$provide.decorator('ngShowDirective', function($delegate) {
var directive = $delegate[0];
directive.restrict = "";
return $delegate;
});
});
If you have a directive that you're using multiple times on a page how can 1 directive communicate with another?
I'm trying to chain directives together in a parent child relationship. When directive A is clicked i want to filter Directive B to only have the children of the selected item in Directive A. In this case there may be infinite number of directives and relationships on the page.
Normally i would have Directive A call a filter method on each of it's children, and each child calls it's child to continue filtering down the hierarchy.
But i can't figure out if calling methods from 1 directive to another is possibe.
Thanks
It sounds like you are looking for a directive controller. You can use the require: parameter of a directive to pull in another directive's controller. It looks like this:
app.directive('foo', function() {
return {
restrict: 'A',
controller: function() {
this.qux = function() {
console.log("I'm from foo!");
};
},
link: function(scope, element, attrs) {
}
};
});
app.directive('bar', function() {
return {
restrict: 'A',
require: '^foo',
link: function(scope, element, attrs, foo) {
foo.qux();
}
};
});
From the angular docs, here are the symbols you can use with require and what they do.
(no prefix) - Locate the required controller on the current element.
? - Attempt to locate the required controller, or return null if not found.
^ - Locate the required controller by searching the element's parents.
?^ - Attempt to locate the required controller by searching the element's parents, or return null if not found.
Here's a jsbin of my example. http://jsbin.com/aLikEF/1/edit
Another option that may work for what you need is to have a service that each directive sets up a watch on and can manipulate. For example, directive1 may watch a property in the service and respond to changes and also setup a button that can change that property. Then, directive2 can also watch and change the service, and they will respond to one another however you set that up. If you need a jsbin of that also, just let me know.
I hope this helps!
You could try putting all of the data into a service that the directives can each reference.
Something like:
app.factory('selectedStuffService', function(){
var allItems = [];
var selectedItems = [];
function addSelectedItem(item){
selectedItems.push(item);
}
return {
allItems: allItems,
selectedItems: selectedItems,
addSelectedItem: addSelectedItem
}
}
Interactions in directive A change the values in the selectedItems array and directive B can bind to it. You can easily add other methods to the service to filter/manipulate the items as needed and any directive that uses the service should be able to update based on changes made by other directives.