I tried to make a directive for destroying DOM elements if user doesn't have the permission to see it. I do this as follows:
angular
.module('digital_equation.auth')
.controller('AuthLoginController', AuthLoginController)
.directive('ngPermission', ngPermissionDirective);
function ngPermissionDirective() {
return {
multiElement: true,
scope: {
ngPermission: '#'
},
restrict: 'A',
link: function (scope, element, attributes) {
scope.$watch('ngPermission', function (value) {
console.log(value);
//Access rootScope to get the current user permissions
var permissions = scope.$root.globals.currentUser.role.settings.permissions;
//Loop through permissions object to check if any permission is the same as the value sent to directive
var keepGoing = true;
angular.forEach(permissions, function (permission, key) {
if (keepGoing == true)
{
if (key == value) {
element.show();
keepGoing = false;
}
else {
element.hide();
}
}
});
})
}
};
}
HTML:
<div class="col-xs-12 col-md-9" ng-permission="manage_appointments"></div>
In this situations for example if the current user taken from rootScope doesn't have the permission "manage_appointments" among it's permission this div should be destroyed. As you can see I only know how to hide it but this is not enough I need it to be destroyed so on page inspect this div doesn't show up.
My second problem is that console.log(value); returns undefined not matter what I tried.
And I also need an advice upon accessing rootScope. If I pass the rootScope as parameter here it works but I cannot pass the scope as well.. so how can I do this.
link: function(rootScope, element, attributes )
Please keep in mind that even though I declare my directive in the authController I need it to be available in my entire project.
Sorry for the description but it is my first custom directive in AngularJs and I tried a lot of options.
Thank you all for your time and help!
Edit:
scope.$watch('ngPermission', function (value)
This solved my problem with value being undefined but when I try to use the directive on two different elements (one to be hidden and one to be shown) it will do whatever the last use of my directive is applied (show both in this case). Any ideas why this happens?
Solution:
function ngPermissionDirective() {
return {
multiElement: true,
priority: 900,
scope: {
ngPermission: '#'
},
restrict: 'A',
link: function (scope, element, attributes) {
scope.$watch('ngPermission', function (value) {
console.log(value);
//Access rootScope to get the current user permissions
var permissions = scope.$root.globals.currentUser.role.settings.permissions;
//Loop through permissions object to check if any permission is the same as the value sent to directive
var keepGoing = true;
angular.forEach(permissions, function (permission, key) {
if (keepGoing == true)
{
if (key == value) {
element.show();
keepGoing = false;
}
else {
element.remove();
}
}
});
})
}
};
}
Try changing scope.$watch(attributes.ngPermission, ... to scope.$watch('ngPermission', ....
An alternative approach might be $observe - attributes.$observe('ngPermission', ...
Related
I am using $watch() to control the required attribute using the following custom directive:
app.directive('checkIfRequired', ['$compile', '$timeout', '$parse', function ($compile, $timeout, $parse) {
return {
/*require: '?ngModel',*/
link: function (scope, el, attrs, ngModel) {
var children = $(":input", el);
var saveIsValidationRequired;
//Run the following as early as possible but just wait (using promise) until
// the list of required fields is retrieved from Database
scope.requiredFieldsPromise.then(function(success) {
//If needed, stop validation while adding required attribute
saveIsValidationRequired = scope.isValidationRequired; //Save current flag value
scope.stopExecValidations();
//remove the attribute `check-if-required` to avoid recursive calls
el.removeAttr('check-if-required');
angular.forEach(children, function(value, key) {
if (scope.isFieldRequired(value.id)) {
angular.element(value).attr('required', true);
$compile(value)(scope);
}
//Check if the element is not in "Required" list, and it has a function to control requried, then
//... execute the function using $watch and $parse and add the required attribute accordingly
if (!angular.element(value).prop('required') && value.attributes.hasOwnProperty("check-if-required-expr")) {
var isRequiredExpr = value.attributes["check-if-required-expr"].value;
scope.$watch(function (){
var exprValue = $parse(isRequiredExpr)(scope);
return exprValue;
}
, function (oldValue, newValue){
var isRequired = $parse(isRequiredExpr)(scope);
if (isRequired) {
angular.element(value).prop('required', true);
$compile(value)(scope);
} else {
angular.element(value).prop('required', false);
$compile(value)(scope);
}
var elModel = angular.element(value).controller("ngModel");
elModel.$setViewValue(elModel.$viewValue);
})
}
});
//If saved flag value is true, enable validation
if (saveIsValidationRequired) {
scope.startExecValidations();
}
})
//})
}
};
}]);
Basically, the above directive checks if any if the child elements is required, then it will add the required attribute using $compile(value)(scope), if not, then it will check if check-if-required-expr expression is specified, if yes, it will use $watch() to evaluate the expression, and set the required attribute accordingly.
See image below for more details.
All is working fine. The only problem is that when the "Name" field is cleared, the required attribute for "Member #" field should be removed, and the highlight should be cleared, however, the class ng-invalid-required is still added and this is causing the highlight to be there.
You can see the the $error object for the NgModelController named super_aic_member_number is calculated correctly as per the intended logic.
So basically the question here is how to ensure the style is synchronized correctly with the change of the required attribute?
Finally, I managed to find the solution. Instead of using $watch() to monitor the expression check-if-required-expr, I am simply now adding this expression to the ng-required attribute, then, I will compile.
So far, it is working fine.
Here is the updated code:
app.directive('checkIfRequired', ['$compile', '$timeout', '$parse', function ($compile, $timeout, $parse) {
return {
/*require: '?ngModel',*/
link: function (scope, el, attrs, ngModel) {
/*if (!ngModel) {
return;
}*/
//debugger;
var children = $(":input", el);
var saveIsValidationRequired;
//Run the following as early as possible but just wait (using promise) until
// the list of required fields is retrieved from Database
scope.requiredFieldsPromise.then(function(success) {
//If needed, stop validation while adding required attribute
saveIsValidationRequired = scope.isValidationRequired; //Save current flag value
scope.stopExecValidations();
//remove the attribute `check-if-required` to avoid recursive calls
el.removeAttr('check-if-required');
angular.forEach(children, function(value, key) {
//debugger; && (value.id != "pi_subject_namex" && value.id != "pi_subj_provincex")
if (scope.isFieldRequired(value.id)) {
angular.element(value).attr('required', true);
//el.removeAttr('check-if-required');
$compile(value)(scope);
}
//Check if the element is not in "Required" list, and it has an expression to control requried, then
//... add the attribute 'ng-required' with the expression specified to the element and compile.
if (!angular.element(value).prop('required') && value.attributes.hasOwnProperty("check-if-required-expr")) {
var isRequiredExpr = value.attributes["check-if-required-expr"].value;
angular.element(value).attr('ng-required', isRequiredExpr);
$compile(value)(scope);
}
});
//If saved flag value is ture, enable validation
if (saveIsValidationRequired) {
scope.startExecValidations();
}
})
//})
}
};
}]);
I have an angular driven form using $dirty to spot for changes to enable/disable submit button.
Part of the form uses a Directive for uploading a logo but the form is noticing this as a changed element so upon setting a logo that validates in size I need to manually trigger that the form has had a change so should be a case of formName.$setDirty(); however console is saying that $setDirty() is not defined and I think this is because I am within a directive.
Within my directives controller upon file selection I call the function below and it is here when the file is valid that I would want to call the setdirty method.
function isFileValid(file) {
vm.fileValid = true;
vm.errorMessage = "";
if (file.size > 16777216) {
vm.errorMessage = "The File is too big!";
vm.fileValid = false;
} else if (file.size == 0) {
vm.errorMessage = "The File is empty!";
vm.fileValid = false;
}
if (vm.fileValid) {
// make form dirty
$setDirty();
}
return vm.fileValid;
}
Here is the directive JS
(function () {
'use strict';
.module("tpBusinessProfile")
.directive("tpLogoUploader", tpLogoUploader);
function tpLogoUploader() {
return {
templateUrl: "tpLogoUploader.directive.html",
bindToController: true,
scope: {
changedMethod: "&"
},
controller: "tpLogoUploaderCtrl",
controllerAs: 'logoCtrl',
restrict: "E"
};
}
})();
Any help is appreciated.
You need to use directive require option and require controller of form directive:
{
require: '^form'
and then in link function bind method that you need to your scope (dirty solution):
link(scope, elem, attrs, formController){
scope.makeFormDirty = formController.$setDirty
}
and now you can use it in your controller via makeFormDirty
I have a controller with following code snippet,
...
$scope.selected_contents = [];
$scope.$watch('selected_contents', function (sel_contents) {
console.log(sel_contents, 'selected contents');
}, true);
...
a directive,
commonDirectives.directive('chkbox', function() {
return {
restrict: 'A',
require: '?ngModel',
scope : {
item : '=item',
selection_pool: '=selectionPool'
},
link: function(scope, elem, attrs, ngModel) {
console.log('selected contents are', scope.selection_pool);
// watch selection_pool
scope.$watch('selection_pool', function (pool) {
console.log(pool, scope.selection_pool, 'pool updated');
if (_.contains(pool, scope.item)) {
elem.prop('checked', true);
}
else {
elem.prop('checked', false);
}
});
// toggle the selection of this component
var toggle_selection = function () {
if(_.indexOf(scope.selection_pool, scope.item) != -1) {
scope.selection_pool = _.without(scope.selection_pool , scope.item);
}
else {
scope.selection_pool.push(scope.item);
}
};
elem.on('click', toggle_selection);
}
};
});
and a template which uses the directive,
<tr ng-repeat="content in contents">
<td><input type="checkbox" selection_pool="selected_contents" item="content" chkbox></td>
</tr>
The problem is, changes in selection_pool in the directive is not reflected to selected_contents in the controller. What am i missing?
Update 1:
Following the suggestion from #mohamedrias I wrapped the changes in scope with scope.$apply. Doing so updates selected_contents in controller only while adding the content but not while removing it.
...
// toggle the selection of this component
var toggle_selection = function () {
if(_.indexOf(scope.selection_pool, scope.item) != -1) {
scope.$apply(function () {
scope.selection_pool = _.without(scope.selection_pool , scope.item);
});
}
else {
scope.$apply(function () {
scope.selection_pool.push(scope.item);
});
}
};
...
Angular uses name-with-dashes for attribute names and camelCase for
the corresponding directive name
From here.
The variable should be changed from this selection_pool:
<input type="checkbox" selection_pool="selected_contents" item="content" chkbox>
to selection-pool:
<input type="checkbox" selection-pool="selected_contents" item="content" chkbox>
And this selectionPool into the directive:
scope : {
item : '=item',
selectionPool: '=selectionPool'
}
EDIT: Because the selectionPool is an array, you should use $watchCollection:
scope.$watchCollection('selectionPool', function (pool)
And when you add/remove values from the array in toggle_selection function, should be wrapped within the $timeout function:
$timeout(function () {
if (_.indexOf(scope.selectionPool, scope.item) != -1) {
scope.selectionPool = _.without(scope.selectionPool, scope.item);
} else {
scope.selectionPool.push(scope.item);
}
});
This is to assure that a digest cycle is going to be applied afterwards.
Here's the code working on a jsfiddle: http://jsfiddle.net/0rvcguz0/3/
After researching for entire day, I ended up here. If someone is having any trouble with Angularjs scope, I highly encourage to read it.
The proper solution in my case was to wrap selected_contents by an object. e.g.
$scope.selected = {};
$scope.selected.contents = [];
Then in the template replace selcted_contents with selected.contents.
But still what I don't understand is, [] or an Array is also an object. My earlier code should have worked according to the information I found in the wiki. If anyone could explain me why I would really appreciate it :).
Here is the directive:
.directive("unloggedWarning", function () {
return {
restrict: "EA",
link: function (scope) {
scope.$watch('currentUser', function() {
if(scope.currentUser === null) {
scope.notLogged = true;
} else {
scope.notLogged = false;
} });
}
};
})
currentUser is rootscope persistant user current status with Parse backend. So whenever user logs out, the watch will set notLogged to true. I guess I can then in the html file view use conditonal ng-if to display warning when user unlogs.
How could I improve this directive, so from INSIDE the directive, I can conditionally inject a template with some html in it ? I can't seem to pass the log status from the if statement, to a standard directive "template: " parameter inside the directive.
Here is a working example: http://plnkr.co/edit/S9fVZKXLx0Fc6QpkI8iH?p=preview
All I did was add the template:
template: '<div><div ng-show="notLogged">You are not logged in</div></div>',
Using a directive focus-me="inTextModeInput" in a text input
app.directive('focusMe', function($timeout) {
/*focuses on input
<input type="text" focus-me="focusInput">
*/
return {
scope: { trigger: '=focusMe' },
link: function(scope, element) {
scope.$watch('trigger', function(value) {
if(value === true) {
$timeout(function() {
element[0].focus();
scope.trigger = false;
});
}
});
}
};
});
Actually having 2 inputs, both uses focus-me
When i programatically set the value to focus on an input the ng-blur of other is not called.
NOTE : i am also using this in an ng-repeat.
Isolated scope
The blur is called, but you're not seeing that because you've created a directive with an isolated scope. The ng-blur is executed on the $parent scope. You should only use an isolated scope when the directive is implementing re-useable templates.
Two way binding on trigger
The line 'scope.trigger = false' is also setting a different boolean value because it's on a different scope. If you want to assign a value to a variable from a directive you should always wrap the value inside another object: var focus = { me: true } and set it like trigger=focus.me.
A better solution
But I wouldn't set the trigger to false at all. AngularJS is a MVC/MVVM based framework which has a model state for the user interface. This state should be idempotent; meaning that if you store the current state, reload the page and restore the state the user interface should be in the exact same situation as before.
So what you probably need is a directive that
Has no isolated scope (which allows all other directives to work: ng-blur, ng-focus, ...)
Keeps track of a boolean, which indicates the focus state
Sets this boolean to false when the element has lost focus
It's probably easier to see this thing in action: working plunker.
Maybe this (other) plunker will give you some more insight on scopes and directives.
Code
myApp.directive('myFocus', function($parse, $timeout) {
return {
restrict: 'A',
link: function myFocusLink($scope, $element, $attrs, ctrls) {
var e = $element[0];
// Grab a parser from the provided expression so we can
// read and assign a value to it.
var getModel = $parse($attrs.myFocus);
var setModel = getModel.assign;
// Watch the parser -- and focus if true or blur otherwise.
$scope.$watch(getModel, function(value) {
if(value) {
e.focus();
} else {
e.blur();
}
});
function onBlur() {
$timeout(function() {
setModel($scope, false);
});
}
function onFocus() {
$timeout(function() {
setModel($scope, true);
});
}
$element.on('focus', onFocus);
$element.on('blur', onBlur);
// Cleanup event registration if the scope is destroyed
$scope.$on('$destroy', function() {
$element.off('focus', onFocus);
$element.off('blur', onBlur);
});
}
};
});