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.
Related
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.
I've got a directive that adds a click handler to an element:
module.directive('toggleSection', ['$timeout', function ($timeout) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
element.bind('click', function (event) {
scope.$apply(function () {
var scopeProp = 'show' + attrs.toggleSection;
event.preventDefault();
event.stopPropagation();
scope[scopeProp] = !scope[scopeProp];
return false;
});
});
}
};
}]);
When the element is clicked, it toggles another property on the scope, which another element is bound to with ng-show. It's working as it should in the app.
I've added the following test for the directive:
(function () {
'use strict';
// get the app module from Angular
beforeEach(module('app'));
describe('myCtrl', function () {
var $scope, $rootScope;
beforeEach(inject(function ($controller, _$rootScope_) {
$scope = {};
$controller('myCtrl', { $scope: $scope });
$rootScope = _$rootScope_;
}));
describe('the toggleSection directive', function () {
var testElement;
beforeEach(function () {
testElement = $compile('<a toggle-section="Test" href="#">Collapse section</a>')($rootScope);
$rootScope.$digest();
});
it('inverts the value of the specified scope property', function () {
$scope.showTest = false;
testElement.click();
expect($scope.showTest).toEqual(true);
});
});
});
In the real code there are properties like $scope.showSection1 = false and by adding console logs in the directive I can see the properties before and after clicking the bound element and they have the expected values (e.g. the property starts as false and after you click the toggle element once it changes to true).
However, the test always fails with 'Expected false to equal true'. I think it's to do with the $apply method, because none of the show properties seem to exist on the scope when I run the test.
Other tests I have (even in the same spec file), which don't use the directive can see properties on the scope just fine.
What am I doing wrong?
There are a few things to be changed in your test:
1 - scope creation should be changed from $scope = {} into $scope = $rootScope.$new();
2 - the directive should be compiled not into rootScope, but into scope
3 - the directive should first be created via angularjs.element and then compiled:
element = angular.element('<my-directive/>');
compile(element)(scope);
scope.$digest();
I want to test that parent scope gets new value after directive changed it, but for some reason it does not work.
This is my directive
angular.module('myModule').directive('myDirective', function(){
return {
template: 'just a template',
restrict: 'A',
scope: {
'model' : '=myDirective'
},
link: function postLink( scope ){
scope.changeModelValue = function( value ){
scope.model = value;
}
}
}
});
This is my test
describe('the test', function(){
var scope, isolateScope, element;
var reset = function(){
scope = null;
isolateScope = null;
element = null;
};
beforeEach( inject(function( $rootScope ){
scope = $rootScope.new();
reset();
}));
var setup = inject(function( $compile ){
element = angular.element('<div my-directive="item"></div>');
element = $compile(element)(scope);
scope.$digest();
isolateScope = element.children().scope();
});
it('should work', inject(function( $timeout ){
scope.item = null;
setup();
expect(typeof(isolateScope.changeModelValue)).toBe('function'); // works!
isolateScope.changeModelValue('new value');
expect(isolateScope.model).toBe('new value'); // works!
// tried all the of this - but didn't help..
waitsFor(function(){
try{ $timeout.flush(); } catch (e) { }
try{ scope.$digest.flush(); } catch (e) { }
try{ isolateScope.$digest(); } catch (e) { }
return scope.reportLesson !== null;
});
runs(function(){
expect(scope.item).toBe('new value'); //fails!!
});
}));
});
As you can see I tried some flushing and such, thinking perhaps there's some async actions that need to happen in order for it to work, but it didn't help.
The test reaches timeout on the waitsFor.
How can I make it work?
Turns out that $digest and $timeout.flush will not affect the binding.
In order to make it work, I had to call isolateScope.$apply(). I would still like to have an explanation for this.
Uh I might be wrong but it looks like the way you're doing the binding there doesn't make sense.
// 2-way-bind the value of 'model' in this scope to the myDirective attribute in the parent scope
scope: {
'model' : '=myDirective'
},
If that were '=item' instead then that makes sense given what you want, so try that.
Another thing you are doing which is a bit odd:
// Give my directive an attribute which is the 'item' element from the scope
element = angular.element('<div my-directive="item"></div>');
So that's a bit weird, read this SO answer: AngularJS Directive Value
You're overloading your directive declaration with an attribute, and you're mixing up scope variables with attributes (which can be accessed through your link function, but not what you're doing).
Edit:
As a final note, you should probably trust AngularJS is doing the binding correctly. It is time consuming to write these kind of tests and you should focus on program logic instead.
so I have this custom directives that you could see below.
myApp.directive('myDirective', function (testService) {
return {
restrict:'EA',
link:function (scope, element, attr) {
//defined the object
var object = new object();
testService.setObject(object);
}
}
});
myApp.directive('mySecondDirective', function (testService) {
return {
restrict:'EA',
link:function (scope, element, attr) {
//call the variable from previous custom directive
console.log(testService.getobject()); -> always return undefined
}
}
});
and this is the html structure where I used the directives above.
<my-directive></my-directive>
<my-second-directive></my-second-directive>
there I want to retreive the object that contains new object() from previous custom directive, but it always return an undefined I wonder how could I do this without using require nor isolated scope as well.. could you guys help me ?
UPDATE
I create a service to provide the facility to set and retreive the object and apparently it returned undefined because I set the custom direcitve this way
<my-second-directive></my-second-directive>
<my-directive></my-directive>
and this is the service
define(
['services/services'],
function(services)
{
'use strict';
services.factory('testService', [function() {
var me = this;
var testObject = '';
return {
setObject: function(object) {
me.testObject = object;
},
getObject: function() {
return me.testObject;
}
}
}
]);
}
);
the thing is that I actually set the html markup like I already mentioned above which is
<my-second-directive></my-second-directive>
<my-directive></my-directive>
so could you give me some advice on how should I do this ?
note* that the passing object actually worked I prefer using services because it will easy to mantain latter. The question is how do I make the object accessible from another directive even though the initiate install of the object (set the object) in the directive that I defined at the html markup, as the last position of the html it self ?
UPDATE this is the PLUNKER that I've been made for you to understand the question it self
You could achieve this by firing a custom event and then listening for it in the second directive. Here's an updated plunker: http://plnkr.co/edit/512gi6mfepyc04JKfiep?p=info
Broadcast the event from the first directive:
app.directive('myDirective', function(testService) {
return {
restrict: 'EA',
link: function(scope, elm, attr) {
var object = {};
testService.setObject(object);
console.log('setting object');
scope.$broadcast('objectSet');
}
}
});
... and then listen for it on the second:
app.directive('mySecondDirective', function(testService) {
return {
restrict: 'EA',
link: function(scope, elm, attr) {
scope.$on('objectSet', function(){
console.log('retrieving object', testService.getObject());
})
}
}
});
You could also pass data along with the event, if you wanted to emit a specific piece of data to be picked up by the second directive.
1). Scope. Since you don't want to use controllers and isolated scope, then you can simply set this object as scope property.
myApp.directive('myDirective', function() {
return {
restrict: 'EA',
link: function(scope, element, attr) {
var object = {};
object.test = 21;
// set object as scope property
scope.object = object;
}
}
});
myApp.directive('mySecondDirective', function() {
return {
priority: 1, // priority is important here
restrict: 'EA',
link: function(scope, element, attr) {
console.log('Retrieve: ', scope.object);
}
}
});
Just make sure you are also defining priority on the second directive (only if both directive a applied to the same element) to make sure it's evaluated in proper turn (should be bigger then the one of myDirective, if omitted it's 0).
2). Service. Another simple solution is to use service. You can inject custom service into both directives and use it storage for you shared object.
Expanding from what #gmartellino said.
Anything that you wanted to do after listening to the event in second directive, can have a callBack method and use it.
app.directive('mySecondDirective', function(testService) {
return {
restrict: 'EA',
link: function(scope, elm, attr) {
// what if I created like this ?
// define the test variable
var test;
scope.$on('objectSet', function(){
//set the variable
test = testService.getObject();
console.log('retrieving object : ', testService.getObject());
//Anything that you wanted to do
//after listening to the event
//Write a callBack method and call it
codeToExecuteAsCallBack();
})
//then use this method to make a call back from the event
//and outside the event too.
var codeToExecuteAsCallBack = function(){
console.log(test);
}
codeToExecuteAsCallBack();
}
}
});
Updated plnkr link
So I have this directive called say, mySave, it's pretty much just this
app.directive('mySave', function($http) {
return function(scope, element, attrs) {
element.bind("click", function() {
$http.post('/save', scope.data).success(returnedData) {
// callback defined on my utils service here
// user defined callback here, from my-save-callback perhaps?
}
});
}
});
the element itself looks like this
<button my-save my-save-callback="callbackFunctionInController()">save</button>
callbackFunctionInController is for now just
$scope.callbackFunctionInController = function() {
alert("callback");
}
when I console.log() attrs.mySaveCallback inside my-save directive, it just gives me a string callbackFunctionInController(), I read somewhere that I should $parse this and it would be fine, so I tried to $parse(attrs.mySaveCallback) which gave me back some function, but hardly the one I was looking for, it gave me back
function (a,b){return m(a,b)}
What am I doing wrong? Is this approach flawed from the beginning?
So what seems like the best way is using the isolated scope as suggested by ProLoser
app.directive('mySave', function($http) {
return {
scope: {
callback: '&mySaveCallback'
}
link: function(scope, element, attrs) {
element.on("click", function() {
$http.post('/save', scope.$parent.data).success(returnedData) {
// callback defined on my utils service here
scope.callback(); // fires alert
}
});
}
}
});
For passing parameters back to controller do this
[11:28] <revolunet> you have to send named parameters
[11:28] <revolunet> eg my-attr="callback(a, b)"
[11:29] <revolunet> in the directive: scope.callback({a:xxx, b:yyy})
There are a lot of ways to go about what you're doing. The FIRST thing you should know is that the $http.post() is going to be called as soon as that DOM element is rendered out by the template engine, and that's it. If you put it inside a repeat, the call will be done for each new item in the repeater, so my guess is this is definitely not what you want. And if it is then you really aren't designing things correctly because the existence of DOM alone should not dictate queries to the backend.
Anyway, directly answering your question; if you read the albeit crappy docs on $parse, it returns you an evaluation expression. When you execute this function by passing the scope to evaluate on, the current state of that expression on the scope you passed will be returned, this means your function will be executed.
var expression = $parse(attrs.mySave);
results = expression($scope); // call on demand when needed
expression.assign($scope, 'newValu'); // the major reason to leverage $parse, setting vals
Yes, it's a little confusing at first, but you must understand that a $scope changes constantly in asynchronous apps and it's all about WHEN you want the value determined, not just how. $parse is more useful for a reference to a model that you want to be able to assign a value to, not just read from.
Of course, you may want to read up on creating an isolate scope or on how to $eval() an expression.
$scope.$eval(attrs.mySave);
You can use .$eval to execute a statement in the given scope
app.directive('mySave', function($http) {
return function(scope, element, attrs) {
$http.post('/save', scope.data).success(returnedData) {
// callback defined on my utils service here
// user defined callback here, from my-save-callback perhaps?
scope.$eval(attrs.mySaveCallback)
}
}
});
TD: Demo
If you want to share data between a directive and a controller you can use the two way binding
app.controller('AppController', function ($scope) {
$scope.callbackFunctionInController = function() {
console.log('do something')
};
$scope.$watch('somedata', function(data) {
console.log('controller', data);
}, true);
});
app.directive('mySave', function($http, $parse) {
return {
scope: {
data: '=mySaveData',
callback: '&mySaveCallback' //the callback
},
link: function(scope, element, attrs) {
$http.get('data.json').success(function(data) {
console.log('data', data);
scope.data = data;
scope.callback(); //calling callback, this may not be required
});
}
};
});
Demo: Fiddle
scope: {
callback: '&mySaveCallback'
}
Setting the scope explicitly could be a good solution but if you want the reach other parts of the original scope you can't because you have just overwritten it. For some reason, I needed to reach other parts of the scope too so I used the same implementation as ng-click do.
The use of my directive in HTML:
<div my-data-table my-source="dataSource" refresh="refresh(data)">
Inside the directive (without setting the scope explicitly):
var refreshHandler = $parse(attrs.refresh);
scope.$apply(function () {
refreshHandler( {data : conditions}, scope, { $event: event });
});
With this I can call the function in controller and pass parameters to it.
In the controller:
$scope.refresh= function(data){
console.log(data);
}
And it prints the conditions correctly out.
This worked for me
Inside the view script
<tag mycallbackattrib="scopemethod">
Inside the directive
$scope[attrs.mycallbackattrib](params....);
It is correctly called and params are passed, but maybe is not a best 'angular way' to work.
You should be using ng-click instead of creating your own directive.
app.directive('mySave', function($http, $parse) {
return {
scope: {
data: '=mySaveData',
callback: '&' //the callback
},
link: function(scope, element, attrs) {
$http.get('data.json').success(function(data) {
console.log('data', data);
if (scope.callback()) scope.callback().apply(data);
});
}
};
});