Scope Isolation & nested directives - angularjs

Dealing with '&' and isolated scope.
Is it possible to pass a value up through a parent directive? I want to pass id from the textdisp directive to the controller.
HTML:
<body ng-controller="MainCtrl">
<builder removequest="deleteQuestion(id)"></builder>
</body>
ANGULAR:
app.controller('MainCtrl', function($scope) {
$scope.deleteQuestion = function(id) {
alert(id);
}
});
app.directive('builder', function() {
return {
restrict: 'E',
scope: {
removequest: '&'
},
template: '<div>Hello how are you? <textdisp removequest=removequest(id)></textdisp></div>'
}
});
app.directive('textdisp', function() {
return {
restrict: 'E',
scope: {
removequest: '&'
},
template: '<div ng-click="remove()">Click here!</div>',
link: function (scope, el) {
scope.remove = function(id) {
console.log('workin')
scope.removequest(1);
}
}
}
});

I believe there are 2 things going on with your code:
When you're placing removequest="removequest(id)" that is calling the function, and not just referring to the function.
I believe that the &attr binding isn't returning the function that you're expecting.
Try this Plunker; it essentially uses { removequest: '=' } for bi-directional binding, and removequest="deleteQuestion" / removequest="removequest" for function references rather than calling the function.

It's a little confusing, but you can use object parameter when you need to pass values into your function invoked via & binding. Take a look at this code it will make everything clear:
app.controller('MainCtrl', function($scope) {
$scope.deleteQuestion = function(id) {
alert(id);
}
});
app.directive('builder', function() {
return {
restrict: 'E',
scope: {
removequest: '&'
},
template: '<div>Hello how are you? <textdisp removequest="removequest({id: id})"></textdisp></div>'
}
});
app.directive('textdisp', function() {
return {
restrict: 'E',
scope: {
removequest: '&'
},
template: '<div ng-click="remove()">Click here!</div>',
link: function(scope, el) {
scope.remove = function(id) {
scope.removequest({id: 34534}); // <-- 1.
}
}
}
});
Demo: http://plnkr.co/edit/3OEy39UQlS4EyOu5cq4y?p=preview
Note how you specify scope.removequest({id: 34534}) parameter to be passed into <textdisp removequest="removequest({id: id})">.

Related

Returning variable from directive instead of exposing the scope

In the code below I have a directive that calculates a variable y every time an input field x is changed. The variable y is exposed, so it's available to the declaring controller/directive. This works fine but it's a simple abstraction, in my real scenario the computation of y is very expensive, so I cannot afford to calculate y every time x changes. Ideally, I would calculate y only when the declaring controller/directive needs it. Is there a way to achieve that?
var app = angular.module('app', []);
app.controller('ctl', function () {
});
app.directive("theDirective", function() {
return {
restrict: "AE",
scope: {
y: '='
},
template: '<input ng-model="x" ng-change="xChanged()" />',
link: function (scope, element, attrs) {
scope.xChanged = function() {
scope.y = scope.x * 2;
};
}
}
});
If you need this data from a child of this directive you can accomplish this by exposing a method in your directives controller and then exposing a method that the child directive can require.
app.directive("theDirective", function() {
return {
restrict: "AE",
scope: {
y: '='
},
template: '<input ng-model="x" ng-change="xChanged()" />',
controller: function (scope) {
scope.getY = function() {
return scope.x * 2;
};
}
}
});
And then your chid can require the parent can call that method.
app.directive("theDirectiveChild", function() {
return {
restrict: "A",
require: ["^theDirective"],
link: function(scope, element, attrs, ctrls){
var theDirective = ctrls[0];
var y = theDirective.getY();
}
}
});
EDIT: To do the opposite, where you want the parent to tell the child to update, you can utilize $scope.broadcast() This can fire a message down the scope chain, it would look something like this.
app.directive("theDirective", function() {
return {
restrict: "AE",
scope: {
y: '='
},
template: '<input ng-model="x" ng-change="xChanged()" />',
link: function (scope) {
scope.on('update-the-directive' , function() {
scope.y = scope.x * 2;
});
}
}
});
And then your chid can require the parent can call that method.
app.directive("theDirectiveParent", function() {
return {
restrict: "A",
link: function(scope, element){
scope.click = function() {
scope.$broadcast('update-the-directive');
}
}
}
});

two-way data binding inside attribute directive that requires element directive

