How to avoid using 'scope.$parent...' in angular 1.2 - javascript

We're developing a set of (ideally) flexible, component-based re-usable templates in angularjs 1.2 to develop a series of e-learning modules.
Part of the spec requires the tracking of 'completable' components. At the moment the main controller looks like this:
app.controller('mainCtrl', ['$scope', function($scope) {
$scope.completables = [];
$scope.completed = [];
$scope.addCompletable = function (object) {
$scope.completables.push(object);
// also set correlating completed property to 'false' for each completable added
$scope.completed.push(false);
}
$scope.componentCompleted = function(id) {
// Set complete to 'true' for matching Sscope.completed array index
// We COULD use .indexOf on the completables array, but that doesn't work with IE8
var tempArray = $scope.completables;
var matchingIndex = -1;
for (var i=0; i<tempArray.length; i++) {
if (tempArray[i]==id) {
matchingIndex = i;
}
}
if (i>-1) {
$scope.completed[matchingIndex] = true;
}
}
}]);
We have a eng-completable attribute that triggers the following directive:
app.directive('engCompletable', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
// add the id of this element to the completables array in the main controller
scope.$parent.addCompletable(attrs.id);
}
}
});
So every time angular encounters an 'eng-completable' attribute on an element, it calls addCompletable on the parent scope which adds the element id to the 'completables' array and 'false' to the corresponding index of the 'completed' array.
In the eng-popup attribute directive, we have a function to check if it has been made visible:
app.directive('engPopup', function() {
return {
restrict: 'A',
replace: true,
templateUrl: 'components/popup.html',
link: function(scope, element, attrs) {
scope.$watch(function() { return element.is(':visible') }, function() {
scope.$parent.componentCompleted(attrs.id);
});
}
};
});
Which also uses the parent scope to trigger the 'componentCompleted' function. I've been told that referring to the parent scope is bad practise, and it is also messing up our unit tests, apparently.
I'd like to know what is the alternative. How can I let my app know that a specific component has been completed? And where should this state be tracked?
I'd really like to know HOW to do this - not just be told that I'm doing it the wrong way. Please let me know what the alternative is.
But, as always, any help will be much appreciated.

One alternative would be to create a Service to be responsible to track all the components and keep their states (complete/not completed).
It will remove the need for $scope.parent and the service can be injected into any controller or directive you need.
:)

If that completables list is application-wide yo could consider adding it to your $rootScope along with the addCompletable method —and any other relate methods— instead of adding it to your mainController's $scope.
This way you could substitude your scope.$parent.componentCompleted(attrs.id); with $rootScope.componentCompleted(attrs.id); and avoid to make calls to scope.$parent.

Related

How to detect when scope property is set in Angular directive link function?

