AngularJS : transcluding multiple sub elements in a single Angular directive

22,170

Solution 1

Starting Angular 1.5, it's now possible to create multiple slots. Instead of transclude:true, you provide an object with the mappings of each slot:

https://docs.angularjs.org/api/ng/directive/ngTransclude

angular.module('multiSlotTranscludeExample', [])
 .directive('pane', function(){
    return {
      restrict: 'E',
      transclude: {
        'title': '?pane-title',
        'body': 'pane-body',
        'footer': '?pane-footer'
      },
      template: '<div style="border: 1px solid black;">' +
                  '<div class="title" ng-transclude="title">Fallback Title</div>' +
                  '<div ng-transclude="body"></div>' +
                  '<div class="footer" ng-transclude="footer">Fallback Footer</div>' +
                '</div>'
    };
})

Solution 2

Cool question. I'm not sure there is a built in way, but you can do it yourself in a pretty generic way.

You can access the transcluded element by passing in the $transclude service like this:

$transclude(function(clone, $scope) {

Where clone will be a copy of the pre-linked transcluded content. Then if you label the content in the element like this:

    <div id="content">
        <div id="content0">{{text}}</div>
        <div id="content1">{{title}}</div>
    </div>

You can loop over the content and compile it like this:

$scope.transcludes.push($compile(clone[1].children[i])($scope));

Great! now you just need to put the content in the correct place in your template

     '<div id="transclude0"></div>' +
     '<div id="transclude1"></div>' +

Then you can in your link function assign the content correctly

angular.element(document.querySelector('#transclude' + i)).append(scope.transcludes[i]);

I set up a fiddle you can play with that has this set up.

Hope this helped!

Solution 3

In our project we have modeled multi site trasclusion after JSF 2's ui:composition, ui:insert, ui:define (see ui:composition).

Implementation consists of three simple directives: ui-template, ui-insert, ui-define (see angularjs-api/template/ui-lib.js).

To define a template one writes the following markup (see angularjs-api/template/my-page.html):

<table ui-template>
  <tr>
    <td ui-insert="menu"></td>
  </tr>
  <tr>
    <td ui-insert="content"></td>
  </tr>
</table>

and declares a directive (see angularjs-api/template/my-page.js):

  var myPage = 
  {
    templateUrl: "my-page.html",
    transclude: true
  };

  angular.module("app").
    directive("myPage", function() { return myPage; });

and finally, to instantiate the directive one needs to write (see angularjs-api/template/sample.html):

<my-page>
  <div ui-define="content">
    My content
  </div>
  <div ui-define="menu">
    <a href="#file">File</a>
    <a href="#edit">Edit</a>
    <a href="#view">View</a>
  </div>
</my-page>

The working sample can be seen through rawgit: sample.html

See also: Multisite Transclusion in AngularJS

Share:
22,170
epeleg
Author by

epeleg

Trying to reach out to me? use "epeleg" on gmail.com.

Updated on February 19, 2021

Comments

  • epeleg
    epeleg over 3 years

    I was reading about ng-transclude in the AngularJS docs on Creating a Directive that Wraps Other Elements and I think I understand properly what it does.

    If you have a directive that applies to an element that has content inside it such as the following:

    <my-directive>directive content</my-directive>
    

    it will allow you to tag an element within the directive's template with ng-transclude and the content included in the element would be rendered inside the tagged element.

    So if the template for myDirective is

    <div>before</div>
    <div ng-transclude></div>
    <div>after</div>
    

    it would render as

    <div>before</div>
    <div ng-transclude>directive content</div>
    <div>after</div>
    

    My question is if it is possible to somehow pass more then a single block of html into my directive?

    For example, suppose the directive usage would look like this:

    <my-multipart-directive>
         <part1>content1</part1>
         <part2>content2</part2>
    </my-multipart-directive>
    

    and have a template like:

    <div>
      this: <div ng-transclude="part2"></div>
       was after that: <div ng-transclude="part1"></div>
       but now they are switched
    <div>
    

    I want it to render as follows:

    <div>
      this: <div ng-transclude="part2">content2</div>
       was after that: <div ng-transclude="part1">content1</div>
       but now they are switched
    <div>
    

    Perhaps I could somehow bind the HTML value of a node to the model so that I will be able to use it in such a way without calling it "transclude"?

  • epeleg
    epeleg over 10 years
    thanks. this looks very close to what I was thinking of. I am not sure I understand what the $transclude service actually does but I guess I can read about it in the docs. why did you choose to have "div#content" as a wrapper of "div#content0" and "div#content1" and what would change if content was not there to wrap them? also what exactly is in the clone parameter? (what is clone[0] if clone[1] is div#content?)
  • epeleg
    epeleg over 10 years
    well... I went to the docs and did not find any reference to $transclude ... where did you find it and how to use it?
  • hassassin
    hassassin over 10 years
    You can see it in the transclude directive github.com/angular/angular.js/blob/… It is defined here: github.com/angular/angular.js/blob/… and described here: github.com/angular/angular.js/blob/…
  • epeleg
    epeleg over 10 years
    I would have expected it to show somewhere within docs.angularjs.org/api ... are you sure its o.k. to use? (being "undocumented") ?
  • hassassin
    hassassin over 10 years
    Well ultimately you need evaluate your situation. Is it possible to render what you want with normal transclusion? If you really have this complicated case, then it seems fine to use this. In some versions of angular you can get the transclude function as an argument to $compile. Either seem valid since normal transclusion would not be sufficient.
  • epeleg
    epeleg over 10 years
    thanks for all the help and information. (at least for now) it is not an actual need, just something I was wandering about during my learning phase... maybe it would be even possible to create a directive that will wrap this up nicely... something like: multiTransclude="subjectTemplate,bodyTemplate" ...
  • Zach Snow
    Zach Snow almost 10 years
    I ended up needing similar functionality and wrote ng-multi-transclude. It allows you to do almost exactly what you requested: in the template you define named "holes" via ng-multi-transclude, and at include time you pass in multiple elements whose name attributes match the holes you'd like them to fill.
  • Jerry
    Jerry over 8 years
    For anyone curious where this functionality is documented, search for $transclude in the $compile docs
  • epeleg
    epeleg over 8 years
    nice, They use my above suggested syntax almost as is. too bad that they did not update the docs for the transclude docs.angularjs.org/api/ng/service/$compile. I am also not sure I understand the naming convention of the the transclude object properties and their relation to the HTML elements of the directive's template and those in the HTML that uses the directive.
  • epeleg
    epeleg over 8 years
    it looks like the object is a mapping between the normalized transcluded elements tag names (i.e <pane-title> normalized to paneTitle) and the value of ng-transclude attribute of the directives template element .
  • epeleg
    epeleg over 8 years
    IMHO this is counter intuitive and I would have expected it to be the other way around in a similar fashion to how it works with scope:{} i.e. that the resolved value for the template would be on the left and value would give the instructions on where to get it from.
  • epeleg
    epeleg over 8 years
    i.e. I think the example would have been much clearer if it was transclude: { 'title': '?paneTitle': , 'body':'paneBody' } meaning that title for the template is optionaly resolved from the pane-title element. Anybody from the angular team is reading this ?!
  • epeleg
    epeleg over 8 years
    so I opened an issue for this (IMHO) required change of syntax here: github.com/angular/angular.js/issues/13439
  • epeleg
    epeleg over 8 years
    please note that following the above issue the keys/values in the transclude object have been swapped. so it should now be transclude: { 'title':'?pane-title', 'body':'pane-body' }, I still asked for another change which is to use 'paneTitle' and 'paneBody' which are again more consistent with the scope object syntax. I hope they will accept this request as well.
  • John
    John about 7 years
    in Angular 1.6.4 they are using the syntax: paneTitle instead of pane-title.