Last week I talked a little about using transclusion in AngularJS from a starting primer on the topic to where I still wasn't quite sure how to use it to pass DOM into a uibModal.

Here are a few more transclude-related links I rediscovered as I was closing out browser tabs this morning. With any luck, I'll come back and process this jive into something that answers the question I punted on for the time being last week.


From What exactly do you do with the transclude function and the clone linking function?:

As @Jonah pointed out below,ย here is a really good article on the compile option of directives and using the transclusion function

The basic idea is the compile function should return a linking function. You can use the transclusion function provided inside the linking function to take a clone of the transcluded DOM element, compile it, and insert it wherever it needs to be inserted.

Here is a better example I've [pull outta the air] on Plunker

The idea of the compile function is it gives you a chance to programmatically alter the DOM elements based on attributes passed BEFORE the linking function is created and called.

// a silly directive to repeat the items of a dictionary object.
app.directive('keyValueRepeat', function ($compile){
  return {
    transclude: true,
    scope: {
      data: '=',
      showDebug: '@'
    },
    compile: function(elem, attrs, transclude) {

      if(attrs.showDebug) {                
        elem.append('<div class="debug">DEBUG ENABLED {{showDebug}}</div>');
      }

      return function(scope, lElem, lAttrs) {
        var items = [];
        console.log(lElem);
        scope.$watch('data', function(data) {

          // remove old values from the tracking array
          // (see below)
          for(var i = items.length; i-- > 0;) {
            items[i].element.remove();
            items[i].scope.$destroy();
            items.splice(i,1);
          }

          //add new ones
          for(var key in data) {

            var val = data[key],
                childScope = scope.$new(),
                childElement = angular.element('<div></div>');

            // for each item in our repeater, we're going to create it's
            // own scope and set the key and value properties on it.
            childScope.key = key;
            childScope.value = val;

            // do the transclusion.
            transclude(childScope, function(clone, innerScope) {
              //clone is a copy of the transcluded DOM element content.
              console.log(clone);

              // Because we're still inside the compile function of the directive,
              // we can alter the contents of each output item
              // based on an attribute passed.
              if(attrs.showDebug) {                
                clone.prepend('<span class="debug">{{key}}: {{value}}</span>');
              }

              //append the transcluded element.
              childElement.append($compile(clone)(innerScope));
            });

            // add the objects made to a tracking array.
            // so we can remove them later when we need to update.
            items.push({
              element: childElement,
              scope: childScope
            });

            lElem.append(childElement);
          }
        });
      };
    }
  };
});

So that's at least a detailed example of using the transclude function in a compile.


You should probably also know the difference between transclude: true and transclude: 'element'.


This one I need to read again to understand what's going on precisely, but seems interesting.

How to make transcluded template to bind to component scope:

You can programmatically transclude using the $transclude service and append the elements to the form like this:

$transclude($scope, function(clone) {
  $element.find('form').append(clone);
})

Then you add the elements to your form controller like this:

$scope.testForm.$addControl($element);

And then to follow back up on Transclusion in Angular UI Modal not working, here's a bit of the answer given:

Taking the above points into account, theย openModalย method in the link function of your custom directiveย theModalย should look like the following:

scope.control.openModal = function () {
    scope.instance = $uibModal.open({
        animation: false,
        scope: scope,
        template: '<div>in the template</div><div class="content"></div>',
        appendTo: element  
    });

    
    /**
    * Give Angular some time to render your DOM
    */
    $timeout(function (){
        /**
        * Since you have jQuery loaded already
        */
        var content = element.find('.content');
        
        /**
        * Finally, append the transcluded element to the correct position, 
        * while also making sure that the cloned DOM is bound to the parent scope (i.e. ctl) 
        */
        transclude(scope.$parent, function(clonedContent){
            content.append(clonedContent);
        });
    });
};

Note how theย transcludeย function gives you control over how you want to bind some transcluded DOM toย a custom scopeย and NOT the default directive's scope. The plainย transclude()ย call will take the current available scope object into account - i.e. the directive's scope - for binding the transcluded DOM.


So, again, I punted and created a reusable modal that expects a specific payload in scope that gets mapped to the body. Here's just the body of the modal to demonstrate (no comments in the live version, natch):

<div id="multiMessageModal" class="modal-body">
    <span ng-repeat="message in messages">
    
        <!-- If you got text, put it here -->
        <p ng-if="message.text">{{message.text}}</p>
    
        <!-- If you got list items that may be related to text (but can be their own thing), put them here -->
        <ul ng-if="isArray(message.listItems) && message.listItems.length">
            <li ng-repeat="listitem in message.listItems">{{listitem}}</li>
        </ul>
        
        <!-- And then note that we're repeating as often as you want
            so you can have as many texts or lists in a row by leaving
            the other part falsy with each `message` -->    
    </span>
</div>

Makes sense, right? So if you need text, list items, then text, you have two messages, one with text and listItems and the next with just text. You could do three different unordered lists in a row with text before and after, or just three paragraphs of text with [{ text: 'p1' }, { text: 'p2' }, { text: 'p3' }], whatever.

Again, not nearly as good as being able to pass any template at all in a call to the modal, but it's doing the job for now. I still feel a little dirty from the hack, though.

Labels: ,