Search code examples
angularjsangular-ui-bootstrapangular-uiangular-directive

Transclusion in Angular UI Modal not working


The objective of this plunk is to transclude elements into an Angular UI Modal from a controller, where the Modal is wrapped by a directive. The solution should follow these premises:

  • The directive declares the transclusion of fields. These fields are included in the directive declaration in the controller HTML markup.
  • These fields declared in the controller should show up in the Modal.
  • The scope of these fields should be accessible in the controller (see that I declared an input1 variable in the controller that should set a value in the Modal).
  • I defined a content element to transclude the fields. This element is in the modal's template. I'm not sure when this template is available to transclude it.

To summarize, the objective is to have a set of fields declared in the controller HTML markup and available in the modal, where the modal is wrapped in a directive and the scope is managed in the controller. Any ideas will be greatly appreciated.

HTML

<div the-modal control="modalCtl">
    <p>some text</p>
    <input type="text" ng-model="input1" />
</div>

<button type="button" ng-click="open()">Open me!</button>

Javascript

var app = angular.module("app", ['ui.bootstrap']);

app.controller("ctl", function($scope,$timeout) {

  $scope.modalCtl = {};

  $scope.input1 = "abc";

  $scope.open = function(){
    $scope.modalCtl.openModal();
  };

});


app.directive("theModal", function($uibModal) {
  return {
    restrict: "AE",        
    scope: {              
      control: "="
    },
    transclude: true,
    link: function (scope, element, attrs, ctrl, transclude) {
      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());
      };
    }
  }
});

Solution

  • You have come close enough to achieving your objective with transclusion but, there are a few things you need to consider:

    1. First of all, according to UI Bootstrap docs, there is an appendTo property in the options for the $uibModal.open() method which defaults to body.

      If appendTo is not specified, the modal will be appended to the body of your page and becomes a direct child of the body. Therefore querying .content in your directive via element.find('.content') won't work because it doesn't exist there.

    2. Secondly, AngularJS comes with jQLite, a lightweight version of jQuery. This implies that there is limited support for most of jQuery's functionalities. One such case is with the .find() method which only works with tag names.

      To make it work how it does with jQuery (although you don't really have to because you could still use .children() in a chain to query nested DOM elements), you'll have to load jQuery before Angular (which I suppose you have already).

      Refer AngularJS docs on angular.element for more info.

    3. Rendering DOM takes a little bit of time for Angular since it needs to make the correct bindings related to scopes and the views, to complete a digest cycle, and so on. Therefore you may end up instantly querying a DOM element which in fact might not have been rendered yet.

      The trick to wait for DOM rendering and completion of a digest cycle is to wrap your DOM related code into $timeout wrapper.

    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>',
            /**
            * Make sure the modal is appended to your directive and NOT `body`
            */
            appendTo: element  
        });
    
        
        /**
        * Give Angular some time to render your DOM
        */
        $timeout(function (){
            /**
            * In case jQuery is not available
            */
            // var content = element.children('.modal').children('.modal-dialog').children('.modal-content').children('.content');
            
            /**
            * 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.

    Demo