I'm trying to implement a recursive directive and it seems like to get it to work nicely I need to define an isolate scope as well as have access to the parent scope. Basically I want my directive to have access to variables set as attributes on the directive itself, but i also want to be able to access variables and methods set in the controller's scope. Is there a way to combine the two? I've tried with transclude but i suppose i'm not entirely sure if i've used it properly. Here is a fiddle example of my problem, where i'd like each 'child' in the directive to be able to call the function sayHi(): http://jsfiddle.net/n8dPm/655/
You have to pass the sayHi function to your directive.
Directives create their own scope, So sayHi function is not part of your directive scope, the way to allow it is by creating a new prop an pass it.
HTML
<div ng-app="myapp">
<div ng-controller="TreeCtrl">
<tree family="treeFamily"
say-hi="sayHi(name)"
ngTransclude></tree>
</div>
</div>
JS
var module = angular.module('myapp', []);
module.controller("TreeCtrl", function($scope) {
$scope.treeFamily = {
name : "Parent",
children: [{
name : "Child1",
children: [{
name : "Grandchild1",
children: []
},{
name : "Grandchild2",
children: []
}]
}, {
name: "Child2",
children: []
}]
};
$scope.sayHi = function(name){
alert(name+' says hello!')
}
});
module.directive("tree", function($compile) {
return {
restrict: "E",
scope: {
family: '=',
sayHi : '&'
},
transclude: true,
template:
'<p>{{ family.name }}</p>'+
'<ul>' +
'<li ng-repeat="child in family.children">' +
'<tree family="child" say-hi="sayHi(name)"></tree>' +
'<button ng-click="sayHi({name : child.name})">Say Hi</button>' +
'</li>' +
'</ul>',
compile: function(tElement, tAttr) {
var contents = tElement.contents().remove();
var compiledContents;
return function(scope, iElement, iAttr) {
if(!compiledContents) {
compiledContents = $compile(contents);
}
compiledContents(scope, function(clone, scope) {
iElement.append(clone);
});
};
}
};
});
Related
In angularjs I have been trying to access main controller $scope variable in my directive isolated scope.
My html code,
<body ng-controller="MainCtrl">
<div id="TestContainer" class="TestContainer" ng-init=Intialfunc()>
<collection collection='testdata'>{{testdata}}</collection>
</div>
</body>
My directive code,
var app = angular.module('plunker', []);
app.directive('collection', function () {
return {
restrict: "E",
replace: true,
scope: {collection: '='},
//controller: 'TreeController',
//bindToController: true,
template: "<ul><member ng-repeat='member in collection' member='member'></member></ul>"
}
})
app.directive('member', function ($compile) {
var linkerfunc = function(scope, element, attrs) {
var collectionSt = '<collection collection="member.children"></collection>';
$compile(collectionSt)(scope, function(cloned, scope) {
element.append(cloned);
});
}
return {
restrict: "E",
replace: true,
scope: {member: '=', ShowDetailsCtrlFunc : '&'},
template: "<li><span ng-click=ShowDetailsCtrlFunc()>{{member.NodeName}}</span></li>",
controller: 'MainCtrl',
//controllerAs: 'MainCtrl',
//bindToController: true,
link: linkerfunc
}
})
My controller code,
app.controller('MainCtrl', function ($scope) {
$scope.Intialfunc = function() {
$scope.testdata = []
var myjsondata = JSON.parse('{ "NodeName": "Parent", "children": [ { "NodeName": "mychild", "children": [ { "NodeName": "chld1", "children": [] } ] } ] }');
$scope.testdata.push(myjsondata);
console.log($scope.testdata) //This one is showing
}
$scope.ShowDetailsCtrlFunc = function(element,event) {
console.log("in function ShowDetailsCtrlFunc"); // coming to this fucntion on click.
console.log($scope.testdata) // but this one is not showing . shows undefined.
//event.stopImmediatePropagation();
};
});
it is coming to the function but not showing the controller $scope. I have created a plunker ,
plunker
Please help me. I have been struggling for many days.
You need to add a function expression to both of your directives' isolate scopes in order to properly call a function in your parent scope. Taking your original code, it should look something like this:
var app = angular.module('plunker', []);
app.directive('collection', function () {
return {
restrict: "E",
//replace: true, <- this is deprecated and should no longer be used
scope: {
collection: '=',
onMemberClick: '&'
},
template: "<ul><member ng-repeat='member in collection' member='member' on-click='onMemberClick()'></member></ul>"
}
})
app.directive('member', function ($compile) {
return {
restrict: "E",
//replace: true, <- this is deprecated and should no longer be used
scope: {
member: '=',
onClick : '&'
},
template: "<li><span ng-click='onClick()'>{{member.NodeName}}</span></li>"
}
});
And you original html should look something like this:
<body ng-controller="MainCtrl">
<div id="TestContainer" class="TestContainer" ng-init=Intialfunc()>
<collection collection='testdata' on-member-click='ShowDetailsCtrlFunc ()'>{{testdata}}</collection>
</div>
</body>
Argument binding
If you would like to actually know which member was clicked, you'll need to bind arguments to your function calls.
var app = angular.module('plunker', []);
app.directive('collection', function () {
return {
restrict: "E",
scope: {
collection: '=',
onMemberClick: '&'
},
template: "<ul><member ng-repeat='member in collection' member='member' on-click='onMemberClick({member: member})'></member></ul>"
}
})
app.directive('member', function ($compile) {
return {
restrict: "E",
scope: {
member: '=',
onClick : '&'
},
template: "<li><span ng-click='onClick({member: member})'>{{member.NodeName}}</span></li>"
}
});
Html:
<body ng-controller="MainCtrl">
<div id="TestContainer" class="TestContainer" ng-init=Intialfunc()>
<collection collection='testdata' on-member-click='ShowDetailsCtrlFunc (member)'>{{testdata}}</collection>
</div>
</body>
MainCtrl:
app.controller('MainCtrl', function ($scope) {
$scope.Intialfunc = function() {
$scope.testdata = []
var myjsondata = JSON.parse('{ "NodeName": "Parent", "children": [ { "NodeName": "mychild", "children": [ { "NodeName": "chld1", "children": [] } ] } ] }');
$scope.testdata.push(myjsondata);
console.log($scope.testdata) //This one is showing
}
$scope.ShowDetailsCtrlFunc = function(member) {
console.log("In show details function");
console.log(member);
};
});
plunker
Lets Begin with the query you have. You want to call a function from link inside the directive even when the scope is isolated. It's simple you want to access parent scope.
Here's the code you can use to access parent scope.
scope.$parent.yourFun();
//or you can do this by the code give below.
//Inside Directive Use this.
scope:{
fun:"&"
},
//now you can call this function inside link
link:function(scope, element,attr){
scope.fun();
}
In your app.directive, just put scope : false.
Your directive will use the same scope as his parent scope.
In my AngularJs (v1.5.9) app, I have a kind of list view directive that itself depends on a directive to render the individual, complex items. What exactly should be rendered inside the list items is decided by the consumer and passed in via transclusion.
So the basic structure looks something like this:
<list-directive>
<list-item>
<some more stuff />
<transcluded content />
</list-item>
</list-directive>
I am now trying to add another directive to this structure, that takes data that can be passed in via an attribute from the outside into the top-level element and then does stuff depending on that input for each of the list-items.
The structure is somewhat complex and I tried to reduce the code snippet below to the bare minimum.
// controller
(function () {
'use strict';
function FcDataListCtrl($scope, $q, $element) {
var that = this;
initVars();
init();
function initVars() {
that.actionButtons = that.actionButtons || [];
}
function init() {
}
}
angular
.module('controls.fcDataList.controller', [])
.controller('fcDataListCtrl', FcDataListCtrl);
})();
(function () {
'use strict';
function FcDataList() {
return {
restrict: 'E',
transclude: true,
template: `<fc-data-list-item ng-repeat="item in ::fcDataList.items" item="::item">
<div ng-transclude></div>
</fc-data-list-item>`,
scope: {
items: '=?',
actionButtons: '=?'
},
controller: 'fcDataListCtrl',
controllerAs: 'fcDataList',
bindToController: true
};
}
angular
.module('controls.fcDataList', [
'controls.fcDataList.controller',
'controls.fcDataList.item'
])
.directive('fcDataList', FcDataList);
})();
(function () {
'use strict';
function FcDataListItem() {
return {
restrict: 'E',
replace: true,
require: '^fcDataList',
transclude: 'element',
template: `<div>
<div ng-transclude></div>
<fc-item-menu items="fcDataList.actionButtons"></fc-item-menu>
</div>`,
scope: {
item: '=?'
},
link: {
pre: FcDataListItemLink
}
};
function FcDataListItemLink(scope, elem, attrs, fcDataListCtrl) {
initVars();
init();
function initVars() {
}
function init() {
console.log('FcDataListItem')
console.dir(fcDataListCtrl.actionButtons);
}
}
}
angular
.module('controls.fcDataList.item', [
'components.fioControlsExtensions.fcDataList.menu'
])
.directive('fcDataListItem', FcDataListItem);
})();
(function () {
'use strict';
function FcItemMenu() {
return {
restrict: 'E',
template: `<div ng-repeat="item in items">
<div>{{ item.icon }}</div>
</div>`,
scope: {
items: '=?'
},
link: {
pre: FcItemMenuLink
}
};
function FcItemMenuLink(scope, elem, attrs) {
scope.open = open;
initVars();
init();
function initVars() {
console.log('MenuItem');
console.dir(scope.items);
}
function init() {
}
function open(event) {
}
}
}
angular
.module('components.fioControlsExtensions.fcDataList.menu', [])
.directive('fcItemMenu', FcItemMenu);
})();
(function () {
'use strict';
function AppCtrl() {
var that = this;
init();
function init() {
that.fcDataList = {
buttons: [
{ icon: 'ff-show' }
],
items: [
{ firstName: 'Ivan', lastName: 'Petrov', jobPosition: 'Zookeeper' },
{ firstName: 'Andrei', lastName: 'Müller', jobPosition: 'Pilot' },
{ firstName: 'Christian', lastName: 'Klein', jobPosition: 'Cook' },
{ firstName: 'Peter', lastName: 'Stoyanov', jobPosition: 'Fuller' },
{ firstName: 'Nadine', lastName: 'Wolf', jobPosition: 'Driving Instructor' },
{ firstName: 'Amya', lastName: 'Krüger', jobPosition: 'Military' }
],
}
}
}
angular
.module('controls.example', [
'controls.fcDataList'
])
.controller('AppCtrl', AppCtrl)
})();
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.8/angular.min.js"></script>
<html lang="en" ng-app="controls.example">
<body ng-controller="AppCtrl as app">
<b>Test for data list n stuff</b>
<fc-data-list items="app.fcDataList.items"
action-buttons="app.fcDataList.buttons">
<div class="row">
<span> This is a list item </span>
</div>
</fc-data-list>
</body>
</html>
here is alsow a Codepen with the same example: https://codepen.io/lyioth/pen/LbqWLz/
Please note that the transclusion of the actual item content is not shown here (but that is working without a problem, so I skipped it).
The actual problem is that, the items in fc-item-menu stay undefined. I added some log statements to show that at the levels above this component the array in question is not in fact empty.
If I change the directive to also require the controller and access the actionButtons property directly, it seems to work. But I'd rather not do that.
So the question is, why doesn't this work as expected? What am I missing?
The items at fc-item-menu are bound to fcDataList.actionButtons. That means that the fc-item-menu directive will look for them at scope.fcDataList.actionButtons.
Let's go one step back - in your fc-data-list directive you have the same type of binding - fcDataList.item. That works, because you have used bindToController and controllerAs, which puts the isolated scope in a scope field, with the name defined in controllerAs (scope.fcDataList).
Back to fc-item-menu: although you have require: ^fcDataList in the directive, the above expression will be undefined, because no scope.fcDataList exist. In addition that is true, because with the transclusion, an isolated scope has been created, which does not carry the scope.fcDataList. The way to make it work is define a scope property yourself with the values from the controller, like so:
function FcDataListItemLink(scope, elem, attrs, fcDataListCtrl) {
scope.buttons = fcDataListCtrl.actionButtons;
}
Here's a working plnkr: https://codepen.io/anon/pen/GNzELy
I created a select directive and am using this directive twice. I need to see the selected items of both. What should I do?
HTML
<div select-list="items"></div>
<div select-list="items2"></div>
Controller
var myApp = angular.module('myApp',[]);
myApp.controller('mainController', function($scope) {
$scope.items = [
{
name: "1"
},
{
name: "2"
}
];
$scope.items2 = [
{
name: "3"
},
{
name:"4"
}
];
$scope.selectedValues = [];
});
Select directive
myApp.directive("selectList", function() {
return {
restrict: "EACM",
template: '<select ng-model="selectedValues" ng-options="item.name for item in data"></select>',
scope: {
data: '=selectList'
}
}
});
I need to add selected items of both "selects" into $scope.selectedValues.
I tried through ng-change, but it didn't work.
Your directive use isolated scope, so you can't access from the controller to the directive or from the directive to the controller.
You have to create a new entry.
I let you a fiddle that is working :
https://jsfiddle.net/Lv1q2sh2/1/
// Code goes here
var myApp = angular.module('app', []);
angular.module('app')
.directive("selectList", function(){
return {
restrict: "EACM",
require: 'ngModel',
template: '<select ng-model="selected" ng-change="onSelectedValue()" ng-options="item.name for item in data"></select>',
scope: {
data: '=selectList'
},
link: function (scope, element, attr, ngModel) {
scope.onSelectedValue = function () {
ngModel.$setViewValue(scope.selected);
}
}
}
})
.controller('mainController', function($scope) {
$scope.items = [
{name: "1"},
{name: "2"}
];
$scope.items2 = [
{name:"3"},
{name:"4"}
];
$scope.selectedValues = [];
});
Directive needs to be created properly:
Have a controller for your directive
If you are using isolated scope, make sure to pass selectedValue to the scope.
ex:
Directive:
myApp.directive("selectList", function(){
return{
restrict: "EACM",
template: '<select ng-model="selectedValues" ng-options="item.name for item in data"></select>',
scope: {
data: '=selectList',
ngModel: '='
}
//Add link function here, crate watcher on ngModel and update it back on select dropdown selection.
})};
HTML:
<div select-list="items" ng-model="selectedValue1" ></div>
<div select-list="items2" ng-model="selectedValue2"></div>
Add link function to directive and put a watcher on ngModel, once user makes change in selection, update parent ng-model.
I've been looking at isolateScope directives, to get a better understanding of how they interact with other nested isolateScope directives, so put together a plnkr to test a few things out.
http://plnkr.co/edit/7Tl7GbWIovDSmVeKKN26?p=preview
This worked as expected. As you can see each directive has it's own separate template.
I then decided to move the html out of each directive and into the main html file, but it's stopped working? I can see that the e1Ctrl is on the scope of the directive, but it doesn't appear to be available when the enclosed markup is processed.
http://plnkr.co/edit/33Zz1oO4q7BVFw0cMvYa?p=preview
Can someone please tell me why this is happening?
----------- UPDATE -----------
I've simplified the non-working plunker to clearly show the problem. The directive uses the controllerAs syntax and the e1Ctrl is clearly set on its $scope (see the console output).
http://plnkr.co/edit/g2U2XskJDwWKuK3gqips?p=preview
angular
.module('app', [])
.controller('AppCtrl', AppCtrl)
.directive('elementOne', elementOne)
.controller('E1Ctrl', E1Ctrl)
function AppCtrl() {
var vm = this;
vm.data = [
{
label: 'one'
},
{
label: 'two'
},
{
label: 'three'
},
{
label: 'four'
}
];
vm.callback = function() {
console.log('called app callback');
};
}
function elementOne() {
return {
restrict: 'E',
scope: {
data: '=',
handler: '&'
},
controller: 'E1Ctrl',
controllerAs: 'e1Ctrl',
bindToController: true
}
}
function E1Ctrl($scope) {
console.log('E1Ctrl', $scope);
var vm = this;
vm.click = function() {
vm.handler();
};
vm.callback = function() {
console.log('called e1 callback');
};
}
Mark up:
<body ng-app="app" ng-controller="AppCtrl as appCtrl">
<ul>
<div ng-repeat='item in appCtrl.data'>
<element-one data='item' handler='appCtrl.callback()'>
<button ng-click='e1Ctrl.click()'>e1: {{e1Ctrl.data.label}}</button>
</element-one>
</div>
</ul>
</body>
------ Transclusion solution -----
http://plnkr.co/edit/l3YvnKOYoNANteNXqRrA?p=preview
function elementOne() {
return {
restrict: 'E',
transclude: true,
scope: {
data: '=',
handler: '&'
},
controller: 'E1Ctrl',
link: function($scope, $element, $attr, ctrl, transclude) {
transclude($scope, function(clone){
$element.append(clone);
});
}
}
}
There's a difference in scope for HTML in the template of the directive and HTML that in a subtree of the directive. The former is evaluated in the context of the scope of the directive; the latter - in the scope of the View.
If the directive has an isolate scope - scope: {}, then the subtree doesn't see it. If it uses scope: true, then it creates a new child scope for the subtree which prototypically inherits from the View's scope.
Consider the following:
// isolate scope
app.directive("foo", function(){
return {
scope: {},
link: function(scope){
scope.name = "foo";
}
}
});
// child scope
app.directive("bar", function(){
return {
scope: true,
link: function(scope){
scope.name = "bar";
}
}
});
app.controller("Main", function($scope){
$scope.name = "main";
});
Here's how the View would render:
<body ng-controller="MainCtrl">
<pre>in main: {{name}} will render "main"</pre>
<foo>
<pre>in subtree of foo: {{name}} will render "main"</pre>
</foo>
<bar>
<pre>in subtree of bar: {{name}} will render "bar"</pre>
</bar>
</body>
In your case, the subtree is evaluated in the scope of the View - not the directive, and that is why it doesn't work as you expected.
plunker
EDIT:
In some cases it may makes sense to evaluate the subtree in the context of the isolate scope of the directive. I've seen this used with directives that allow templating. But be careful with this because the author of the Main View should not know (too much) about the inner workings of the directive (i.e. what is exposed in the inner scope). This would also be difficult to read because you would see variables that do not make sense in the outer scope.
To evaluate the subtree in the isolate scope of the directive, the directive needs to $compile the subtree and link it against its scope.
Here's a directive that allows the user to provide a template for each item in the list. The item variable is not defined in the main scope, and only makes sense in the context of the directive's isolate scope:
<list src="items">
<item-template>
{{item.a}} | {{item.b}}
</item-template>
</list>
The directive 'list' is below:
app.directive("list", function($compile){
return {
scope: {
src: "="
},
link: {
pre: function(scope, element){
var itemTemplate = element.find("item-template");
element.empty();
var template = angular.element('<div ng-repeat="item in src"></div>')
.append(itemTemplate.html());
element.append(template);
$compile(element.contents())(scope);
}
}
}
});
plunker 2
Creating an angular tree directive with an isolated scope that binds the value of two attributes...items and item-click-handler. Here is the code:
HTML
<div ng-app="myapp">
<div ng-controller="TreeCtrl">
<item-tree class="tree" items="treeFamily" item-click-handler="groupSelectedFunction(itemId)"></item-tree>
</div>
JAVASCRIPT
var module = angular.module('myapp', []);
module.controller("TreeCtrl", function($scope) {
$scope.treeFamily = [{
Name : "Parent",
Code : "Parent",
Children: [{
Name : "Child1",
Code : "Child1",
Children: [{
Name : "Grandchild1",
Code : "Grandchild1",
Children: []
},{
Name : "Grandchild2",
Code : "Grandchild2",
Children: []
},{
Name : "Grandchild3",
Code : "Grandchild3",
Children: []
}]
}, {
Name: "Child2",
Code : "Child2",
Children: []
}]
}];
$scope.groupSelectedFunction = function (itemId) {
alert(itemId + ' selected');
}
});
module.directive("itemTree", function($compile) {
return {
restrict: "E",
scope: {
items: '=',
itemClickHandler: '&'
},
template:
'<ul id="group-nodes" ng-repeat="item in items">' +
'<li id={{ item.Code }}>' +
'<a ng-click="itemClickHandler({itemId: 1})">{{ item.Name }}</a>'+
'<item-tree items="item.Children" item-click-handler="itemClickHandler(itemId)"></item-tree>' +
'</li>' +
'</ul>',
compile: function(tElement, tAttr) {
var contents = tElement.contents().remove();
var compiledContents;
return function(scope, iElement, iAttr) {
if(!compiledContents) {
compiledContents = $compile(contents);
}
compiledContents(scope, function(clone, scope) {
iElement.append(clone);
});
};
}
};
});
The tree builds fine. If i click the top level parent node the controller function runs and shows an alert with the expected value: "Parent selected". However if any of the other nodes are clicked the the function runs but the alert message is: "undefined selected". Relatively new to Angular so this one has left me scratching my head. Would appreciate any help.
It's not clear what ID should be showing in the alert for each child node, but it is more obvious why you're seeing undefined instead of a value...
When using the & isolate scope type, the arguments passed to a function from a template need to be passed in an object, which you followed for the top level element:
ng-click="itemClickHandler({itemId: 1})"
You need to follow the same syntax for the children. So,
item-click-handler="itemClickHandler(itemId)"
should be
item-click-handler="itemClickHandler({itemId: itemId})"
Demo