4

I'm attempting to recreate a jQuery plugin that I've created within AngularJS as a directive. I'm having a bit of an issue in regards to transclusion.

jQuery Widget plugin: http://plnkr.co/edit/xxZIb2DyAere7pBY6qm7?p=preview

AngularJS Directive: http://plnkr.co/edit/N6f5H8oZkpNy5jbVQPgj?p=preview

-

I have an array of users like:

[
    { name: 'intellix' }, { name: 'and' }, { name: 'konoro' }, { name: 'kingdom' }, { name: 'are' }, { name: 'awesome' },{ name: 'really!' }
]

And my jQuery widget chunks data so they're in 3 rows and slides them... the data gets transformed into chunks and put into their own containers like:

[
    [
        { name: 'intellix' }, { name: 'and' }, { name: 'konoro' }
    ], 
    [
        { name: 'kingdom' }, { name: 'are' }, { name: 'awesome' }
    ],
    [
        { name: 'really!' }
    ]
]

As a designer or anyone using this widget, they shouldn't have to chunk it themselves, that's what the widget/directive is supposed to be for and you should be able to have your own HTML... something like:

<flicker delay="1000">
    <flicker-row ng-repeat="user in users">
        <p>User: {{user.name}}</p>
    </flicker-row>
</flicker>

The result I would want is:

<flicker delay="1000">
    <div class="container">
        <flicker-row>
            <p>User: intellix</p>
        </flicker-row>
        <flicker-row>
            <p>User: and</p>
        </flicker-row>
        <flicker-row>
            <p>User: konoro</p>
        </flicker-row>
    </div>
    <div class="container">
        <flicker-row>
            <p>User: kingdom</p>
        </flicker-row>
        <flicker-row>
            <p>User: are</p>
        </flicker-row>
        <flicker-row>
            <p>User: awesome</p>
        </flicker-row>
    </div>
    <div class="container">
        <flicker-row>
            <p>User: really</p>
        </flicker-row>
    </div>
</flicker>

But ngTransclude just takes the whole HTML it loops through and places it inside the flicker directive's template. I want to create 3 chunks in that directive, and then loop through those chunks, printing the HTML into those 3 containers.

How can I have transclude for creating a scope, but not have it just dump the whole result into the template?

I've attempted to chunk the data beforehand in my controller and have 2 controllers... but in my flicker-row directive the items haven't been looped through yet so I can't process them

<flicker delay="1000">
    <flicker-row ng-repeat="users in userChunks">
        <div class="item" ng-repeat="user in users">
            <p>{{user.name}}</p>
        </div>
    </flicker-row>
</flicker>
creamcheese
  • 2,524
  • 3
  • 29
  • 55
  • This article contains an example that requires Transclude to place elements in two places of his HTML, which I guess is what I'm after. So I can define a compile function, specifying what to do with my HTML http://blog.omkarpatil.com/2012/11/transclude-in-angularjs.html – creamcheese Jan 17 '14 at 07:41
  • as far as the chunking goes, what about something like `ng-repeat="user in users:chunk:3"` -- and create a chunk filter? – Matt Greer Jan 19 '14 at 19:56
  • This is what I ended up doing. Although I'm using the filter in my controller and just looping through everything in the view. As per my attempted example above. Created this flicker in fact: https://github.com/intellix/angular-flicker – creamcheese Jan 21 '14 at 22:03
  • I added 1 more demo if you need to use a syntax like ng-repeat: `repeat="user in users"` for flicker – Khanh TO Jan 24 '14 at 04:50

2 Answers2

3

Check my solution with custom Transclusion.

DEMO

JS:

.directive('flicker', function() {
  return {
    restrict: 'E',
    scope: {
      collection:"=",
      item:"@"
    },
    transclude: true,
    replace:true, //I don't want redundant tag after compilation
    template: '<div></div>',//simple template for demonstration.

    compile: function (element, attr, linker) {//the linker parameter is for custom translusion

        return function (scope, element, attr) {
          scope.$watchCollection("collection", function(collection){

            var children = element.children();

            //check if there are already elements, if so remove its scope
            for (i = 0; i < children.length; i++){           
              children.eq(i).children().eq(0).scope().$destroy();
            };

            element.html("");//clear old content

            var chunks = collection.chunk(3);//hardcode 3 for demonstration, we could pass this to the directive's scope by exposing 1 more property in the scope.

            for (i = 0; i < chunks.length; i++) {
              var div = angular.element("<div class='container'>");
              element.append(div);

              for (j=0;j<chunks[i].length;j++){
            // create a new scope for every element in the collection.
                var childScope = scope.$new();
                // pass the current element of the collection into that scope
                childScope[scope.item] = chunks[i][j];

                  linker(childScope, function(clone){
                    // clone the transcluded element, passing in the new scope.
                       div.append(clone); // add to DOM
                  });
                }
            }
          });

        };
     }
  };
})