I have an element directive (e-dir) and an attribute directive (a-dir) on the same element:
<e-dir a-dir attr="msg"></e-dir>
I pass msg into e-dir's isolate scope via the attr attribute:
app.directive('eDir', function eDir($timeout) {
return {
restrict: 'E',
scope: {
attr: '='
}
};
});
In this way, msg is bound (two-ways) with $scope.attr (in EDirCtrl) or scope.attr (in e-dir's link function).
Is there a simple way I can achieve the same two-way data-binding inside a-dir's directive? Or would you recommend another, simpler approach?
The closest thing I've been able to come up with is to set eDirCtrl.attr = $scope.attr; inside e-dir's controller (EDirCtrl):
app.directive('eDir', function eDir($timeout) {
return {
restrict: 'E',
scope: {
attr: '='
},
controller: function EDirCtrl($scope) {
var eDirCtrl = this;
eDirCtrl.attr = $scope.attr;
},
controllerAs: 'eDirCtrl'
};
});
Then, have a-dir require e-dir, and access attr via e-dir's controller (eDirCtrl.attr):
app.directive('aDir', function aDir($timeout) {
return {
restrict: 'A',
require: 'eDir',
link: linkFn
};
function linkFn(scope, element, attrs, eDirCtrl) {
eDirCtrl.attr = 'eDirCtrl.attr';
}
});
But, it's not bound two-ways. As you can see this code snippet:
var app = angular.module('app', []);
app.controller('Ctrl', function Ctrl($scope) {
$scope.msg = 'initial message';
})
app.directive('eDir', function eDir($timeout) {
return {
restrict: 'E',
scope: {
attr: '='
},
template: '<div>$scope.attr: {{attr}}</div>'+
'<div>eDirCtrl.attr: {{eDirCtrl.attr}}</div>',
controller: function EDirCtrl($scope) {
var eDirCtrl = this;
eDirCtrl.attr = $scope.attr;
$timeout(function() {
$scope.attr = 'changing $scope.attr also changes msg';
}, 2000);
},
controllerAs: 'eDirCtrl'
};
});
app.directive('aDir', function aDir($timeout) {
return {
restrict: 'A',
require: 'eDir',
link: linkFn
};
function linkFn(scope, element, attrs, eDirCtrl) {
$timeout(function() {
eDirCtrl.attr = 'changing eDirCtrl.attr does not effect $scope.attr or msg';
}, 4000);
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="Ctrl">
msg: <input type="text" ng-model="msg"><br>
<e-dir a-dir attr="msg"></e-dir>
</div>
The reason the two-way binding isn't working is that attr is being bound to a string rather than an object. In JavaScript, primitives (booleans, numbers, strings) are immutable, so when you change one, the previous instance is discarded and a new one is used. This breaks Angular's two-way binding and any changes to scope.msg are not propagated through attr into the directive.
You can get this to work as expected by setting msg on an object e.g. scope.test.msg and binding attr to test (the object) rather than msg (a string).
I've updated your code snippet to do this:
var app = angular.module('app', []);
app.controller('Ctrl', function Ctrl($scope) {
$scope.test = {msg : 'initial message'};
})
app.directive('eDir', function eDir($timeout) {
return {
restrict: 'E',
scope: {
attr: '='
},
template: '<div>$scope.attr: {{attr.msg}}</div>'+
'<div>eDirCtrl.attr: {{eDirCtrl.attr.msg}}</div>',
controller: function EDirCtrl($scope) {
var eDirCtrl = this;
eDirCtrl.attr = $scope.attr;
$timeout(function() {
$scope.attr.msg = 'changing $scope.attr also changes msg';
}, 2000);
},
controllerAs: 'eDirCtrl'
};
});
app.directive('aDir', function aDir($timeout) {
return {
restrict: 'A',
require: 'eDir',
link: linkFn
};
function linkFn(scope, element, attrs, eDirCtrl) {
$timeout(function() {
eDirCtrl.attr.msg = 'changing eDirCtrl.attr does not effect $scope.attr or msg';
}, 4000);
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="Ctrl">
msg: <input type="text" ng-model="test.msg"><br>
<e-dir a-dir attr="test"></e-dir>
</div>

Dynamic controller

I have two nested directive and a few controllers and I want inject controller to second controller.
When I bind action to some button it work but list don't show up, some one know why?
Dynamic Controller directive
.directive("dynamicController", ["$compile", function($compile) {
return {
restrict: "A",
scope: {
dynamicController: "#"
},
compile: function(tElement, tAttrs) {
return {
pre: function preLink(scope, iElement, iAttrs, controller) {
iElement.attr("ng-controller", scope.dynamicController);
iElement.removeAttr("dynamic-controller");
$compile(iElement)(scope);
}
}
}
}
}])
V1: http://codepen.io/anon/pen/LVeaWo
V2: http://codepen.io/anon/pen/EjoJVx
[ EDIT ]
I almost do it but it's one more problem.
I have two directive:
.directive("wrapDirective", function() {
return {
restrict: "A",
template: "<div dynamic-controller=\"Ctr1\">" +
"<button ng-click='action()'>Click</button>" +
"<ul>" +
"<li ng-repeat=\"item in list\">{{item}}</li>" +
"</ul>" +
"</div>",
scope: {
controller: "#wrapDirective"
}
}
})
and
.directive("dynamicController", function($compile) {
return {
restrict: "A",
scope: true,
controller: "#",
name: "dynamicController"
}
})
The problem is this line <div dynamic-controller=\"Ctr1\"> in warpDirective
I can't do something like this <div dynamic-controller=\"{{controller}}\">
CodePen with both cases: http://codepen.io/anon/pen/EjoJXV
You should use require and link to get the controllers of parent directives.
See Creating Directives that Communicate.
.directive('myDirective', function() {
return {
require: '^ngController', // <-- define parent directive
restrict: 'E',
scope: {
title: '#'
},
link: function(scope, element, attrs, ctrl) { // <-- get the controller via the link function
ctrl.doSomething();
}
};
The reason behind your code is not working is, {{}} interpolation value is not evaluated in you pre link function. So by compiling ng-controller with not value in it is throwing an error. You should use iAttrs.$observe as you are evaluating expression inside {{}}.
Code
var dynamicControllerObserver = iAttrs.$observe('dynamicController', function(newVal, oldVal) {
wrapElement.attr("ng-controller", scope.dynamicController);
wrapElement.append(iElement.html());
console.log(wrapElement)
iElement.html("");
console.log(iElement)
iElement.append(wrapElement);
$compile(wrapElement)(scope);
dynamicControllerObserver(); //destruct observe
})
Working Codepen
I did it, really helpful was this post: Dynamic NG-Controller Name
I modified it to my needs:
.directive('dynamicCtrl', ['$compile', '$parse', function($compile, $parse) {
return {
restrict: 'A',
terminal: true,
scope: {
dynamicCtrl: "#"
},
link: function(scope, elem, attr) {
var initContent = elem.html();
var varName = getName(elem.attr('dynamic-ctrl'));
update();
scope.$watch("dynamicCtrl", function() {
update();
})
function update() {
var wrapper = angular.element("<div></div>");
wrapper.append(initContent);
var name = $parse(varName)(scope.$parent);
wrapper.attr('ng-controller', name);
elem.empty();
elem.append(wrapper);
$compile(wrapper)(scope);
}
function getName(attr) {
var startIndex = attr.lastIndexOf("{") + 1,
endIndex = attr.indexOf("}");
return attr.substring(startIndex, endIndex);
}
}
};
}])
http://codepen.io/anon/pen/xGYyqr

How to access wrapped variables in a directive?

I have an object which consists of multiple arrays:
$scope.myArrays = {
array1: ['Pizza', 'Spaghetti'],
array2: ['Lasagne', 'Schnitzel']
};
Moreover, I have a custom directive to which I want to pass this object myArrays and bind those arrays to scope variables:
<my-directive my-data="myArrays"></my-directive>
myApp.directive('myDirective', function() {
return {
restrict: 'E',
scope: {
arrayOne: '=myData.array1',
arrayTwo: '=myData.array2'
},
link: function(scope, elem) {
// get access to scope.array1 and scope.array2
}
};
});
All together in a fiddle for you to play around!
Is there a way to bind the arrays directly or do I need to bind arrays: '=myArrays' and access them like arrays.array1?
Binding has to be one to one, you cannot do that. Yes, you will have to access the arrays inside your directive.
myApp.directive('myDirective', function() {
return {
restrict: 'E',
scope: {
myData: '='
},
link: function(scope, elem) {
scope.arrayOne = scope.myData.array1;
scope.arrayTwo = scope.myData.array2;
}
};
});
You can directly access scope.myData.array1 and scope.myDate.array2 inside the directive template if you dont have to process these arrays in the link function and get rid of it.
You could do this by just assigning them to scope fields:
myApp.directive('myDirective', function() {
return {
restrict: 'E',
scope: {
myData: '='
},
link: function(scope, elem) {
scope.arrayOne = myData.array1;
scope.arrayTwo = myData.array2;
}
};
});
However, I would recommeend to just bind the object and to get rid of the link-function. That way the directives code is a lot shorter, more readable, less "black magic" happens and the references inside the directive are more expressive:
myApp.directive('myDirective', function() {
return {
restrict: 'E',
scope: {
myData: '='
}
};
});
Then you can just reference it inside the directive with myData.array1. Replace the '=' with '=someName' to reference it with someName.array1
your HTML should be :
<my-directive arrayone="myData.array1" arraytwo="myData.array2"></my-directive>
and your directive :
myApp.directive('myDirective', function() {
return {
restrict: 'E',
scope: {
arrayOne: '=arrayone',
arrayTwo: '=arraytwo'
},
link:function(scope){
console.log('arrayOne :',scope.arrayOne);
console.log('arrayTwo :',scope.arrayTwo);
},
controller: function($scope) {
console.log('arrayOne :',$scope.arrayOne);
console.log('arrayTwo :',$scope.arrayTwo);
}
};
});
Demo
HTML-Partial:
<div ng-controller="MyCtrl">
<my-directive my-data="myArrays" array-one="myArrays.array1" array-two="myArrays.array2">
</my-directive>
</div>
Script:
angular.module('myApp', [])
.directive('myDirective', function() {
return {
restrict: 'E',
scope: {
arrayOne: '=',
arrayTwo: '='
},
link: function(scope, elem) {
// get access to scope.array1 and scope.array2
//console.log(scope.array1)
console.log(scope.arrayOne);
console.log(scope.arrayTwo);
}
};
})
.controller('MyCtrl', function($scope) {
$scope.myArrays = {
array1: ['Pizza', 'Spaghetti'],
array2: ['Lasagne', 'Schnitzel']
};
});

AngularJS : Change parent scope value from custom directive

For some reason I can't make this work based on the other examples I've seen here on SO.
Here's my directive:
(function () {
angular.module('materialDesign')
.directive('aSwitch', directive);
function directive() {
return {
templateUrl: 'elements/material/switch/switch.html',
transclude: false, // I've tried true here
restrict: 'E',
scope: {
enabled: '=',
toggleState: '=',
},
link: function(scope, element) {
element.on('click touchstart', function() {
scope.toggleState = !scope.toggleState;
});
}
};
}
})();
And the controller scope value that I want to change when toggling the switch/checkbox:
$scope.hideInactive = true;
The html:
<a-switch toggle-state="hideInactive"></a-switch>
and further down in my html page, I have this:
<div ng-show="!hideInactive">
<!-- stuff -->
</div>
EDIT:
This version is "working now", but as soon as I click my switch/checkbox a second time, the element.on fires twice, this flipping my scope value back to what it was.....basically, it's not letting me "un-check" my toggle.
angular.module('material')
.directive('aSwitch', [
'$timeout', function($timeout) {
return {
templateUrl: 'elements/material/switch/switch.html',
transclude: false,
restrict: 'E',
scope: {
enabled: '=',
toggleState: '=',
},
link: function (scope, element) {
element.on('click touchstart', function () {
$timeout(function () {
scope.toggleState.state = !scope.toggleState.state;
scope.$apply();
});
});
}
};
}
]);
EDIT and FINAL SOLUTION:
Here's the updated directive link property that fixed everything. I'd like to add that Oleg Yudovich's answer was also used (passing an object as the property instead of a true/false by itself)
link: function (scope, element) {
element.on('click touchstart', function (event) {
if (event.srcElement && event.srcElement.id && event.srcElement.id === "switch") {
event.stopPropagation();
$timeout(function() {
scope.toggleState.state = !scope.toggleState.state;
});
}
});
}
Try to pass object instead of primitive variable like this:
$scope.hideInactive = {
state: false;
}
html without changes:
<a-switch toggle-state="hideInactive"></a-switch>
in your directive:
scope.toggleState.state = !scope.toggleState.state;
Reed this awesome article: https://github.com/angular/angular.js/wiki/Understanding-Scopes
You need to run digest cycle after changes in scope, because changing scope binding from event will not run angular digest cycle, you need to run it manually by doing scope.$apply()
Directive
(function () {
angular.module('materialDesign')
.directive('aSwitch', directive);
function directive($timeout) {
return {
templateUrl: 'elements/material/switch/switch.html',
transclude: false, // I've tried true here
restrict: 'E',
scope: {
enabled: '=',
toggleState: '=',
},
link: function(scope, element) {
element.on('click touchstart', function() {
$timeout(function(){
scope.toggleState = !scope.toggleState;
});
});
}
};
}
})();
Try below code:
angular.module('material').directive('aSwitch', ['$timeout', function($timeout) {
return {
templateUrl: 'elements/material/switch/switch.html',
transclude: false,
restrict: 'E',
scope: {
enabled: '=',
toggleState: '=',
},
link: function(scope, element) {
element.on('click touchstart', function() {
$timeout(function() {
scope.toggleState.state = !scope.toggleState.state;
scope.$apply();
});
});
}
};
}]);

Resources