I think Iโ€™ve mentioned Iโ€™m stuck in an AngularJS (Angular 1) codebase for most of my work right now, which isnโ€™t great. The portion of the codebase where I do most of my work is pretty solid, but there are lots of places where it doesnโ€™t fully exploit what AngularJS had to offer.
 
I donโ€™t love learning all this stuff about AngularJS in 2021, but thatโ€™s the job. ๐Ÿ˜ One solution that I keep suggesting is transclusion, which is where you embed html over into a custom component just like you would any other html element's tags.
<my-custom-tag>
  <ul>
    <li>Item 1</li>
    <li>Item 2</li>
  </ul>
</my-custom-tag>
And each time Iโ€™ve promptly forgotten how to use it.
 
So as a note to self, hereโ€™s a [*coughcough*five-year old*coughcough*] video Iโ€™ve found that explains it fairly well:
 
 
Youโ€™re welcome for cuing the video up past the annoyingly loud โ€œWHAAAA!!!!โ€ at the beginning where heโ€™s trying to solidify his self-branding as The Net Ninja.

Well, it turns out there's a lot more to transclusion than I'd hoped.

Straight transclusion, where you want to drop html inside of your new custom tags just like they're standard html, and what I was describing before, is pretty much just as advertised.

But then there's the situation when you want to have more than one transclusion. Say your custom component has a header, footer, and body. Do you have to parse stuff or can you set up three separate transclusions in your markup?

Turns out it's the latter. (This is not covered in the video, above.)

So if I have a wacky component like this one:

<div>
    <div>Header: <span ng-transclude="header"></span></div>
    <div>
        <h2>Body</h2>
        <div ng-transclude="body"></div>
    </div>
    <div ng-transclude="footer"></div>
</div>

... and I map it up in my code like this...

.component('mySelect', {
    bind: {
        collection: '<',
    },
    transclude: {
        header: '?header', // note the `?` to keep each one optional
        body: '?body', // also note that you can change the names
        footer: '?footer', // between parent element and component template.
    },
    templateUrl: '/path/to/your/template.html',
});

... I can drop html into each by doing this...

<my-select>
    <header>head</header>
    <body>body</body>
    <footer>my foot</footer>
</my-select>

... and have them pop into the right places.

That's neat, I guess.


Transclusion and uibModal

Which brings us to my actual use case. What I wanted to do was create One Modal to Rule Them All based on the AngularJS version of bootstrap UI, the $uibModal. And I wanted a component where you could pass in via transclusion the contents for the modal.

So, you know, what I want is to take this conventional uibModal template:

<script type="text/ng-template" id="myModalContent.html">
    <div class="modal-header">
        <h3 class="modal-title" id="modal-title">I'm a modal!</h3>
    </div>
    <div class="modal-body" id="modal-body">
        <ul>
            <li ng-repeat="item in $ctrl.items">
                <a href="#" ng-click="$event.preventDefault(); $ctrl.selected.item = item">{{ item }}</a>
            </li>
        </ul>
        Selected: <b>{{ $ctrl.selected.item }}</b>
    </div>
    <div class="modal-footer">
        <button class="btn btn-primary" type="button" ng-click="$ctrl.ok()">OK</button>
        <button class="btn btn-warning" type="button" ng-click="$ctrl.cancel()">Cancel</button>
    </div>
</script>

... and replace it with something like this...

<script type="text/ng-template" id="myModalContent.html">
    <div class="modal-header" ng-transclude="header">
        <h3 class="modal-title" id="modal-title">I'm a modal!</h3>
    </div>
    <div class="modal-body" id="modal-body"  ng-transclude="body"></div>
    <div class="modal-footer" ng-transclude="footer">
        <button class="btn btn-primary" type="button" ng-click="$ctrl.ok()">OK</button>
        <button class="btn btn-warning" type="button" ng-click="$ctrl.cancel()">Cancel</button>
    </div>
</script>

One problem: Where do I put the html for each transclusion spot? That is, how do I insert the equivalent of this for my modal? I don't really want to put it into a new template each time. That defeats the purpose.

<my-select>
    <header>head</header>
    <body>body</body>
    <footer>my foot</footer>
</my-select>

You can't really insert into a template without the template. That is, uibModals aren't in their own component. You're already passing in the entire template, I believe (I'm not an AngularJS expert by any stretch, and I'm not really super psyched to be one where I don't have to, tbh).

There are at least two questions on StackOverflow that seem related.

The first one has some code with a method passed to link that I've seen mentioned elsewhere, but never explained well, including in the docs. And those are the only examples I've stumbled into so far, though I'm admittedly specifically looking for explanations of transclusion with uibModal, not link and transclude more generally.

app.directive("theModal", function($uibModal) {
  return {
    restrict: "AE",        
    scope: {              
      control: "="
    },
    transclude: true,
    link: function (scope, element, attrs, ctrl, transclude) {// <<<<<< This and...
      scope.control = scope.control || {}
      scope.control.openModal = function () {
        scope.instance = $uibModal.open({
          animation: false,
          scope: scope,
          template: '<div>in the template</div><div class="content"></div>'
        });
        element.find('.content').append(transclude()); // <<<<<< ... this look promising. (-mfn)
      };
    }
  }
});

So I'm still not real sure how to send over transcluded content, but that looks reasonably promising.

For now, I've wimped out and created a new modal template type that takes a payload with messages and arrays of list items that the specialized modal turns into a display.

For instance, here's the body that takes in messages payloads like [{ text: "Stuff before the list", listItems: [1,2,3] }, { text: "Jive after the list" }]...

<div class="modal-body">
    <span ng-repeat="message in messages">
        <p ng-if="message.text">{{message.text}}</p>
        <ul ng-if="isArray(message.listItems) && message.listItems.length">
            <li ng-repeat="listitem in message.listItems">{{listitem}}</li>
        </ul>
    </span>
</div>

Feels dumb though.

Labels: ,