HTML:

<flicker item="user" collection="users" >
    <flicker-row>
        <p>U: {{user.name}}</p>
    </flicker-row>
</flicker>

The flicker directive takes 2 parameters:

  • collection: the collection used to render the children.
  • item: a string to indicate property name for binding. In this case, I use user.

IMO, scope management in this case should be the responsibility of the <flicker> because flicker decides how to chunk the data. <flicker>'s inner html is just the template to be generated dynamically.

If you need to use a syntax like ng-repeat. Try this:

DEMO

HTML:

<flicker repeat="user in users" >
      <flicker-row>
        <p>U: {{user.name}}</p>
      </flicker-row>
    </flicker>

JS:

.directive('flicker', function() {
  return {
    restrict: 'E',

    transclude: true,
    replace:true,
    template: '<div></div>',

    compile: function (element, attr, linker) {
        return function (scope, element, attr) {
           var match = attr.repeat.match(/^\s*(.+)\s+in\s+(.*?)\s*$/), //parse the syntax string
            itemString = match[1],
            collectionString = match[2];

          scope.$watchCollection(collectionString, function(collection){

            var children = element.children();

            //check if there are already elements, if so remove its scope
            for (i = 0; i < children.length; i++){           
              children.eq(i).children().eq(0).scope().$destroy();
            };

            element.html("");//clear old content

            var chunks = collection.chunk(3);

            for (i = 0; i < chunks.length; i++) {
              var div = angular.element("<div class='container'>");
              element.append(div);

              for (j=0;j<chunks[i].length;j++){
            // create a new scope for every element in the collection.
                var childScope = scope.$new();
                // pass the current element of the collection into that scope
                childScope[itemString] = chunks[i][j];

                  linker(childScope, function(clone){
                    // clone the transcluded element, passing in the new scope.
                       div.append(clone); // add to DOM
                  });
                }
            }
          });

        };
     }
  };
})
Khanh TO
  • 48,509
  • 13
  • 99
  • 115
1

I have just tried not to invent the wheel again so i use flicker.js and jquery in my directive. The result is an easy to use directive. Hope to be usefull. DEMO PAGE

In the view

 <flicker users="users"></flicker>

flicker-template:

<section class="flicker">
    <article ng-repeat="users in chunks" class="row">
        <div class="innerSlider">
            <div class="item" ng-repeat="user in users">
                <p>U: {{user.name}}</p>
            </div>
        </div>
    </article>
</section>

script:

 Array.prototype.chunk = function (chunkSize) {
        var array = this;
        return [].concat.apply([],
          array.map(function (elem, i) {
              return i % chunkSize ? [] : [array.slice(i, i + chunkSize)];
          })
        );
    };

    angular.module('flicker', [])
    .controller('FlickerCtrl', function ($scope) {
        $scope.users = [
          { name: 'intellix' }, { name: 'are' }, { name: 'konoro' }, { name: 'kingdom' }, { name: 'are' }, { name: 'awesome' }, { name: 'really!' }
        ];
    })
    .directive('flicker', function ($timeout) {
        return {
            restrict: 'E',
            replace: false,
            scope: {
                users: '='
            },
            templateUrl: 'flicker-template.html',
            link: function (scope, element, attrs) {
                scope.chunks = scope.users.chunk(3);

                $timeout(function () { $('.flicker', element).flicker({}); });

            }
        };
    });
Alborz
  • 6,843
  • 3
  • 22
  • 37
  • 1
    I'm trying to keep away from jQuery as I feel that Angular should be able to stand up on it's own two feet. If I can't do something without jQuery then I'm doing it wrong :D I managed it in the end, created this directive: https://github.com/intellix/angular-flicker I am chunking via a filter in my controller so didn't get to do what I originally intended but when I find out how... will be cool – creamcheese Jan 21 '14 at 22:05