Reputation: 159
So I'm trying to create a directive that will layout a collection of items in columns. In the plunker I have a extremely simplified version which only uses a single ul, but that is not important. I want the directive to be called like .
<my-column-layout collection="names">
<tab name="{{ item }}"></tab>
</my-column-layout>
I want to use the inner html (the tab here) as a template for each item in the collection. I tried to just have a ng-repeat in the my-column-layout template like
template : '<ul><li ng-repeat="item in collection" ng-transclude></li></ul>
which worked but it didnt have access to the containing controllers scope, so I couldn't have any click events on the tab and have it call an function in the controller. So i think I am heading the right direction with using transclude but not sure. Also when I try to add some other name to the collection of names, then this doesnt show up in the collection in my directive. My scope.$watch('collection' ...) is never called.
http://plnkr.co/edit/4vyZDAhBcbULEd3uIznh?p=preview
Hope someone can help
Upvotes: 2
Views: 4761
Reputation: 32357
Building a custom repeater is a complicated task. Mostly because of performance issues but also because it should play well with other directives.
If you really need to build one, You must first dive into ngRepeat
source code to understand some considerations and then to mutate it to your own needs.
ngRepeat now uses $watchCollection (since 1.2) which replaced the deep $watch hog.
So my recommendation is do not build a custom repeater , use ngRepeat
!
I still don't know what you want to achieve but this is the construction.
Here is a plunker: http://plnkr.co/edit/pziqRzz0i1mU6eG5lAmd?p=preview
ngTransclude
app.directive('myTransclude',function(){
return {
require: "^myColumnLayout",
link: function(scope,elm,attr,ctrl,$transclude){
$transclude(function(clone){
elm.empty();
elm.append(clone);
ctrl.do("Hi")
})
}
}
});
app.directive('myColumnLayout', function() {
return {
restrict: 'EA',
transclude: true,
controller: function(){
this.do = function(x) {
console.log(x)
}
},
template: '<ul><li ng-repeat="item in collection track by $index" my-transclude></li></ul>',
scope: {
collection: '='
}
}
});
my-transclude
Upvotes: 1
Reputation: 6903
It's not clear for me what exactly you do but i fix some mistake in your directive as follows.
Note the third parameter of $watch function. It is necessary for watching a collection.
app.directive('myColumnLayout', function () {
return {
restrict: 'EA',
transclude: true,
scope: {
collection: '='
},
link: function (scope, elem, attrs, container, transclude) {
scope.$watch('collection', function (newVal, oldVal) {
elem.empty();
for (var i = 0; i < newVal.length; i++) {
var li = angular.element('<li></li>');
var scp = scope.$parent.$new();
scp.item = newVal[i];
transclude(scp, function (clone) {
elem.append(clone);
});
}
}, true);
}
}
});
Upvotes: 0
Reputation: 4237
I do something that I think is similar. Let me know if I've somehow missed the point. I have a directive that does a transcluded ng-repeat based on remote data. Here's how it works.
Update
It's the template in the page markup that's the issue. However, if you want the ng-repeat template to exist on the same page markup, you can do this:
<script type="text/ng-template" id="navbar.html">
<li ng-repeat="item in items" ng-class="{active: item.selected}">
<a href="/{{item.link}}">{{item.title}}</a>
</li>
</script>
Not exactly the same thing, but it get's you the same effect - template on the same page as the directive - just not nested with it.
Update End
I have the same array in the parent and the child scopes: i.e. $scope.items
. Because it's a reference type, through prototypical inheritance, both scopes reference the same object. In the location that doesn't update the property, I initialize it like this $scope.items = $scope.items || [];
-- i.e. if the property hasn't been initialized, initialize it, otherwise keep it.
directive('navbar', ['$location', '$http', function ($location, $http) {
return {
restrict: 'E',
transclude: true,
scope: { heading: '@'},
controller: 'NavbarCtrl',
templateUrl: 'navbar.html',
replace: true,
link: function ($scope, $element, $attrs, navbarCtrl) {
$scope.items = [];
$scope.heading = $scope.heading || $attrs.heading;
$http.get(itemsUrl).success(function(data) {
$scope.items = ... async get of data ... ;
navbarCtrl.selectByUrl($location.absUrl());
});
$scope.$watch('$location.absUrl()', function (locationPath) {
navbarCtrl.selectByUrl(locationPath)
});
}
}
}])
The directive's $watch calls a controller function, and that function has access to the controller $scope through its closure.
function NavbarCtrl($scope, $timeout, $http, $location, $attrs) {
$scope.items = $scope.items || [];
this.select = $scope.select = function (item) {
angular.forEach($scope.items, function (item) {
item.selected = false;
});
item.selected = true;
};
this.selectByUrl = function (url) {
angular.forEach($scope.items, function (item) {
if ('http://' + item.link === url) {
$scope.select(item);
}
});
};
}
Then, in my template, which I transclude, I have:
<li ng-repeat="item in items" ng-class="{active: item.selected}">
<a href="/{{item.link}}">{{item.title}}</a>
</li>
In the page markup, I use it like this:
<div ng-controller="NavbarCtrl">
<navbar heading="Navbar Heading"/>
</div>
Upvotes: 1