Angular directive "=" value binds to scope, but is undefined? - javascript

I have defined the following directive inside my Angular app:
(function() {
'use strict';
angular
.module('almonds')
.directive('security', ['$animate', 'AuthFactory', directive]);
function directive($animate, AuthFactory) {
var directive = {
restrict: 'EA',
scope: {
operation: "#",
clearance: "#",
project: "="
},
link: linkFunc
};
return directive;
function linkFunc($scope, $element, $attr, ctrl, $transclude) {
var block, childScope, previousElements;
console.log($scope.project);
if($scope.project) {
var projectId = $scope.project.id;
}
var operation = $scope.operation;
var clearance = $scope.clearance;
if (projectId) {
var value = AuthFactory.hasProjectAccess(projectId, clearance);
console.log('PROJECT SECURITY:', projectId, clearance, value);
} else {
var value = AuthFactory.hasAccess(operation, clearance);
console.log('ORG SECURITY:', operation, clearance, value);
}
}
}
// Controller.$inject = ['$scope'];
//
// function Controller($scope) {
// var vm = this;
//
// activate();
//
// function activate() {
//
// }
// }
})();
It is to be used as an element that receives either an operation or project value as well as a clearance value, which will then be used whether said element will render (I omitted that part but it's functionality is basically the same as ng-if).
Here's an example of it in use:
<span security project="vm.project" clearance="admin">
<a role="button" ng-click="vm.confirmDeletion();"><span class="melon-icon-md melon-icon-trash"></span></a>
</span>
What's happening though, is that even though vm.project is indeed defined, that console.log($scope.project); yields undefined. Interestingly, if I simply console.log($scope); it will contain a project property with the information I need. What am I doing wrong?
I actually only need the project's id value, so I can either pass in the entire project object and access its id within the directive, or somehow pass the id number alone.

When your directive's link function starts executing, vm.project is undefined at that point. Set a watch on $scope.project.
Inside Directive:
$scope.$watch('project', function (newValue, oldValue) {
if (newValue) {
// do stuff
}
});

Related

AngularJS - $q undefined in a directive

I'm trying to inject $q into my directive, but though $q is defined as a resolver() at first, when calling the function it is undefined. Maybe something related to binding? I don't know.
(function () {
'use strict';
myForm.$inject = ["$q"];
angular
.module('myModule')
.directive('myForm', myForm);
function myForm($q) {
return {
restrict: 'EA',
scope: {
ngSubmitFunction: '&',
},
templateUrl: 'myTemplate',
controllerAs: 'ctrl',
controller: ["$scope", "$window", "$q", function ($scope, $window, $q) {
var vm = this;
vm.name = 'myForm';
$scope.submitPromise = function(){};
vm.ngSubmit = ngSubmit;
function ngSubmit($form) {
vm.submitDisabled = true;
$form.$setSubmitted();
if ($form.$valid) {
$scope.submitPromise().then(function() {
vm.submitDisabled = false;
});
}
}
}],
link: function (scope, element, attrs) {
console.log($q);
scope.submitPromise = function($q) {
console.log($q);
var deferred = $q.defer();
scope.ngSubmitFunction();
return deferred.promise;
}
}
};
}
}());
The objective is to call ngSubmit when user clicks on a button. ngSubmit disables the button, waits for the async calls to be over and then enables the button.
In the example code, the 1st console.log($q) (executed when loading the page) outputs this:
Q(resolver) {
if (!isFunction(resolver)) {
throw $qMinErr('norslvr', "Expected resolverFn, got '{0}'", resolver);
}...
Which to me looks like correct.
But when calling submitPromise() after pressing the button, this is the output:
undefined
TypeError: Cannot read property 'defer' of undefined
When is $q lost?
Note: this is not the only version I tried, originally all code was on controller, nothing on link. I've also been told this pattern is deprecated and to use this one, which is better:
function submitPromise($q) {
return $q(function (resolve) {
$scope.ngSubmitFunction();
})
}
Nothing worked. Everything produces the same error, $q gets undefined at some point and can't find out why.
Using $q as an argument parameter is causing $q to become undefined.
myForm.$inject = ["$q"];
angular
.module('myModule')
.directive('myForm', myForm);
function myForm($q) {
return {
link: function (scope, element, attrs) {
console.log($q);
//scope.submitPromise = function($q) {
//Remove $q as parameter
scope.submitPromise = function() {
console.log($q);
var deferred = $q.defer();
//scope.ngSubmitFunction();
deffered.resolve(scope.ngSubmitFunction());
return deferred.promise;
}
}
The myForm function is a directive construction function to which the AngularJS framework will inject service providers. But the submitPromise function is not injectable; it is a child function of myForm. All injections should be done in the parent function.
Also the code can be simplified by using $q.when to create a promise.
link: function (scope, element, attrs) {
console.log($q);
//scope.submitPromise = function($q) {
//Remove $q as parameter
scope.submitPromise = function() {
console.log($q);
return $q.when(scope.ngSubmitFunction());
}
}
You should solve with this different injection
(function () {
'use strict';
angular
.module('myModule')
.directive('myForm', ['$q', function($q){
return {
. . .
}
}]);
Hope I've been helpful.
The right code is ( I removed parameter from function declaration ):
$scope.submitPromise=function() {
return $q(function (resolve) {//$q is available in function declared in the same scope
$scope.ngSubmitFunction();
});
}
Above code use $q variable from scope ( javascript scope not angular $scope ), $q is visible for all functions declared inside myForm function.
Your previous code used function parameter not $q from scope, parameter was not passed, so was undefined.
Javascript scope means everything between open tag { and close tag }. Check this example:
function(y){//scope start
var x; //scope local variable
var someFunc=function(){
//here is available y and x variables
};
//scope end
}
//outside of scope - here variables x and y not exists

What is the scope of ngModel directive in AngularJS?

I think that ngModel directive should not create new scope as it needs to make changes in the variables of parent scope.
Please correct me if i am wrong .
And also looking at the source of ngModel directive scope is not defined so it should not create a new scope for directive.
var ngModelDirective = ['$rootScope', function($rootScope) {
return {
restrict: 'A',
require: ['ngModel', '^?form', '^?ngModelOptions'],
controller: NgModelController,
// Prelink needs to run before any input directive
// so that we can set the NgModelOptions in NgModelController
// before anyone else uses it.
priority: 1,
compile: function ngModelCompile(element) {
// Setup initial state of the control
element.addClass(PRISTINE_CLASS).addClass(UNTOUCHED_CLASS).addClass(VALID_CLASS);
return {
pre: function ngModelPreLink(scope, element, attr, ctrls) {
var modelCtrl = ctrls[0],
formCtrl = ctrls[1] || modelCtrl.$$parentForm;
modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options);
// notify others, especially parent forms
formCtrl.$addControl(modelCtrl);
attr.$observe('name', function(newValue) {
if (modelCtrl.$name !== newValue) {
modelCtrl.$$parentForm.$$renameControl(modelCtrl, newValue);
}
});
scope.$on('$destroy', function() {
modelCtrl.$$parentForm.$removeControl(modelCtrl);
});
},
post: function ngModelPostLink(scope, element, attr, ctrls) {
var modelCtrl = ctrls[0];
if (modelCtrl.$options && modelCtrl.$options.updateOn) {
element.on(modelCtrl.$options.updateOn, function(ev) {
modelCtrl.$$debounceViewValueCommit(ev && ev.type);
});
}
element.on('blur', function() {
if (modelCtrl.$touched) return;
if ($rootScope.$$phase) {
scope.$evalAsync(modelCtrl.$setTouched);
} else {
scope.$apply(modelCtrl.$setTouched);
}
});
}
};
}
};
}];
Also I don't understand why ngModel directive requires ngModel itself.
require: ['ngModel', '^?form', '^?ngModelOptions']
Can't it be ignored and written like
require: ['^?form', '^?ngModelOptions']
If not then please explain why ?
ngModel doesn't create an isolated scope. The reason ngModel is listed in the require array is so that its controller (NgModelController) will be injected into the link function. Notice the ctrls argument that is passed into the ngModelPostLink function. Because ngModel is listed in the array, ctrls[0] will be an instance of the NgModelController. ctrls[1] is the form controller, etc.

How to properly watch scope for data load

I've built a custom Angular directive which uses D3.js to build a visualization. I reference this directive in my HTML like so:
<gm-link-analysis data="linkAnalysis.connections"></gm-link-analysis>
The relevant portion of directive code looks like this:
angular.module('gameApp')
.directive('gmLinkAnalysis', gmLinkAnalysis);
gmLinkAnalysis.$inject = ['$location', 'd3'];
function gmLinkAnalysis($location, d3) {
var directive = {
restrict: 'E',
templateUrl: '/app/gmDataVis/gmLinkAnalysis/gmLinkAnalysis.directive.html',
controller: 'LinkAnalysisController',
controllerAs: 'linkAnalysis',
scope: {
data: '='
},
link: function(scope) {
scope.$watch('data', function(json) {
console.log(json);
if (json) {
root = json;
root.fixed = true;
root.x = width / 2;
root.y = height / 2;
return scope.render(root);
}
});
...
}
};
return directive;
};
...and my controller below:
angular.module('gameApp')
.controller('LinkAnalysisController', LinkAnalysisController);
LinkAnalysisController.$inject = ['$routeParams', 'dataVisService'];
function LinkAnalysisController($routeParams, dataVisService) {
var vm = this;
var userId = $routeParams.userId;
var getConnections = function() {
dataVisService.getConnections({
userId: userId
}).$promise.then(function(connections) {
vm.connections = connections;
console.log(vm.connections);
});
};
var init = function() {
getConnections();
};
init();
}
It appears that my directive loads before my controller loads the data. I keep seeing undefined (from my log within the directive) followed by the data object I'm looking for (from my log within my controller). I understand that the directive would load before my asynchronous API call returns the data in my controller. What I do not understand is why the $watch does not pick up on this data when it finally is loaded. How would I go about getting this data into my directive?
The problem might be that your watch is not 'deep' enough. To watch whole object, not simply variable, you can pass third argument true to $watch function:
link: function(scope) {
scope.$watch('data', function(json) {
...
}, true);
...
If there is no need to go deep all the way, $watchCollection might be used.
More information here

Angular directive link function never runs

I followed the tutorial of ng-bbok, and at the directive definition an empty compile function is inserted and then the link function. With this the code in the link function never got executed. Finally i figured out that is because the empty compile function, when i deleted it magically the link got executed. Why is it happening like this? Im using Angular 1.3
{
compile: function() {},
link: function($scope, element, attributes) {
var size = attributes.gravatarSize || 80;
var hash = md5.digest_s($scope.email.from[0]);
$scope.gravatarImage = url + hash + '?size=' + size;
}
}
You can't define both compile property and link. If you want to use the compile function you can either return the link function:
compile: function() {
return function($scope, element, attributes) {
var size = attributes.gravatarSize || 80;
var hash = md5.digest_s($scope.email.from[0]);
$scope.gravatarImage = url + hash + '?size=' + size;
}
}
Or define both pre and post (link) functions:
compile: function compile(tElement, tAttrs, transclude) {
return {
pre: function preLink(scope, iElement, iAttrs, controller) { ... },
post: function postLink(scope, iElement, iAttrs, controller) { ... }
}
}
Check the documentation
It's happening by design. To quote the $compile docs:
link
This property is used only if the compile property is not defined.

Why is my $watch not working in my directive test - Angularjs

I have a directive in which I pass in an attrs and then it is watched in the directive. Once the attrs is changed, then an animation takes place. My attrs always is undefined when the $watch gets triggered.
App.directive('resize', function($animate) {
return function(scope, element, attrs) {
scope.$watch(attrs.resize, function(newVal) {
if(newVal) {
$animate.addClass(element, 'span8');
}
});
};
});
And here is my test:
describe('resize', function() {
var element, scope;
beforeEach(inject(function($compile, $rootScope) {
var directive = angular.element('<div class="span12" resize="isHidden"></div>');
element = $compile(directive)($rootScope);
$rootScope.$digest();
scope = $rootScope;
}));
it('should change to a span8 after resize', function() {
expect($(element).hasClass('span12')).toBeTruthy();
expect($(element).hasClass('span8')).toBeFalsy();
element.attr('resize', 'true');
element.scope().$apply();
expect($(element).hasClass('span8')).toBeTruthy();
});
});
When the attrs changes, my watchers newValue is undefined and so nothing happens. What do I need to do to make this work? Here is a plunker
You are not watching the value of attrs.resize; you are watching the value pointed by attrs.resize instead, in the test case a scope member called isHidden. This does not exist, thus the undefined.
For what you aare trying to do, the following would work:
App.directive('resize', function($animate) {
return function(scope, element, attrs) {
scope.$watch(
// NOTE THE DIFFERENCE HERE
function() {
return element.attr("resize");
// EDIT: Changed in favor of line above...
// return attrs.resize;
},
function(newVal) {
if(newVal) {
$animate.addClass(element, 'span8');
}
}
);
};
});
EDIT: It seems that the attrs object does NOT get updated from DOM updates for non-interpolated values. So you will have to watch element.attr("resize"). I fear this is not effective though... See forked plunk: http://plnkr.co/edit/iBNpha33e2Xw8CHgWmVx?p=preview
Here is how I was able to make this test work. I am passing in a variable as an attr to the directive. The variable name is isHidden. Here is my test with the updated code that is working.
describe('resize', function() {
var element, scope;
beforeEach(inject(function($compile, $rootScope) {
var directive = angular.element('<div class="span12" resize="isHidden"></div>');
element = $compile(directive)($rootScope);
$rootScope.$digest();
scope = $rootScope;
}));
it('should change to a span8 after resize', function() {
expect($(element).hasClass('span12')).toBeTruthy();
expect($(element).hasClass('span8')).toBeFalsy();
element.scope().isHidden = true;
scope.$apply();
expect($(element).hasClass('span8')).toBeTruthy();
});
});
I am able to access the variable isHidden through the scope that is attached to the element. After I change the variable, the I have to run $digest to update and then all is golden.
I feel that I should probably be using $observe here as was noted by package. I will look at that and add a comment when I get it working.
As Nikos has pointed out the problem is that you're not watching the value of attrs.resize so what you can try doing is this:
Create a variable to hold your data and create these $watch functions:
var dataGetter;
scope.$watch(function () {
return attrs.resize;
}, function (newVal) {
dataGetter = $parse(newVal);
});
scope.$watch(function () {
return dataGetter && dataGetter(scope);
}, function (newVal) {
// Do stuff here
});
What should happen here is that Angular's $parse function should evaluate attrs.resize and return a function like this. Then you pass it the scope and do something. As long as attrs.resize is just a boolean then newVal in the 2nd watch expression should be a boolean, I hope.

Categories

Resources