Transclusion in Angular UI Modal not working - angularjs

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());
};
}
}
});

You have come close enough to achieving your objective with transclusion but, there are a few things you need to consider:
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.
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.
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

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.

transclude: true,
Doesn't work like that, it will insert any markup defined inside the scope of your directive, and will put that markup inside your directive template (where you will put ngTransclude). That's not what you are (seemingly) attempting to do.
What you are looking is to define a template, give it a url and provide it to the modal using the templateUrl property.
HTML
<script type="text/ng-template" id="/some-tpl.html">
<p>some text</p>
<input type="text" value="1234" />
</script>
JS
$uibModal.open({
animation: false,
scope: scope,
templateUrl: "/some-tpl.html" // link to template url
})
Then you can place your directive/your logic inside the controller you provide the modal.
Here is an updated plunk

Related

Angularjs - Directive Two-way binding Isolated Scope Issue

I'm building a SPA based on AngularJS. In one component of the SPA I have a document upload system, which is built via a custom directive below called docmgmt. Within the component docmgmt I have an another custom directive component called modalCompanyQuery. It is a modal window that searches the company database and returns matching company results. Upon the finding the right company the user clicks on the company name which is then passed back to the parent directive docmgmt called modalOutput.
The issue I have is that despite using two way binding '=' a new scope for modalOutput (output) is created in modalCompanyQuery. How can I pass the modalCompanyQuery search result (modalOutput) back to the parent directive docmgmt? Any help on the simplest way to return the results would be great. Thank you in advance!
Here is my code simplified
modalCompanyQuery Template
<div modal-company-query dialog-show="modalCompanyQuery.isShow" dialog-name ="Select Company" dialog-class="modalSelectCompany" dialog-icon ="fa fa-building" dialog-header="modalSelectCompany-header" company-type = "srchCompanyTypeList" output-select="modalOutput">
</div>
Directive docmgmt
angular.module("docmgmt", [])
.directive("docmgmt",['$http','sessionService','Upload','docService', function($http,sessionService,Upload,docService){
return{
link: function(scope,element,attrs){
scope.docRecord = {};
scope.rightPane = {showAction:true, showInsert:false,showUpdate:false, showRead:false};
scope.progressBar = 0;
scope.submit =[{}];
//modal company search and linking search output results to docmgmt scope
scope.modalCompanyQuery = {isShow:false};
scope.modalOutput={};
scope.test=function(){
console.log(scope.modalOutput);
}
},//return
restrict:"A",
replace:true,
templateUrl:"partials/docmgmt/docmgmt.html",//template
transclude:true,
scope:{
}
}//return
}]);
Directive modalCompanyQuery
angular.module("company", [])
.directive("modalCompanyQuery",['$http','companyService', function($http,companyService){
return{
link: function(scope,element,attrs){ // normal variables rather than actual $scope, that is the scope data is passed into scope
//Read Company
scope.getRecord = function(result){
scope.output={id:result.cs_id, type:result.type,name:result.name, active: result.active};
console.log(scope.output);
scope.isShow = false;
}//getRecord
/*AJAX search functions go here*/
},//return
restrict:"A", //assign as attribute only ie <div my-modal> Content </div>
replace:true,//replaces div with element, note if this is the case must all template must be wrapped within one root element. eg button is within ul otherwise get an error.
templateUrl:"partials/company/tpl/desktop/modal-company-query-desktop.html",//template
transclude:true, //incorporate additional data within
scope:{
isShow:"=dialogShow",//two way binding
name:"#dialogName",//name to be in header
dialogClass:"#dialogClass",// style of the dialog
dialogHeader:"#dialogHeader",//color of the dialogHeader
dialogIcon:"#dialogIcon",//font awesome icon
output:"=outputSelect"
//selectCompany:"=selectCompany",//company to be selected from search and passed back to main window
} //If on this should mean the html input is not binded to custom directive
}//return
}]);
alright, in your docmgmt directive, I see you have made the scope of the directive empty doing:
scope: {}.
I think you should do it like:
scope: {
modalOutput: "="
}
btw doing above expects an attribute in your directive template with name modal-output which must be an object type.
Try it...
After some research I found the solution. The following two links really helped me understand the problem and solution.
Understanding $emit, $broadcast and $on in AngularJS
Communication between nested directives
So I end up using $emit and $on. Outcome as follows:
Directive modalCompanyQuery
scope.getRecord = function(result){
scope.output={id:result.cs_id, type:result.type,name:result.name, active: result.active};
scope.$emit('companyRecord', {record:scope.output});
scope.isShow = false;
}//getRecord
Directive docmgmt
scope.$on('companyRecord', function (event, args) {
scope.modalOutput = args.record;
console.log('Success');
console.log(scope.modalOutput);
});
Hope this helps other people that have come across the same brickwall!

