How to pass object to directive through attribute in AngularJS? - angularjs

I want to pass object (reference - two way binding) through ATTRIBUTE not by isolated scope. How can I do this? Because code bellow passing string instead of object:
HTML
<tr ng-form="rowForm" myDirective="{{row.data}}">
Directive
angular.module("app").directive("myDirective", function () {
return {
require: ["^form"],
restrict: "A",
link: function (scope, element, attrs, ctrls) {
scope.$watch(function () {
return attrs.myDirective;
}, function (newValue, oldValue) {
// .....

Directives can do two-way data binding without parsing or compile anything manually, sorry for not delivering the plunker but it's rebelius and won't save for me
JS
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.myObj = {name: 'Tibro', age: 255}
})
.directive('myDirective', function(){
return {
scope: {
'myAttribute': '='
},
template: '{{myAttribute}}',
link: function(scope){
scope.myAttribute.age= 31
}
}
})
HTML
<body ng-controller="MainCtrl">
controller: {{myObj}} <br/>
directive: <my-directive my-attribute="myObj"></my-directive>
</body>
OUTPUT
controller: {"name":"Tibro","age":31}
directive: {"name":"Tibro","age":31}
you can see from the output that passed object has been binded two-way and change made in directive is reflected on controller level

The result of {{ }} interpolation is a string. An object can't be passed like that.
Bindings are idiomatic here and thus preferable. The whole thing becomes messy when the directive is forced to use parent scope. However, it can be done by parsing scope properties manually with $parse:
$scope.$watch(function () {
var myDirectiveGetter = $parse($attrs.myDirective);
return myDirectiveGetter($scope);
}, ...);
This is a job for binding (< or =, depending on the case). If isolated scope isn't desirable, this can be done with inherited scope and bindToController:
scope: true,
bindToController: {
myDirective: '<'
},
controllerAs: `vm`,
controller: function ($scope) {
$scope.$watch('vm.myDirective', ...);
}
Notice that directive attribute is my-directive, not myDirective.

Couple of things:
myDirective in the tr should be my-directive, as per Angular's
conventions.
{{row.data}} prints the variable, you need to pass it without the
{{}} for it to go through as an object.

Related

angularjs: can' t pass parameter to the parent controller method via '&' binding in isolated directive scope

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});

How to bind the ng-repeat variable to a custom angular directive?

In Angular (v1.3) how do you bind the variable from an ng-repeat to a custom directive?
All the examples seem to bind to a $scope variable instead. eg given a list of products and a custom directive called product, how can we do something to this effect:
<li ng-repeat="product in products">
<product item="product" />
</li>
Where the directive is created as:
(function () {
'use strict';
var productDirective = function () {
return {
restrict: 'E',
scope: {item: '='},
templateUrl: '/views/product.html',
};
};
angular
.module('myApp')
.directive('product', productDirective);
})();
And it has a very simple template:
<p>{{item.name}} {{item.price}}</p>
This fails because scope: '=' only binds to $scope, not to ng-repeat.
I was able to get it to work with
return {
restrict: 'E',
scope: {item: '#'},
templateUrl: '/views/product.html',
link: function(scope, element, attributes){
scope.item = scope.$eval(attributes.item);
}
};
But the usage of $eval is not really acceptable of course (bad style).
What is the correct angular way to achieve this?
This fails because scope: '=' only binds to $scope, not to ng-repeat.
I don't quite understand what you mean by the above, but the = in the bindings context is useful when you bind an object from the parent scope of the directive - which is in fact the controller where you have used the product directive - to the isolated scope of the directive, and NOT the ng-repeat.
Therefore, in your case, you don't actually need to bind an object from a parent controller, to the directive's isolated scope with the $eval trick.
Just make sure that in the particular parent controller you have defined the array of products properly.
Here's a Demo of how you can get it working.
This fails because scope: '=' only binds to $scope, not to ng-repeat.
It should NOT fail because ng-repeat creates a scope for every iteration and puts the current product to it along with the other loop vars like $index. So, the product is actually on the scope and you can bind to it normally.
I created a FIDDLE with no changes to your code to confirm this.
Try adding a controller to the productDirective:
var productDirective = function () {
return {
restrict: 'E',
scope: {item: '='},
templateUrl: '/views/product.html',
controller: ['$scope', function($scope) {}]
};
};

Unable to pass controller and directive scope into child directive using require

I am trying to pass the controller scope of parent controller and parent directive into a child directive but facing an error saying that the controller is not available. Here is a plunk for that
http://plnkr.co/edit/aahgOK9oFFjcP2y5VkVa?p=preview
HTML:
<div ng-controller="MainCtrl as mc">
<new-dir>
<data-customer-details customer="mc.customers[0]" logger="mc.logger()" version-age="{{mc.age}}"></data-customer-details>
</new-dir>
</div>
OK, so I tinkered with your plunker a bit. I couldn't get it working using Controller As...I had to change it over to $scope injection on the main controller. Then I created a new scope on newDir by setting scope: true.
You don't actually need to require the MainCtrl because these directives are automatically children of that scope anyway.
I changed your 'MainCtrl' to this:
angular.module('plunker').controller('MainCtrl', function ($scope) {
$scope.name = 'World';
$scope.customers = [{
"name": "angularjs 1.4",
"version": "1.4"
}, {
"name": "angularjs 1.3",
"version": "1.3"
}, {
"name": "angularjs 1.2",
"version": "1.2"
}];
$scope.age = 30;
$scope.logger = function() {
console.log('clicked');
}
$scope.ctrlScopeVariable = 'im in controller scope';
})
Minor change to newDir:
function newDir (){
return {
scope: true, //you need this
controller: function($scope){
$scope.val= 'someval';
console.log($scope.$parent.ctrlScopeVariable)
},
link: function(scope, el, attr, ctrl) {
console.log(scope.$parent.name)
}
}
}
And the last directive:
function CustomerDetails() {
var directive = {
scope: {
customer: '=',
logger: '&',
myNewAge: '#versionAge'
},
restrict: 'EA',
require: ['^newDir'],
controllerAs: 'cd',
templateUrl: 'customer-details.html',
link: linkFunction,
controller: function($scope){
console.log($scope.$parent.$parent.ctrlScopeVariable);
var cd = this;
cd.newval = 'new val';
}
};
function linkFunction(scope, elem, attributes, controllers, transclude) {
console.dir(controllers);
scope.fromMainCtrl = scope.$parent.$parent.ctrlScopeVariable
}
return directive;
}
The Plunker
I added a binding to the customer details template that passes in the $scope.ctrlScopeVariable from the main controller, so you can see the MainCtrl scope is accessible form the child directive.
In regards to require, the relevant documentation is here, I think:
If it is necessary to reference the controller or any functions bound
to the controller's scope in the template, you can use the option
controllerAs to specify the name of the controller as an alias. The
directive needs to define a scope for this configuration to be used.
This is particularly useful in the case when the directive is used as
a component.
Looking back at myPane's definition, notice the last argument in its
link function: tabsCtrl. When a directive requires a controller, it
receives that controller as the fourth argument of its link function.
Taking advantage of this, myPane can call the addPane function of
myTabs.
Essentially, you can use it to reference a parent controller on which you need to access some functions or something. Notably, it becomes available under whatever alias you give it as the fourth argument of your link function.
EDIT:
In this Plunker I added a function to the controller of newDir, required newDir in the CustomerDetail directive, and then called that function in the CustomerDetail link function:
CustomerDetails directive:
//some stuff
require: '^newDir',
//some stuff
link: function(scope, el, attr, newDirCtrl) {
console.log(newDirCtrl.doubleNum(100));
}
newDir controller:
controller: function($scope){
this.doubleNum = function(num) {
return num*2
}
// some stuff
}
First you need to declare a variable as callback function:
var MainCtrlFn = function() { .... }
Then, you can set it as parameter to angularJS:
angular.module('plunker').controller('MainCtrl', MainCtrlFn);

In an AngularJs directive, how do I call its controller's function contained within an directive element's attribute?

I am trying to call a function in a controller, which is part of a custom angular directive, following is the code,
Method 1: (Doesn't work: Controller's function doesn't write to the console)
HTML:
<div ng-app="MyApp">
<my-directive callback-fn="ctrlFn(arg1)"></my-directive>
</div>
JS:
var app = angular.module('MyApp', []);
app.directive('myDirective', function() {
return {
restrict: 'E',
scope: { someCtrlFn: '&callbackFn' },
link: function(scope, element, attrs) {
scope.someCtrlFn({arg1: 22});
},
controller: function ($scope) {
$scope.ctrlFn = function(test) {
console.log(test);
}
}
}
});
When I remove the directive's controller from it and and create a new controller it works,
Method 2: (Works: Controller's function does write to the console)
HTML:
<div ng-app="MyApp" ng-controller="Ctrl">
<my-directive callback-fn="ctrlFn(arg1)"></my-directive>
</div>
JS:
var app = angular.module('MyApp', []);
app.directive('myDirective', function() {
return {
restrict: 'E',
scope: { someCtrlFn: '&callbackFn' },
link: function(scope, element, attrs) {
scope.someCtrlFn({arg1: 22});
}
}
});
app.controller('Ctrl', function ($scope) {
$scope.ctrlFn = function(test) {
console.log(test);
}
});
I would like know how to get the behavior of Method 2 in Method 1 i.e., to be able to call the directive's controller's function from directive's attribute.
Any help is greatly appreciated, Thank you!
In Method 1, you are creating an isolated scope and defining a scope value someCtrlFn that takes in a function from the parent scope that is using your directive. The function to use is specified by the attribute callbackFn.
The way directives work with these scope items is that they are expected to be assigned from things that are on the parent scope that is active when the directive is used. So, if you have a controller Ctrl as in your Method 2, then use the directive within that scope, your directive is trying to match the what you defined in the attribute to what is available on Ctrl's scope.
So, in your first example, it's looking for a function called ctrlFn on the parent scope, but there isn't one. It will not try to look for it on the directive's controller. This is why Method 2 works, because there is a parent scope where ctrlFn is defined, and the directive is able to properly invoke that expression.
The purpose of these scope attributes is to allow directives to bind to values or functions on a parent scope to facilitate communication. For example, to give the directive data that it will display or modify, or allow the parent to define a function the directive can invoke for a callback during an event or what have you. The parent scope cannot move into the directive's scope and force the directive's scope to use its own defined items (unless you set it up so your directive uses a default value or function if the attribute is omitted or whatever).
They are not used so a directive can define things on its scope that it uses internally. If these things are internal to the directive, you can simply add them to the scope during link or whatever is suitable.
Did you mean something like this?
var app = angular.module('MyApp', []);
app.directive('myDirective', function() {
return {
restrict: 'E',
scope: { },
link: function(scope, element, attrs) {
// defines ctrlFn that can be used later by this directive's template or controller
scope.ctrlFn = function(test) {
console.log(test);
}
// immediately invokes ctrlFn to log a message, just here to illustrate
scope.ctrlFn('Hello World!');
}
}
});
Achieved it using $rootScope instead of $scope within the directive's controller
HTML:
<div ng-app="MyApp">
<my-directive callback-fn="ctrlFn(arg1)"></my-directive>
</div>
JS:
<script>
var app = angular.module('MyApp', []);
app.directive('myDirective', function($rootScope) {
return {
restrict: 'E',
scope: { someCtrlFn: '&callbackFn' },
link: function(scope, element, attrs) {
scope.someCtrlFn({arg1: 22});
},
controller: function ($scope) {
$rootScope.ctrlFn = function(test) {
console.log(test);
}
}
}
});
</script>

Angularjs: Can transcluded elements "live" in the parent controller's scope?

I would like directives in the transcluded content to "live" in the directive's parent controller
ex: in the view below, I would like the binding "varInMyController" to
be bound to the variable of the same name in MyController.
I could explicitely pass variables in the directive scope:{aVar: '='},
but I want to avoid this, because the directive needs to be generic.
is this possible ?
<div ng-controller="MyController">
<div my-directive>
{{varInMyControllerScope}}
</div>
</div>
angular.module('myModule')
.directive('myDirective', [function() {
return {
transclude: true,
replace: true,
templateUrl: '<div><div ng-transclude></div></div>',
scope: {
options: '='
},
link: function(scope, element, attrs) {}
}
}])
angular.module('MyModule')
.controller('myController', ['$scope'function($scope) {
$scope.varInMyControllerScope = "hello"
}])
See Associated Plunker.
The most crucial problem of your above is this line in the directive
templateUrl: '<div><div ng-transclude></div></div>'
use template instead since it isn't a URL.
If this isn't a partial code, then another problem might be the initialization of your module. It should look like this from the start:
angular.module('myModule', [])
Another problem is the spelling typo of your second invocation for the angular.module() method:
angular.module('MyModule');
It should be
angular.module('myModule');
Lastly is the comma between the array of passed in the controller function is not separated by ,
['$scope'function($scope) {
$scope.varInMyControllerScope = "hello"
}]
It should be
['$scope', function($scope) {
$scope.varInMyControllerScope = "hello"
}]

Resources