kiznore
kiznore

Reputation: 159

Create a container directive in angularjs

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

Answers (3)

Ilan Frumer
Ilan Frumer

Reputation: 32357

Custom repeater...

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.


Solution

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

create another custom directive instead of 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")
      })
    }
  }
});

create your directive with ng-repeating my-transclude and a controller:

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: '='
    }
  }
});

why?

  • Now you have total control of the transclusion phase inside my-transclude
  • you can define on the controller anything you need to share into the transcluded content.
  • You are not getting your hands dirty with repeating stuff.

Upvotes: 1

Alborz
Alborz

Reputation: 6903

It's not clear for me what exactly you do but i fix some mistake in your directive as follows.

  1. Clear the elem children before recreating them.
  2. 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

Pauli Price
Pauli Price

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

Related Questions