I'm building a directive that needs to initialize some data based on values passed into it through the scope. The problem is that when I try and initialize the data in the link function, the passed in value isn't available yet. Is there anyway to only run the initialization when the passed in value is available? I thought about using a watch as in the following code but it seems messy (and doesn't seem to work anyway).
.directive('etMemberActivitySummary', [function () {
return {
restrict: 'E',
templateUrl: '<div>My template</div>',
transclude: false,
scope: {
memberModel: '='
},
link: function(scope, element, attrs, controller) {
var watcher = scope.$watch(
function() {
return scope.memberModel
},
function(value) {
console.log(value);
if (value != null) {
console.log('Watch');
console.log(value);
watcher();
// Perform initialization based on scope.memberModel here
}
});
}
}
}])
Is there a correct way to do this? If it helps, the passed in value is in itself retrieved from a web service.
Update 1
Turns out that if I put an ng-if="ctrl.memberModel" on the directive usage like the following and get rid of all the watch stuff, it works. Is this the best way to do this?
<et-member-activity-summary member-model="ctrl.memberModel" ng-if="ctrl.memberModel"></et-member-activity-summary>

How The Object Scope Get That update available and get it ready to be exposed on the view AngularJs

In AngularJS the data-binding work to expose immediate data to our View !!
this stuff's due of the object scope which is the glue between the Logic Code AND The View.
Also all we know that AngularJs support the tow-way-binding !!!
My Question Is :
How the $scope can know that there object binding was changed or not??
if there while condition inside scope for auto-change detect or what?
Check angular.js file, we will get the code for ngBindDirective:
var ngBindDirective = ['$compile', function($compile) {
return {
restrict: 'AC',
compile: function ngBindCompile(templateElement) {
$compile.$$addBindingClass(templateElement);
return function ngBindLink(scope, element, attr) {
$compile.$$addBindingInfo(element, attr.ngBind);
element = element[0];
scope.$watch(attr.ngBind, function ngBindWatchAction(value) {
element.textContent = isUndefined(value) ? '' : value;
});
};
}
};
}];
Note the last two line, it used watcher for attribute ngBind, for any change it apply to the element.

Getting directive name in AngularJS

I've got an Angular directive. Inside the link function, I do this:
link: function(scope, element, attrs) {
...
element.data('startY', value);
...
}
What I'd like to do is perfix 'startY' with the name of the directive, without hard-coding the name. I'd like to dynamically get the name.
Is there any way to do this? Does Angular provide a way to reflect it? Something like:
link: function(scope, element, attrs) {
...
element.data(this.$name + '-startY', value);
...
}
If not, what are the recommended best practices for choosing data() keys to avoid collisions?
As indicated in the AngularJS source code, a directive's name is assigned in the context of the object literal where your directive options reside. The link function however cannot access the object literal's context, this, because it will be transferred to a compile function where it will be returned and invoked after the compilation process has taken place.
To get the name within your link function you can follow any of these suggestions:
[ 1 ] Create a variable that may hold reference to the object literal(directive options).
.directive('myDirective', function() {
var dir = {
link: function(scope, elem, attr) {
console.log(dir.name);
}
};
return dir;
});
[ 2 ] You can also get the directive's name by using the compile function since it is invoked in the context of the directive option.
.directive('myDirective', function() {
return {
compile: function(tElem, tAttr) {
var dirName = this.name;
return function(scope, elem, attr) { /* link function */ }
}
};
});
As far as I can tell you've answered your own question. You may prefix the name by string concatenation as you've done but it will probably be easier to add it as a separate data store.
element.data('directiveName', this.$name).data('startY', value);
I'm not sure what you mean by avoid collision as this will only apply to the element that was passed into the link function.

AngularJS: Watching for async values within directive's linking function

I'm using ng-grid for data display and I want to dynamically adjust height of the grid itself depending on the number of returned results and user's monitor resolution.
Here's the angular code:
angular.module('modulename', [])
.controller('ctrl', function($scope, $http) {
$scope.gridResult = [];
$scope.gridOptions = {
data: 'gridResult'
};
$scope.listData = function() {
//Async call using $http.get which on success callback asigns response.data to $scope.gridResult
}
//Get data when page loads
$scope.listData();
})
.directive('tableheight', function() {
return {
restrict: 'A',
scope: {
},
controller: 'ctrl',
link: function(scope, elem, attrs) {
scope.$watchCollection('gridResult', function(n, o) {
console.log(n); //Shows empty array
if (n.length > 0) {
console.log(n) //Never displayed
//Calculate new size for the grid
...
}
});
}
};
});
HTML portion:
<div data-ng-grid="gridOptions" tableheight></div>
As you can see 'gridResult' is always empty array even after 'listData' success callback. If I move whole $watchCollection method to controller body everything is working as expected. What I'm trying to achieve is somehow run directive's linking function after DOM has been rendered which includes rendering data from async call.
I guess I'm doing something wrong here or my approach is wrong, however I would appreciate if someone could offer a solution to this.
On a subject of calling directive's linking function after DOM is rendered and ready I tried putting the code inside angular's $timeout with 0 delay but that didn't do anything for me. And speaking of this is there any way to call directive's linking function after DOM rendering since I believe some of my problems are coming from this issue?
Just now there is no communication between the controller and directive. Here you find basic scenarios how such communication can be organized.
The most secure and generic option would be to create data bind like that:
angular.module('modulename', [])
.controller('ctrl', function($scope, $http) {
$scope.gridResult = [];
...
})
.directive('tableHeight', function() {
return {
require: 'dataGrid', // it would be better to use this directive only alongside data-grid
restrict: 'A',
scope: {
gridResult: '=tableHeight'
},
// controller: 'ctrl', this is unnecessary -- it points to directive's controller
link: function(scope, elem, attrs) {
scope.$watchCollection('gridResult', function(n, o) {
...
});
}
};
});
<div data-ng-grid="gridOptions" table-height="gridResult"></div>

AngularJS: What's the best practice to add ngIf to a directive programmatically?

I want to create a directive that checks if an element should be present in the dom based on a value coming from a service (e.g. check for a user role).
The corresponding directive looks like this:
angular.module('app', []).directive('addCondition', function($rootScope) {
return {
restrict: 'A',
compile: function (element, attr) {
var ngIf = attr.ngIf,
value = $rootScope.$eval(attr.addCondition);
/**
* Make sure to combine with existing ngIf!
* I want to modify the expression to be evalued by ngIf here based on a role
* check for example
*/
if (ngIf) {
value += ' && ' + ngIf;
}
attr.$set('ng-if', value);
}
};
});
At the end the element has the ng-if attribute attached but somehow it doesn't apply to the element and it is still existing in the dom. So this is obviously a wrong approach.
This fiddle shows the problem: http://jsfiddle.net/L37tZ/2/
Who can explain why this happens? Is there any other way a similar behaviour could be achieved? Existing ngIfs should be considered.
SOLUTION:
Usage: <div rln-require-roles="['ADMIN', 'USER']">I'm hidden when theses role requirements are not satifisfied!</div>
.directive('rlnRequireRoles', function ($animate, Session) {
return {
transclude: 'element',
priority: 600,
terminal: true,
restrict: 'A',
link: function ($scope, $element, $attr, ctrl, $transclude) {
var block, childScope, roles;
$attr.$observe('rlnRequireRoles', function (value) {
roles = $scope.$eval(value);
if (Session.hasRoles(roles)) {
if (!childScope) {
childScope = $scope.$new();
$transclude(childScope, function (clone) {
block = {
startNode: clone[0],
endNode: clone[clone.length++] = document.createComment(' end rlnRequireRoles: ' + $attr.rlnRequireRoles + ' ')
};
$animate.enter(clone, $element.parent(), $element);
});
}
} else {
if (childScope) {
childScope.$destroy();
childScope = null;
}
if (block) {
$animate.leave(getBlockElements(block));
block = null;
}
}
});
}
};
});
It is very important to add the priority in the directive, otherwise other directives attached to that element are not evaluated!
You can reuse ngIf in your own directive like this:
/** #const */ var NAME = 'yourCustomIf';
yourApp.directive(NAME, function(ngIfDirective) {
var ngIf = ngIfDirective[0];
return {
transclude: ngIf.transclude,
priority: ngIf.priority,
terminal: ngIf.terminal,
restrict: ngIf.restrict,
link: function($scope, $element, $attr) {
var value = $attr[NAME];
var yourCustomValue = $scope.$eval(value);
$attr.ngIf = function() {
return yourCustomValue;
};
ngIf.link.apply(ngIf, arguments);
}
};
});
and then use it like this
<div your-custom-if="true">This is shown</div>
and it will use all the "features" that come with using ngIf.
Joscha's answer is pretty good, but actually this won't work if you're using ng-if in addition of it.
I took Joscha's code and just added a few lines to combine it with existing ng-if directives :
angular.module('myModule').directive('ifAuthenticated', ['ngIfDirective', 'User', function(ngIfDirective, User) {
var ngIf = ngIfDirective[0];
return {
transclude: ngIf.transclude,
priority: ngIf.priority - 1,
terminal: ngIf.terminal,
restrict: ngIf.restrict,
link: function(scope, element, attributes) {
// find the initial ng-if attribute
var initialNgIf = attributes.ngIf, ifEvaluator;
// if it exists, evaluates ngIf && ifAuthenticated
if (initialNgIf) {
ifEvaluator = function () {
return scope.$eval(initialNgIf) && User.isAuthenticated();
}
} else { // if there's no ng-if, process normally
ifEvaluator = function () {
return User.isAuthenticated();
}
}
attributes.ngIf = ifEvaluator;
ngIf.link.apply(ngIf, arguments);
}
};
}]);
So if can then do things like :
<input type="text" ng-model="test">
<div ng-if="test.length > 0" if-authenticated>Conditional div</div>
And the conditional div will show only if you're authenticated && the test input is not empty.
The first part of your question, "why?", is something I can answer:
The problem you are running into is that you can't dynamically apply directives to elements without calling $compile on the element.
If you call $compile(element)(element.scope()) after you set the attribute, you run into a stack overflow because you are compiling yourself, which cause you to compile yourself which causes you to compile yourself, etc.
The second part, "how else to achieve", I am having trouble with. I tried a couple of approaches (like transcluding the content with a nested ng-if) but I can't get exactly the behavior you are looking for.
I think the next step might be to study the code for ng-if and try to implement something similar directly in your directive.
Here is a first pass of getting it working. I expect it needs some cleanup and modification to get it working how you really want it, however.
There is another way to solve this problem, using a templating function. This requires jquery 1.6+ to function properly.
A working fiddle of the code: http://jsfiddle.net/w72P3/6/
return {
restrict: 'A',
replace: true,
template: function (element, attr) {
var ngIf = attr.ngIf;
var value = attr.addCondition;
/**
* Make sure to combine with existing ngIf!
*/
if (ngIf) {
value += ' && ' + ngIf;
}
var inner = element.get(0);
//we have to clear all the values because angular
//is going to merge the attrs collection
//back into the element after this function finishes
angular.forEach(inner.attributes, function(attr, key){
attr.value = '';
});
attr.$set('ng-if', value);
return inner.outerHTML;
}
}
replace: true prevents embedded elements. Without replace=true the string returned by the template function is put inside the existing html. I.e. Hello becomes <a href="#" ng-if="'true'">Hello</a>
See https://docs.angularjs.org/api/ng/service/$compile for details.
return {
restrict: 'A',
terminal: true,
priority: 50000, // high priority to compile this before directives of lower prio
compile: function compile(element, attrs) {
element.removeAttr("add-condition"); // avoid indefinite loop
element.removeAttr("data-add-condition");
return {
pre: function preLink(scope, iElement, iAttrs, controller) { },
post: function postLink(scope, iElement, iAttrs, controller) {
iElement[0].setAttribute('ng-if', iAttrs.addCondition);
$compile(iElement)(scope);
}
};
}
The combination of high priority and terminal: true is the basis how this works: The terminal flag tells Angular to skip all directives of lower priority on the same HTML element.
This is fine because we want to modify the element by replacing add-condition with ng-if before calling compile, which then will process ng-if and any other directives.

Categories