How do I assign an attribute to ng-controller in a directive's template in AngularJS?

I have a custom attribute directive (i.e., restrict: "A") and I want to pass two expressions (using {{...}}) into the directive as attributes. I want to pass these attributes into the directive's template, which I use to render two nested div tags -- the outer one containing ng-controller and the inner containing ng-include. The ng-controller will define the controller exclusively used for the template, and the ng-include will render the template's HTML.
An example showing the relevant snippets is below.
HTML:
<div ng-controller="appController">
<custom-directive ctrl="templateController" tmpl="template.html"></custom-directive>
</div>
JS:
function appController($scope) {
// Main application controller
}
function templateController($scope) {
// Controller (separate from main controller) for exclusive use with template
}
app.directive('customDirective', function() {
return {
restrict: 'A',
scope: {
ctrl: '#',
tmpl: '#'
},
// This will work, but not what I want
// Assigning controller explicitly
template: '<div ng-controller="templateController">\
<div ng-include="tmpl"></div>\
</div>'
// This is what I want, but won't work
// Assigning controller via isolate scope variable from attribute
/*template: '<div ng-controller="ctrl">\
<div ng-include="tmpl"></div>\
</div>'*/
};
});
It appears that explicitly assigning the controller works. However, I want to assign the controller via an isolate scope variable that I obtain from an attribute located inside my custom directive in the HTML.
I've fleshed out the above example a little more in the Plunker below, which names the relevant directive contentDisplay (instead of customDirective from above). Let me know in the comments if this example needs more commented clarification:
Plunker
Using an explicit controller assignment (uncommented template code), I achieve the desired functionality. However, when trying to assign the controller via an isolate scope variable (commented template code), it no longer works, throwing an error saying 'ctrl' is not a function, got string.
The reason why I want to vary the controller (instead of just throwing all the controllers into one "master controller" as I've done in the Plunker) is because I want to make my code more organized to maintain readability.
The following ideas may be relevant:
Placing the ng-controller tags inside the template instead of wrapping it around ng-include.
Using one-way binding ('&') to execute functions instead of text binding ('#').
Using a link function instead of / in addition to an isolate scope.
Using an element/class directive instead of attribute directive.
The priority level of ng-controller is lower than that of ng-include.
The order in which the directives are compiled / instantiated may not be correct.
While I'm looking for direct solutions to this issue, I'm also willing to accept workarounds that accomplish the same functionality and are relatively simple.
I don't think you can dynamically write a template key using scope, but you certainly do so within the link function. You can imitate that quite succinctly with a series of built-in Angular functions: $http, $controller, $compile, $templateCache.
Plunker
Relevant code:
link: function( scope, element, attrs )
{
$http.get( scope.tmpl, { cache: $templateCache } )
.then( function( response ) {
templateScope = scope.$new();
templateCtrl = $controller( scope.ctrl, { $scope: templateScope } );
element.html( response.data );
element.children().data('$ngControllerController', templateCtrl);
$compile( element.contents() )( templateScope );
});
}
Inspired strongly by this similar answer.

AngularJS Access DOM inside $watch function

I'm making a directive that resizes a div based on changes in the controller. I need to calculate the amount of available space left in the browser window when changes happen to the model. How do you pass in the element from the link function into the $watch function?
In short, how do I manipulate the DOM based on changes to the model?
var module = angular.module('cmsApp')
module.directive("changeWidth", function($timeout) {
return {
restrict: 'A',
link: function($scope, element, attrs) {
width = element.width();
$scope.$watch('currentFolder', function(value){
// manipulate dom here
});
}
}
});
<!-- need to calculate the size of this -->
<div change-width class="col-md-9 right-pannel"></div>
I don't think Angular is even executing your directive based on your template code. It should be
<div change-width class="col-md-9 right-pannel"></div>
I know this is a source of errors if you are new to Angular. From the docs:
Angular uses name-with-dashes for its custom attributes and camelCase
for the corresponding directives which implement them)

