Transcluding in Ancient Times: AngularJS

I attempted transcluding in multiple slots before multiple slot transclusion was invented in AngularJS. Weapon of choice: AngularJS 1.3.4.

My use case was to create a customizable table that could be reused by changing the table columns and headers. The goal was to keep some functionality and styling throughout the different uses but only change the columns and headers displayed.

Available Approaches

  • Use column data as a property passed to the directive.
  • Multiple transclusions – I liked this approach, but AngularJS does not support slotted transclusions until version 1.5, I am using 1.3.4.
  • Create an entirely new custom table directive with Divs and CSS it into looking like a table with display: table., etc.

Column Data

In this case, it works fine for simple columns with primitive data, but not with complex columns with a lot of HTML and bindings in each cell. Plus, it would fill up the controller with a bunch of large objects and HTML.

Custom Table

Creating my own custom table would be a substantial amount of time and work. I believe this approach would be the most prone to errors, not only programmatically, but also visually. It would also be less readable than the other options. I would have to make sure I use the correct CSS and maintain it in such a way that it looks and behaves like a table. I usually opt to “go with the flow” when it comes to code. Whenever I find myself adding workarounds, I know I must have made a wrong turn somewhere and need to head back to the drawing board.

For the courageous out there, these are the HTML template and the CSS stylings you would need to get you started:

<div class="table">
  <div class="tr">
    <div class="td">Row 1, Cell 1</div>
    <div class="td">Row 1, Cell 2</div>
    <div class="td">Row 1, Cell 3</div>
  </div>
  <div class="tr">
    <div class="td">Row 2, Cell 1</div>
    <div class="td">Row 2, Cell 2</div>
    <div class="td">Row 2, Cell 3</div>
  </div>
</div>
div.table {border: 1px solid black; display: table; }
div.tr {border: 1px solid black; display: table-row; }
div.td {border: 1px solid black; display: table-cell; }

Multiple Transclusions

Creating a custom directive that handles multiple transclusions seemed like the obvious choice, but it was not straightforward or documented in a way that made it easy.

There were a bunch of cases to consider. For one, the transcluded elements might have transcluded content as well as directives. The transcluded content had to be compiled the right way.

Enter the transclude function

The transclude function (fifth parameter in the link function in a directive, fourth in the controller of a directive) returns the compiled transcluded data. Inside this function, you can also append the cloned element. You can also specify what scope to be used when compiling the elements.

const transcludedContent = transclude(scope, function(clone) {});

Then, you can choose what to do with the compiled data. In most cases, you would either append it or replace the directive’s element HTML with it. In this case, the element is the directive’s jqLite element, which has some functionality from JQuery.

element.replaceWith(transcludedContent);

Now it was a matter of selecting what part of the element to replace with the transcluded and compiled data. I chose the “name” attribute to tag the parts of the transcluded data:

const selector = `[name=${attrs.relvinMultipleTransclude}]`;

An Example

Let’s look at an example. Transcluding multiple elements in different parts inside the my-directive directive. Inside the transcluded data the parts tagged(name=”headers” and name=”columns”) would look something like (using Jade as an HTML processor):

        my-directive
            table
              tr(name="headers")
                th Name
                th     
              tr(name="columns")
                td
                  p {{item.name}}
                td
                  a(href="" ng-click="goToDetails()") Go to details    

On a side note: When transcluding parts of a table this way, you need to wrap them inside a table element, or else the browser will remove them (td, th, etc).

Inside the my-directive‘s template, the HTML slots for each transclusion would look like:

.my-directive
  table
    thead
      tr
        th(relvin-multiple-transclude="headers")       
    tbody
      tr(ng-repeat="item in items")
        td(relvin-multiple-transclude="columns") 

And finally, the relvinMultipleTransclude directive would look like:

angular.module('relvinAmazingApp')
  .directive('relvinMultipleTransclude', function () {
    return {
      controller: function($scope, $element, $attrs, $transclude) {
        if(!$transclude){
          throw {
            name: 'DirectiveError',
            message: 'relvin multiple transclude found without parent requesting transclusion'
          };
        }
      },
      link: function(scope, element, attrs, controller, transclude){
        const selector = `[name=${attrs.relvinMultipleTransclude}]`;
        //Get the entire compiled transcluded content
        const transcludedContent = transclude(scope, function(clone) {});
        //Replace the part needed for this directive in the correct slot
        element.replaceWith(transcludedContent.find(selector).contents());
      }
    };
  });

Conclusion

I found that the best way was to use multiple slot transclusion. In my case, I had to use an older version of AngularJS so I had to do implement a version fo this myself, but this should be readily available from AngularJS 1.5 by adding this to the directive definition:

transclude: {a:<nameA>, b:<nameB>}

To make our custom implementation work, we need three things:

  • transclude: true – in the directive containing the multiple transcludes
  • name = “{transcludeSlotName}” – in the transcluded content
  • relvin-multiple-transclude=”{transcludeSlotName}”- in the template where you want to transclude the elements. The transcludeSlotName must be the same as in step 2

I hope all this helps you and saves you hours of research and trial and error.

Note: This worked for my use case, you might have to tweak it to satisfy your needs.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.