Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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());
      };
    }
  }
});
like image 837
ps0604 Avatar asked Sep 27 '16 21:09

ps0604


2 Answers

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

like image 171
Ahmad Baktash Hayeri Avatar answered Sep 23 '22 06:09

Ahmad Baktash Hayeri


As the previous answers suggest, you can use the appendTo property to provide the element of your directive as the parent of the modal. You can "wait for the modal's template to be rendered" by using the rendered promise in the UibModalIstance. (Documentation).

scope.control.openModal = function () {
    scope.instance = $uibModal.open({
        animation: false,
        scope: scope,
        template: '<div>in the template</div><div class="content"></div>',
        appendTo: element  
    });
    // We use the redered promise to make sure
    // the modal's template has been loaded.
    scope.instance.rendered.then(function (){
        // You'll most likely want to pass the `$parent` scope as the first
        // parameter for proper scope binding with your controller.
        element.find('.content').append(transclude(scope.$parent));
    });
};

Here's the modified plunker.

like image 40
Leonardo Chaia Avatar answered Sep 20 '22 06:09

Leonardo Chaia