I'm having troubles updating a variable on my controller's $scope from within a directive binding to those variables. For instance, I'm trying update the value with a setter-function whenever I move my mouse, but the value never gets updated.
I have made a fiddle here: http://jsfiddle.net/23hdsueb/
Any ideas how to set variables on the parent scope from my directive?
HTML:
<body ng-app="testApp" ng-controller="TestController">
<p>
outside directive
</p>
<input type="text" name="name" ng-model="obj.value">
<p>
value: {{obj.value}}
</p>
<test value="obj.value" set-value="setValue"></test>
</body>
JS:
angular.module('testApp', [])
.controller('TestController', ['$scope', function ($scope) {
$scope.obj = {
value: 'initial value'
};
$scope.setValue = function (val) {
$scope.obj.value = val;
};
}])
.directive('test', ['$document', function ($document) {
return {
restrict: 'E',
scope: {
value: '=',
setValue: '='
},
template: '<div class="test">inside directive<br>value: {{value}}</div>',
link: function (scope, element, attrs) {
$document.on('mousemove', function (event) {
scope.setValue(event.pageX + ' : ' + event.pageY);
});
}
};
}]);
Sorry for not giving the right answer in the first try, I had not understood the question properly. The answer is that your whole code is right except that you're missing one line of code. Normally you don't need to add it, but you do need to add it here.
$document.on('mousemove', function (event) {
scope.setValue(event.pageX + ' : ' + event.pageY);
scope.$apply();
});
While $document is an angularized variable, $document.on is not really the angular way of attaching event handlers to events.
Why is it good to follow angularized ways? Because they automatically run scope.$apply() when the command completes. The apply() function triggers a digest cycle which is responsible for angular's powers. The digest cycle among many things, checks if there are binded variables which need to be updated or not.
If you use ng-mousemove then you won't need the scope.$apply() line because the ng-mousemove directive triggers the digest cycle when it is fired.
What you are missing is, syntax for calling function of controller via directive /or/ How to call method in attribute of directive:
TL:DR you may go through beautiful explanation about same by Dan Wahlin
In your directive:
.directive('test', ['$document', function ($document) {
return {
restrict: 'E',
scope: {
value: '=',
setValue: '&'//for function it should be &
},
template: '<div class="test">inside directive<br>value: {{value}}</div>',
link: function (scope, element, attrs) {
$document.on('mousemove', function (event) {
//argVal should be exact name in the function call mentioned in directive's attribute
scope.setValue({argVal: event.pageX + ' : ' + event.pageY});
});
}
};
}]);
And your directive use should be like: Please Note: argVal should match exactly as in call to function from within link function call
<test value="obj.value" set-value="setValue(argVal)"></test>
For detail please refer: SO question
Related
In my angular application, I defined a directive foo-directive, which is placed in a parent controller as below:
<div ng-app="app">
<div ng-controller="ParentCtrl as parent">
<foo-directive data="parent.city" tab-click="parent.tClick()" tab-click2="parent.tClick2(v)"></foo-directive>
</div>
</div>
pass two method: parent.tClick() and parent.tClick2(v) into the directive and bind to tab-click and tab-click2 attributes respectively. The difference is that the second one has a parameter
and the JS code goes as below:
function ParentCtrl($timeout){
this.city= "London";
this.tClick = function(){
console.log("debugging parent tclick...");
}
this.tClick2 = function(v){
console.log("debugging parent tclick2...");
console.log(v)
}
}
function FirstCtrl() {
this.$onInit = function(){
this.click = function(){
this.tabClick();
this.tabClick2("abc");
}
}
}
function fooDirective() {
return {
scope: {
data: '=',
tabClick : "&",
tabClick2: "&"
},
controller: 'FirstCtrl',
controllerAs: 'foo',
bindToController: true,
template: '<div ng-click="foo.click()">{{ foo.data }}</div>',
link: function ($scope, $element, $attrs, $ctrl) {
//console.log($ctrl.name);
}
};
}
Now the issue comes from the second method this.tabClick2("abc"). There is TypeError message. I have reproduced this issue with this live demo:
https://jsfiddle.net/baoqger/sv4d03hk/1/
any help?
When passing the functions into your directive, you should pass the "reference" to the function, rather than the "result" of the function. Adding the parenthesis, is actually executing the function and returning the result to the directive. As neither function returns a value, they will both be passing undefined.
Given that the value of the parameter you want to pass (v) to the function is inside the directive scope, not that of the parent, you dont need to even tell the directive that the function accepts a parameter. You just pass it to the function inside the directive. ie.
<foo-directive data="parent.city" tab-click="parent.tClick" tab-click2="parent.tClick2"></foo-directive>
According to the docs, using & is then you want to evaluate the result of the attribute:
The & binding allows a directive to trigger evaluation of an expression in the context of the original scope, at a specific time.
Instead, we want to be able to execute the passed attribute, and in particular give it a variable. As such either = (two-way binding) or # (one-way binding) are probably more appropriate for what we're after.
tabClick: "=",
tabClick2: "="
You can also do away with your controller completely by updating your template.
template: '<div ng-click="foo.tabClick();foo.tabClick2(data)">{{ foo.data }}</div>'
Updated JSFiddle
function ParentCtrl($timeout) {
this.city = "London";
this.tClick = function() {
console.log("debugging parent tclick...");
}
}
function FirstCtrl() {}
function fooDirective() {
return {
scope: {
data: '=',
tabClick: "="
},
controller: 'FirstCtrl',
controllerAs: 'foo',
bindToController: true,
template: '<div ng-click="foo.tabClick(data)">{{ foo.data }}</div>',
link: function($scope, $element, $attrs, $ctrl) {
//console.log($ctrl.name);
}
};
}
angular
.module('app', [])
.directive('fooDirective', fooDirective)
.controller('FirstCtrl', FirstCtrl)
.controller('ParentCtrl', ParentCtrl)
<script src="https://code.angularjs.org/1.6.2/angular.min.js"></script>
<div ng-app="app">
<div ng-controller="ParentCtrl as parent">
<foo-directive data="parent.city" tab-click=":: parent.tClick"></foo-directive>
</div>
</div>
PS. if you're concerned about performance using # or =, consider one time bindings using ::. ie. <foo-directive data="parent.city" tab-click=":: parent.tClick" tab-click2=":: parent.tClick2"></foo-directive>
Try the following snippet of code for your "FirstCtrl":
function FirstCtrl() {
this.$onInit = function(){
this.click = function(){
this.tabClick({v: this.data});
}
}
}
As you are using an expression binding (&), you need to explicitly call it with a JSON containing "v" and it's value. like the following:
this.tabClick({v: this.data});
Here is my directive
.directive('closeMapMessage', function($log) {
'use strict';
return function(scope, element) {
var clickingCallback = function() {
angular.element('.map').fadeOut("slow");
};
element.bind('click', clickingCallback);
};
})
How can I change a scope variable in the controller ?
<div class="msg-mobile" ng-show="showInstructionModal">
<div class="close-map-msg ok-got-it-footer" close-map-message>Ok, got it. </div>
</div>
I basically want to set my showInstructionModalfalse when my close directive is called.
From the current snippet of code, it's hard to tell why you're not using a modal solution tailored for Angular, i.e. AngularUI's modal.
However, in your current code, you're attaching a click event to the element outside of Angular's awareness. That's why clicking on the element will not have effect until the next $digest cycle has run. Also, in Agular you normally don't use directives the way you're trying to do. I would suggest updating the directive to also provide the HTML and then use the ng-clickattribute to attach the event handler via Angular.
Update your directive's code to:
.directive('closeMapMessage', function($log) {
'use strict';
return {
restrict: "AE",
link: function(scope, element) {
scope.closeModal = function() {
angular.element('.map').fadeOut("slow");
scope.showInstructionModal = false; // probably need to put this in a $timeout for example to show the fading of the element
};
},
template: '<div class="close-map-msg ok-got-it-footer" ng-click="closeModal()">Ok, got it.</div>'
};
})
And then update your HTML accordingly:
<div class="msg-mobile" ng-show="showInstructionModal">
<close-map-message></close-map-message>
</div>
You should run digest cycle manually after click event occurrence to update all scope bindings
.directive('closeMapMessage', function($log) {
'use strict';
return function(scope, element) {
var clickingCallback = function() {
angular.element('.map').fadeOut("slow");
scope.$apply();
};
element.bind('click', clickingCallback);
};
})
First foray into custom directives and getting a bit stuck on binding to a method in the parent scope.
So when I use my custom directive in my app:
<dropdown x-label="Stuff" x-divider="-"
x-list = "listOfStuff"
x-ng-model="id"
x-change-select="controllerMethodToBeCalled(id)">
</dropdown>
The template is as follows and triggers the 'update' function in the directive controller on ng-change.
<div class="dropdown_container">
<div class="select_label">{{label}}</div>
<div class="select_divider">{{divider}}</div>
<select class="dropdown_select" id="dropdownList"
ng-model="ngModel"
ng-options="option.id as option.name for option in list | orderBy:'name'"
ng-change="update(ngModel)">
</select>
</div>
The directive code is below and I can call the method in parent scope directly whcih works fine but I'd like to trigger the 'change-select' that I've bound function 'controllerMethodToBeCalled' passing the id. Otherwise the component is not truly self contained.
angular.module('myApp.component.dropdown', [])
.directive('dropdown', function() {
return {
restrict: 'E',
require: "^ngModel",
replace: true,
scope: {
list: "=",
ngModel: '=',
changeSelect: '&'
},
templateUrl: 'component/dropdown/dropdown.tpl.html',
link: function ($scope, element, attrs) {
attrs.$observe('label', function (value) {
$scope.label = value;
});
attrs.$observe('divider', function (value) {
$scope.divider = value;
});
},
controller: function($scope){
$scope.update = function(id){
//replace this line with call to changeSelect passing id
$scope.$parent.controllermethodToBeCalled(id);
};
}
};
});
Probably very easy to fix but I just can't see it. Any ideas/suggestions?
You only need to call $scope.changeSelect(). See this Plunker. If you don't want to use the id from a parent scope, but rather pass the id argument from the function, you can do that do:
$scope.update = function(id){
$scope.changeSelect({id: id});
};
It basically says use the value of the local variable id for id in the expression controllerMethodToBeCalled(id).
I have the following in a controller:
Restangular.all('foos').getList().then(
function(foos) {
$scope.foos = foos;
});
In an HTML page, I am able to do the following:
<div ng-repeat="foo in foos | orderBy:'fooName'">
I want to move the ng-repeat to a directive, so I have the following in a directive:
app.directive('interactionFoos', function(){
return {
restrict: 'A',
scope : false,
link: function($scope, $element, $attrs) {
//console.log("*** size: " + $scope.foos.length);
}
}
});
And in the HTML I will have:
<div interaction-foos></div>
In the directive, I am getting undefined for $scope.foos.
As a test, in the controller, I hard coded: $scope.test= 'foobar'.
Then, in the directive, I replaced the log line with the following and it printed 'foobar':
console.log("*** test: " + $scope.test);
I do not know why $scope.test is working as I expect, but $scope.foos is not?
I believe this is an Async issue as Restangular would run as a promise so foo would not be set when the directive link function runs. To get around this you need to add a watch to see when the scope has changed
$scope.$watch('foos', function(newValue, oldValue) { console.log(newValue, oldValue); });
do something like this:
angular.module("app", [])
.controller("interactionCtrl", function($scope) {
$scope.foos = ["A", "B", "C"];
})
.directive("interactionFoos", function() {
return {
restrict: 'A',
scope: {
foos: '='
},
link: function(scope, element, attrs) {
alert(scope.foos.length);
}
};
});
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.min.js"></script>
<div ng-app="app" ng-controller="interactionCtrl">
<div interaction-foos foos="foos"></div>
</div>
basically the foos in the directive binds to the foos that are exposed on the controller.
also in your linking function don't use $ in the name of the function arguments, for example call the first argument scope instead of scope, because those arguments are not really injected to the link function (it's a regular function call, if you name the first argument bob it will still be equal to the scope)
Please refer to this fiddle for the questions. http://jsfiddle.net/AQR55/
1) Why a watch that is attached to an isolate scope property - which is bidirectionally bound to a parent property, is not triggering on changing the parent scope property.
In the fiddle, the below metioned watch is not getting triggered, on changing the parent scope property to which it is bound.
$scope.$watch('acts', function(neww ,old){
console.log(neww)
})
2) ng-click="addaction()" addaction="addaction()". Can this code be put in more elegant way? Because, to perform an action in isolated scope, it seems we need to set bidirectional binding and the attach to ng-click.
3)Can i declare methods inside the isolated scope like shown below? If i do like this, I'm getting .js error.
<isolate-scope-creating-cmp ng-click="isolateCmpClickHandler()"></isolate-scope-creating-cmp>
scope:{
isolateCmpClickHandler:function(){
//If i do like this, I'm getting .js error
}
}
Question 1.
Since you are adding a item to the acts array, you need to set the third parameter in $watch() to true
$scope.$watch('acts', function (neww, old) {
console.log(neww)
}, true);
Demo: Fiddle
Question 2.
Since there is an isolated scope, you need to call the $parent scope's function
<input type="button" bn="" acts="acts" ng-click="$parent.addaction()" value="Add Action" />
Demo: Fiddle
Question 3.
Yes you can, but you need to use a controller
animateAppModule.directive('bn', function () {
return {
restrict: "A",
scope: {
acts: '='
},
link: function ($scope, iElement, iAttrs) {
$scope.$watch('acts', function (neww, old) {
console.log(neww)
}, true)
},
controller: function($scope){
$scope.dosomething = function(){
console.log('do something')
}
}
}
})
Demo: Fiddle
An overall solution could look like
<input type="button" bn="" acts="acts" addaction="addaction()" value="Add Action" />
JS
animateAppModule.controller('tst', function ($scope) {
$scope.acts = [];
$scope.addaction = function () {
$scope.acts.push({
a: "a,b"
})
}
})
animateAppModule.directive('bn', function () {
return {
restrict: "A",
scope: {
acts: '=',
addaction: '&'
},
link: function ($scope, iElement, iAttrs) {
$scope.$watch('acts', function (neww, old) {
console.log(neww)
}, true);
iElement.click(function(){
$scope.$apply('addaction()')
})
}
}
})
Demo: Fiddle