ng-click stops working after the first use of $compile when using nested directives

I have an Angular modal directive that uses a helper/wrapper directive. This way I can always use the same wrapper and just load a different template where needed for different modal content.
PROBLEM: This snippet works, but only for the first life cycle of the modal. So I can fire the modal, close the modal and fire it again. But once the modal is open the second time none of the ng-click directives work. Any tips would be just super.
Usage
<button my-modal="views/login.html">Launch Login-specific Modal</button>
Directive Module (app.js)
angular.module('myModal',[])
.directive('modalWrapper', function(){
return {
replace: true,
templateUrl: 'views/modal.html',
controller: function($scope, $element){
$scope.close = function(){
$element.remove();
};
// NOTE: I use this array to showcase that ng-repeat still works the second time although ng-click stops functioning properly.
$scope.others = ["One", "Two", "Three"];
}
}
})
.directive('myModal', function( $compile){
function link(scope, element, attr){
scope.partial = attr.myModal; // NOTE: Loads sub template via ng-include
var ngModal = $compile('<div modal-wrapper></div>')(scope);
element.on('click', function(){
angular.element('body').append(ngModal);
});
scope.yo = function(){
alert("Yo from inside template.");
};
}
return {
link: link,
scope: {}
}
});
Templates
modal.html
<div class="my-modal">
<p>Modal Wrapper</p>
<div ng-include="partial"></div>
<button ng-click="close()">Close</button>
<p>This just proves that other directives still work (ng-repeat), but ng-click does not.</p>
<div ng-repeat="stuff in others">
<p>{{stuff}}</p>
</div>
</div>
login.html
<h1>Well hey there, I'm the login template.</h1>
<button ng-click="yo()">Say Yo</button>
I think the problem is that you are destroying the scope on which the ng-click is compiled.
When scope.close() is called, an $element.remove() occurs. This both removes the element from the DOM, and destroys the scope to which it is attached. This will result in your ng-click being de-registered.
Unfortunately (as of last time I checked), element.detach() also destroys scope, so your best bet is to compile and append the element to body only once. After this you can use element.show() and element.hide() to show and hide the modal. Alternatively you can recompile the modal each time you want to show it.

angularJS directive with isolated scope, attribute binding doesn't work

Please see this jsfiddle: http://jsfiddle.net/viro/DK5pC/3/
What I did looks right compared to the tutorials and replies I've found, so I'm sure I'm overlooking something trivial.
I'm trying to do a directive on a html element, that will create a sibling div to display a message associated with the original element.
for example, for this html :
<input ng-model="inp" tst-msg="message" />
I would create as a sibling element:
<div class="msg">Msg:<span ng-bind="tstMsg"></span></div>
I was hoping that tstMsg in the div's scope would be bound to message in the input's scope.
Here's what the directive looks like :
angular.module('tst', [])
.directive('tstMsg', function(){
var template = "<div class='msg' >Msg:<span ng-bind='tstMsg'></span></div>";
var link = function(scope,element,attrs) {
element.parent().append(template);
console.log("link called");
};
return {
restrict: 'A',
scope: {
tstMsg: '='
},
link: link
};
});
Well that doesn't work and I can't figure out why.
You need to $compile the template you're adding to the DOM. Angular hasn't had a chance to add it's handlers, for instance the ng-bind directive to that part of the dom.
So instead of just adding the element like this:
element.parent().append(template);
These steps will let Angular process your template and then add it.
newe = angular.element(template);
$compile(newe)(scope);
element.parent().append(newe);
Updated fiddle

Resources