I have an infinitely nested data structure, where there is a top level object that has a collection of objects, and each of these objects can also have a collection of objects.
I need to iterate through this tree, which I am currently doing like so:
collection.js
app.directive('collection', function() {
return {
restrict: 'E',
replace: true,
scope: {
collection: '='
},
templateUrl: 'collection.html'
};
});
collection.html
<ul>
<member ng-repeat="member in collection" member="member"></member>
</ul>
member.js
app.directive('member', function($compile) {
return {
restrict: 'E',
replace: true,
scope: {
member: '='
},
templateUrl: 'member.html',
link: function(scope, element, attrs) {
var collection = '<collection collection="member.children"></collection>';
if (scope.member.children) {
$compile(collection)(scope, function(cloned, scope) {
element.append(cloned);
});
}
}
};
});
member.html
<li>
{{ member }}
</li>
index.html
<div data-ng-controller="collectionController">
<collection collection="collection"></collection>
</div>
I need to be able to click on a member, no matter how nested it is, and set the controller's selectedMember property as that member.
so something like this:
app.controller('collectionController', function($scope, collection) {
collection.getCollection().then(function(collection) {
$scope.collection = collection;
});
$scope.selectMember = function(member) {
$scope.selectedMember = member;
};
});
Since I'm calling a function defined in the parent scope (the controller), I think I need to pass down the selectMember function like this:
index.html
...
<collection collection="collection" select-member="selectMember"></collection>
...
collection.html
<member ng-repeat="member in collection" member="member"
select-member="selectMember()" ng-click="selectMember(member)">
</member>
collection.js
...
scope: {
collection: '=',
selectMember: '&selectMember'
}
...
member.js
...
scope: {
member: '=',
selectMember: '='
}
...
I just can't seem to get the function to trigger correctly and set the controller scope's selectedMember property. The parameter passed to the selectMember function is undefined.
I think it's obvious that I'm misunderstanding something about scopes, but the nested nature of the problem I have to solve isn't making things easier.
Any ideas?
Edit:
Heres a plunker: http://plnkr.co/edit/JfxpoLLgpADs9RXSMife
Yes, I think the approach you're taking is correct - i.e. passing the click handler from the outer scope. There is just some minor confusion with how to pass the handler. I wish you created a plunker, but I'll try to go blind. :)
index.html
<collection collection="collection" select-member="selectMember(member)"></collection>
collection.html template
<member ng-repeat="item in collection"
member="item"
select-member="selectMember({member: member})"></member>
collection.js
...
scope: {
collection: '=',
selectMember: '&'
}
...
member.html template
<li ng-click="selectMember({member: member})>{{ member }}</li>
In addition, when you add <collection> for member.children:
<collection collection="member.children"
select-member="selectMember({member: member})"></collection>
member.js
...
scope: {
member: '=',
selectMember: '&'
}
...
EDIT:
Ok, this wasn't trivial :) But it was fun.
A few modifications:
select-member shouldn't just "pass a function" as I incorrectly suggested.
ng-click wasn't properly firing when it was declared on a member - it was firing for both child and parent. I moved it to member.html template.
For clarity, I used item with ng-repeat: ng-repeat="item in collection"
I'm correcting the above code. I also created a fork of you plunker.
Related
I have a directive treeview which contains a nested directive (being the branches) of each item rendered.
In the scope of both directives I have declared two parameters that should be talking to the parent controller.
filter: '&' //binds the method filter within the nested directive (branch) to the method doSomething() in the tree directive attribute which is bound to the html directive that binds to the controller.
iobj: '=' is the two way binding paramter that should be passing the scoped object to the controller. (but currently isn't)
Directive:
app.directive('tree', function () {
return {
restrict: 'E',
replace: true,
scope: {
t: '=src',
filter: '&',
iobj: '='
},
controller: 'treeController',
template: '<ul><branch ng-repeat="c in t.children" iobj="object" src="c" filter="doSomething()"></branch></ul>'
};
});
app.directive('branch', function($compile) {
return {
restrict: 'E',
replace: true,
scope: {
b: '=src',
filter: '&',
iobj: '='
},
template: '<li><input type="checkbox" ng-click="innerCall()" ng-hide="visible" /><a>{{ b.name }}</a></li>',
link: function (scope, element, attrs) {
var has_children = angular.isArray(scope.b.children);
scope.visible = has_children;
if (has_children) {
element.append('<tree src="b"></tree>');
$compile(element.contents())(scope);
}
element.on('click', function(event) {
event.stopPropagation();
if (has_children) {
element.toggleClass('collapsed');
}
});
scope.innerCall = function () {
scope.iobj = scope.b;
console.log(scope.iobj);
scope.filter();
}
}
};
});
HTML:
<div ng-controller="treeController">
<tree src="myList" iobj="object" filter="doSomething()"></tree>
<a ng-click="clicked()"> link</a>
</div>
Controller:
app.controller("treeController", ['$scope', function($scope) {
var vm = this;
$scope.object = {};
$scope.doSomething = function () {
var item = $scope.object;
//alert('call from directive');
console.log(item);
}
$scope.clicked = function () {
alert('clicked');
}
...
Currently I can invoke the function $scope.doSomething from the directive to the controller. So I know that I have access to the controllers scope from the directive. What I cannot figure out is how to pass an object as a parameter from the directive back to the controller. When I run this code, $scope.object
is always an empty object.
I'd appreciate any help or suggestions on how to go about this.
The & directive binding supports parameter passing. Given your example
scope.filter({message: 'Hello', anotherMessage: 'Good'})
The message and anotherMessage become local variables in the expression bound to directive:
<tree src="myList" iobj="object" filter="doSomething(anotherMessage, message)"></tree>
Here's a sample plunker where the callback parameters are set inside a template.
The documentation clearly states that:
Often it's desirable to pass data from the isolated scope via an
expression to the parent scope, this can be done by passing a map of
local variable names and values into the expression wrapper fn. For
example, if the expression is increment(amount) then we can specify
the amount value by calling the localFn as localFn({amount: 22}).
In one of my Angular.JS controllers, I have the following:
app.controller("MyController", ["$scope", function($scope){
$scope.messages = [
new Message(1),
new Message(2)
];
$scope.addMessage = function(x) { $scope.messages.push(new Message(x)); }
}]);
Then in my main HTML page, I have
<message message="message" ng-repeat="message in messages">
This is bound to a directive:
app.directive("message", function() {
return {
restrict: "E",
scope: {
message: "="
},
templateUrl: "js/Directives/message.html"
};
});
The template file is:
<li class="message">{{message.msg}} </li>
However, when I call addMessage on the controller, while it does add to $scope.messsages, it doesn't actually refresh the ng-repeat and display the new message. How can I do this?
I would suggest some structural changes in your directive.
First of all, why not refer the original array itself instead of referring value at each iteration ??
<message messages="messages">
Then you can actually move ng-repeat part in your directive template, [You must note that since you're using = in message: "=", = binds a local/directive scope property to a parent scope property. So with =, you use the parent model/scope property name as the value of the DOM attribute. ].
Hence your directive will look like :
app.directive("message", function() {
return {
restrict: "E",
scope: {
messages: "="
},
templateUrl: "js/Directives/message.html"
};
});
and the subsequent template will look something like this :
<ul>
<li ng-repeat="message in messages" class="message">{{message.msg}} </li>
</ul>
You can find a demo plunker here
I am having trouble getting this directive to work properly, It was working fine until I tried adding a scope to link a function. Here's what I have so far
The directive (in requirejs format hence the module/component name stuff) :
angular.module(metadata.moduleName, [
filterFieldControl.moduleName
]).directive(metadata.componentName,
function(scope) {
return {
controller: filterFieldControl.componentName,
restrict: 'C',
replace: true,
scope: {
filterFn: '='
},
template: builderResultFiltersHTML
};
}
);
The controller
angular.module(metadata.moduleName, []).controller(metadata.componentName, [
'$scope',
function($scope) {
$scope.filterChange = function() {
$scope.filterFn();
};
}
]);
The template:
<div>
<ul>{{filter.name}}
<li ng-repeat="value in filter.values">
<input type="checkbox" ng-model="filterObject[filter.name][value]" ng-change="filterChange()">{{value}}
</li>
</ul>
Where it is being used (and maybe some of the issue is it's being repeated?)
<div ng-repeat="result in searchResults.results" class="builder-search-result" filter-fn="filterClicked" ></div>
So if I take out the scope from the directive it works fine. If i add it in, there are no errors in console but nohting shows up.
If I remove this, it works :
scope: {
filterFn: '='
},
Can't seem to figure out why. Could use some ideas.
Edit: added fiddle here https://jsfiddle.net/vt1uasw7/31/ - you will notice if you remove the scope part of the directive, everything shows up fine.
When you do scope: { ... } in a directive, you're telling Angular to create an isolate scope, which is a scope that doesn't prototypally inherit from any other scope.
Now, take a look at your directive's template:
<div>in
<ul>{{filter.name}}
<li ng-repeat="value in filter.values">
<input type="checkbox" ng-change="filterChange()">{{value}}
</li>
</ul>
</div>
Both filter and filterChange() are defined in the controller's scope, and your directive can't access them because it has its own private scope.
You can fix your code by doing one of two things:
Use $parent.filter and $parent.filterChange();
Add filter and filterChange to the directive's isolate scope:
scope: {
filter: '=',
filterChange: '&'
}
I suggest #2. Here's your updated fiddle.
Here's the explanation:
I have the current controller that creates an array of $scope.plan.steps which will be used to store every step:
.controller('PlanCtrl', function ($scope, $http) {
$scope.plan = {
steps: [{}]
};
$scope.addStep = function () {
$scope.tutorial.steps.push({});
}
}
Then I have the following directive which has an isolated scope and that is associated to the index of the $scope.plan.steps array:
.directive('planStep', function () {
return {
template: '<input type="text" ng-model="step.name" />{{step}}',
restrict: 'E',
scope: {
index: '=index'
},
transclude: true,
controller: function($scope, $element, $transclude) {
$scope.removeStep = function() {
$scope.$emit('removeStep', $scope.index);
$element.remove();
$scope.$destroy();
}
}
};
});
These two communicate, create, and delete objects inside of the controller's scope, however, how can I allow the directive to update the controller's scope array in real time?
I've tried doing a $watch on the directive's isolated scope changes, $emit the changes to the controller, and specify the $index... But no luck.
I've created a plunker to reproduce what I currently have: Link
So far I can create and delete objects inside of the array, but I cannot get a single object to update the controller's object based on the $index.
If the explanation was not clear, by all means, let me know and I will elaborate.
Thank you
WHen you do things like this inside ng-repeat you can take advantage of the child scope that ng-repeat creates and work without isolated scope.
Here's the same directive without needing any angular events
.directive('planStep', function() {
return {
template: '<button ng-click="removeStep(step)">Delete step</button><br><input type="text" ng-model="step.name" />{{step}}<br><br>',
restrict: 'E',
transclude: true,
controller: function($scope, $element, $transclude) {
var steps = $scope.plan.steps// in scope from main controller
/* can do the splicing here if we want*/
$scope.removeStep = function(step) {
var idx =steps.indexOf(step)
steps.splice(idx, 1);
}
}
};
});
Also note that removing the element with element.remove() is redundant since it will automatically be removed by angular when array gets spliced
As for the update, it will update the item in real time
DEMO
The way you set up 2-way binding for index you could set one up for step as well? And you really do not need index to remove the item, eventhough your directive is isolated it relies on the index from ng-repeat which probably is not a good idea.
<plan-step ng-repeat="step in plan.steps" index="$index" step="step"></plan-step>
and in your directive:
scope: {
index: '=index',
step:'='
},
Demo
Removing $index dependency and redundant element remove() and scope destroy (when the item is removed from the array angular will manage it by itself):
return {
template: '<button ng-click="removeStep()">Delete step</button><br><input type="text" ng-model="step.name" />{{step}}<br><br>',
restrict: 'E',
scope: {
step:'='
},
transclude: true,
controller: function($scope, $element, $transclude) {
$scope.removeStep = function() {
$scope.$emit('removeStep', $scope.step);
}
}
and in your controller:
$scope.$on('removeStep', function(event, data) {
var steps = $scope.plan.steps;
steps.splice(steps.indexOf(data), 1);
});
Demo
If you want to get rid of $emit you could even expose an api with the isolated scoped directive with function binding (&).
return {
template: '<button ng-click="onDelete({step:step})">Delete step</button><br><input type="text" ng-model="step.name" />{{step}}<br><br>',
restrict: 'E',
scope: {
step:'=',
onDelete:'&' //Set up function binding
},
transclude: true
};
and register it on the view:
<plan-step ng-repeat="step in plan.steps" step="step" on-delete="removeStep(step)"></plan-step>
Demo
Given the following directive:
angular.module('news.directives', [])
.directive('newsArticle', function($location, $timeout) {
return {
restrict: 'AE',
replace: 'true',
templateUrl: 'partials/pages/news/directives/article.html',
scope: true
};
});
And the following template:
<div id="story-{{item.id}}" ng-class="{'red': item.active, 'story-container': true}">
<div class="story-banner-image"></div>
<div class="story stationary">{{ item.title | words: 10 }}</div>
<div class="story-banner-content"></div>
</div>
And the following call to the directive:
<news-article ng-repeat="item in news">
</news-article>
This works. But if I want to use an isolated scope and expose a single item:
scope: {
item: '#'
}
// or
scope: {
news: '#'
}
// or
scope: {}
Then it doesn't. All of the {{item.property}} tags specified in the template return a null value (empty string). Why doesn't item exist in the isolated scope?
It's quite clearly inheriting it's parent properties when scope is set to true, but it's not inheriting when I tell it what it should inherit.
You problem is that you are confused about the way scope configuration is set. In order to setup two-way data binding with isolated scope you should provide corresponding attribute in HTML:
<news-article ng-repeat="item in news" item="item"></news-article>
and then setup directive accordingly:
scope: {
item: '='
}
Demo: http://plnkr.co/edit/b1I8PIc27MvjVeQaCDON?p=preview