$scope is not available to ng-template - angularjs

I'm trying to use modal for editing a form, the modal is in ng-template script, but the form data is not displayed when clicking the edit button.
The $scope is not available to the template script.
I have created a Plunker here
$scope.setCurrentItem = function (item) {
$scope.currentItem = item;
};
$scope.edit = function (item) { //editing item
$scope.setCurrentItem(angular.copy(item));
//$scope.editItem = item;
openEditModal();
};
<!--html-->
<script type="text/ng-template" id="myModalContent.html">
<label for="name">Role: </label>
<input type="text" ng-model="currentItem.roleName" required />
</script>
How can I fix that?

By default ui bootstrap $modal uses $rootScope as its default scope. But you are assuming it will automatically take the scope of the controller that opened the dialog, which does not happen. But there is a scope property that you can set to pass the scope to the ui modal so that it will use that scope and create a child scope out of the provided scope and will be used as the underlying scope for the modal. So have your modal wrapper take the scope property as well in its settings and pass it through.
From Doc
scope - a scope instance to be used for the modal's content (actually the $modal service is going to create a child scope of a provided scope). Defaults to $rootScope.
Example changes:-
function openEditModal() {
var modalOptions = {
closeButtonText: 'Cancel',
actionButtonText: 'Save role',
headerText: 'Edit role',
bodyText: '',
scope:$scope //<-- Pass scope
};
Modal.showModal({ templateUrl: 'myModalContent.html', size: 'lg' }, modalOptions).then(function (result) {
console.log("save!", result);
});
}
and in your service:-
/*I just did this here based on my limited understanding of your code*/
return $modal.open(angular.extend(tempModalDefaults, customModalOptions)).result;
From your modal template pass the item back, i am not sure if your template is generic, if so then you may want to take a different approach to transfer the data back:-
<button class="btn btn-primary" ng-click="modalOptions.ok(currentItem)">{{modalOptions.actionButtonText}}</button>
Demo

Related

How to make custom directive with dialog box template using Angular JS?

I need to show a confirmation box on different pages. So i have decided to create a custom directive for performing this task. I have a html template for confirmation box.There are two buttons and some text in this template. One button is for cancelling the dialog box and one for submitting it. So the functionality will be different for each page when we click on submit button. I have couple of questions regarding this issue.
How to create this kind of directive to show a dialog box on some condition?
How to pass text from my controller to this template?
How to override the "Submit" button functionality.
I had similar requirement where I wanted a custom modal pop-up to alert the user to continue with his actions such as delete, modify etc..,
So I wrote a custom directive. Below is the code.
(function(){
'use strict';
angular.module('mainApp').directive('confirm', ['$log','$uibModal', function($log,$uibModal){
var link = function($scope,elem,attr){
elem.bind('click',function(){
var modalInstance = $uibModal.open({
animation: true,
templateUrl: 'templates/shared/_confirm_modal.html',
controller: 'confirmDirectiveCtrl',
size: 'sm'
,backdrop: 'static' //disables modal closing by click on the backdrop.
,resolve: {
requiredVerbose: function(){
var requiredVerbose = {
modalTitle : attr.modalTitle
,message : attr.message
,confirmVerbose : attr.confirmVerbose
,cancelVerbose : attr.cancelVerbose
} ;
return requiredVerbose;
}
}
});
modalInstance.result.then(function(){
$scope.confirmFn();
}, function(){
if($scope.cancelFn){
$scope.cancelFn();
}
});
});
}
return{
restrict : 'A'
,scope : {
confirmFn : '&'
,cancelFn : '&'
}
,compile : function compile(elem,attr){
if(attr.confirmType && attr.confirmType=='delete')
{
attr.modalTitle = 'Warning';
attr.confirmVerbose = 'Delete';
attr.cancelVerbose = 'No';
attr.message = 'Are you sure, you want to delete?'
}
else{
if(!attr.modalTitle){attr.modalTitle = 'Warning'}
if(!attr.confirmVerbose){attr.confirmVerbose = 'Ok'}
if(!attr.cancelVerbose){attr.cancelVerbose = 'cancel'}
if(!attr.message){attr.message = 'Are you sure?'}
}
return{
post : link
}
}
}
}]);
angular.module('mainApp').controller('confirmDirectiveCtrl', ['$scope','$uibModalInstance','requiredVerbose',
function($scope,$uibModalInstance, requiredVerbose){
$scope.modalTitle= requiredVerbose.modalTitle;
$scope.message = requiredVerbose.message;
$scope.confirmVerbose = requiredVerbose.confirmVerbose;
$scope.cancelVerbose= requiredVerbose.cancelVerbose;
$scope.ok = function(){
$uibModalInstance.close($scope.timeline);
};
$scope.cancel = function(){
$uibModalInstance.dismiss();
};
}]);
}());
To answer your questions,
This is attribute type directive. And the element on which you add this directive tag is bound to onclick function which generates the required popup.
How to pass text?
You can pass the required text through attributes. I wanted this directive to work only for two kinds of alerts and hence had only two different sets of texts. If you want custom texts everytime, you can pass them to directive through attrs.
How to override the submit functionality?
You can pass your custom submit and cancel to this directive and bind them to the popup submit and cancel functions. The above code does the same.
Edit :
HTML template and explanation:
Below is an example describing on how you can use this directive.
<i class="fa fa-trash-o"
confirm
confirm-fn="deletePlaylist($index)"
confirm-type="delete">
</i>
The above template is an trash icon. The attributes are
directive name : confirm
confirm-fn : The function that should be called after user seleting ok/submit etc..,
confirm-type : This attribute defines what type of popup you want to show. In my case, I often use 'delete' type and hence wrote the required verbose related to it. By default, I already defined the verbose(title, message, ok-button, cancel-button).
If you want your custom messages add them in the attributes. Below is one such example.
<i class="fa fa-trash-o"
confirm
confirm-fn="doingGreatFn()"
cancel-fn="justFineFn()"
modal-title="My Modal"
message="How are you doing?"
confirm-verbose="Great"
cancel-verbose="Just Fine">
</i>
I hope, this helps
You can create a directive like below to handle both submit & cancel at any page for different functionalities in any controller. I've created an isolated scope directive but you can use change it according to your need by creating child scope scope : true; or bindToController:true (controller specific)
app.directive('confirm', ['$log', '$modal' ,'$parse','$timeout','factory', function($log, $modal,$parse,$timeout,factory) {
return {
restrict: 'E',
template:'<button type="button" class="btn form-btn" '+
'ng-click="openModal()" ng-disabled="disable" >'+
'{{buttonName}}</button>',
replace: true,
transclude: false,
scope: {
name :'=name', //can set button name ..basically u can send a text
disable :'=disable' //set as an attribute in HTML to disable button
},
link: function ($scope, element, attrs) {
$scope.buttonName = $scope.name;
$scope.openModal= function() {
$scope.modal = $modal.open({
templateUrl: 'customConfirmModal.html',
scope:$scope,
persist: true,
backdrop: 'static'
});
};
$scope.cancel = function(){
$scope.modal.dismiss();
};
$scope.submit= function(){
factory.customSubmitCall($scope);//call the factory method which will call different functions depending on the need..
};
}
Create a factory to contain different functions which can be called at any controller by injecting factory.
app.factory('factory', ['$http','$rootScope','$filter',function($http,$rootScope,$filter){
factory.customSubmitCall = function ($scope){
if($rootScope.page ==1){ //check on which page you are performing action
$scope.pageOneSubmit(); //page specific function in that controller..
}else{
$scope.submit();
}
};
return factory;
}]);
In your HTML
<confirm name="Confirm" disable="disable"> </confirm>

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

how to make custom directive in angular?

I am trying to make custom directive in angular .I try to add input field in my view when I click on button .In other words I am trying to make one custom directive in which when user press the button it add one input field in the browser .I think it is too easy if I am not use custom directive Mean If I use only controller then I take one array and push item in array when user click on button and button click is present on controller.
But when need to make custom directive where I will write my button click event in controller or directive
here is my code
http://play.ionic.io/app/23ec466dac1d
angular.module('app', ['ionic']).controller('appcontrl',function($scope){
$scope.data=[]
}).directive('inputbutton',function(){
return {
restrict :'E',
scope:{
data:'='
},
template:'<button>Add input</button> <div ng-repeat="d in data"><input type="text"></div>',
link:function(s,e,a){
e.bind('click',function(){
s.data.push({})
})
}
}
})
I just need to add input field when user click on button using custom directive ..could you please tell me where i am doing wrong ?
can we make button template and click event inside the directive
The reason it doesn't work is because your registering your click handler with jQuery. So when the click handler fires it is out of the scope of angular so angular does not know it needs to update its bindings.
So you have two options, the first is to tell angular in the click handler, 'yo buddy, update your bindings'. this is done using $scope.$apply
$apply docs: https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$apply
e.bind('click',function(){
s.$apply(function() {
s.data.push({});
});
});
However angular already has built in directive for handling things like mouse clicks you can just use that and let angular do the work for you. This would be the better option.
so first in your view register a click handler on your button
<button ng-click="add()">Add input</button> <div ng-repeat="d in data"><input type="text"></div>
Then in your link simply add the add() method of your scope
s.add = function () {
s.data.push({});
}
Heres a working fiddle showing both examples. http://jsfiddle.net/3dgdrvkq/
EDIT: Also noticed a slight bug in your initial click handler. You registering a click but not specifying the button to apply it to. So if you clicked anywhere in the directive, not just the button, the handler would fire. You should be more specific when registering events manually, using ids, class names attributes etc.
The e or element property of the link function is a jqlite or full jQuery object of the entire directive. If you have jQuery included before angular it will be a full jQuery object. If not it will a jqlite object. A thinned out version of jQuery.
Here is a basic example for your logic .
var TestApp = angular.module('App', []);
// controller
TestApp.controller('mainCtrl', function mainCtrl($scope) {
$scope.data = [];
$scope.addDataItem = function () {
$scope.data.push({
someFilield: 'some value'
});
console.log('pushing value ... ');
}
});
// view
<div ng-app="App" class="container" ng-controller="mainCtrl">
<button type="button" ng-click="addDataItem()">Add an input</button>
<div ng-repeat="d in data track by $index">
<custom-directive model="d"></custom-directive>
</div>
</div>
// directive
TestApp.directive('customDirective', function customDirective() {
return {
restrict: 'E',
scope: {
model: '='
},
template: 'item -> <input type = "text" />',
link: function (scope, elem, attrs) {
console.log('scope.model', scope.model);
},
controller: function ($scope) {
// do staff here
}
}
});

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 can't access form object in controller ($scope)

I am using bootstrap-ui more specifically modal windows. And I have a form in a modal, what I want is to instantiate form validation object. So basically I am doing this:
<form name="form">
<div class="form-group">
<label for="answer_rows">Answer rows:</label>
<textarea name="answer_rows" ng-model="question.answer_rows"></textarea>
</div>
</form>
<pre>
{{form | json}}
</pre
I can see form object in the html file without no problem, however if I want to access the form validation object from controller. It just outputs me empty object. Here is controller example:
.controller('EditQuestionCtrl', function ($scope, $modalInstance) {
$scope.question = {};
$scope.form = {};
$scope.update = function () {
console.log($scope.form); //empty object
console.log($scope.question); // can see form input
};
});
What might be the reasons that I can't access $scope.form from controller ?
Just for those who are not using $scope, but rather this, in their controller, you'll have to add the controller alias preceding the name of the form. For example:
<div ng-controller="ClientsController as clients">
<form name="clients.something">
</form>
</div>
and then on the controller:
app.controller('ClientsController', function() {
// setting $setPristine()
this.something.$setPristine();
};
Hope it also contributes to the overall set of answers.
The normal way if ng-controller is a parent of the form element:
please remove this line:
$scope.form = {};
If angular sets the form to your controllers $scope you overwrite it with an empty object.
As the OP stated that is not the case here. He is using $modal.open, so the controller is not the parent of the form. I don't know a nice solution. But this problem can be hacked:
<form name="form" ng-init="setFormScope(this)">
...
and in your controller:
$scope.setFormScope= function(scope){
this.formScope = scope;
}
and later in your update function:
$scope.update = function () {
console.log(this.formScope.form);
};
Look at the source code of the 'modal' of angular ui bootstrap, you will see the directive has
transclude: true
This means the modal window will create a new child scope whose parent here is the controller $scope, as the sibling of the directive scope. Then the 'form' can only be access by the newly created child scope.
One solution is define a var in the controller scope like
$scope.forms = {};
Then for the form name, we use something like forms.formName1. This way we could still access it from our controller by just call $scope.forms.formName1.
This works because the inheritance mechanism in JS is prototype chain. When child scope tries to create the forms.formName1, it first tries to find the forms object in its own scope which definitely does not have it since it is created on the fly. Then it will try to find it from the parent(up to the prototype chain) and here since we have it defined in the controller scope, it uses this 'forms' object we created to define the variable formName1. As a result we could still use it in our controller to do our stuff like:
if($scope.forms.formName1.$valid){
//if form is valid
}
More about transclusion, look at the below Misco's video from 45 min. (this is probably the most accurate explanation of what transcluded scopes are that I've ever found !!!)
www.youtube.com/watch?v=WqmeI5fZcho
No need for the ng-init trickery, because the issue is that $scope.form is not set when the controller code is run. Remove the form = {} initialization and get access to the form using a watch:
$scope.$watch('form', function(form) {
...
});
I use the documented approach.
https://docs.angularjs.org/guide/forms
so, user the form name, on "save" click for example just pass the formName as a parameter and hey presto form available in save method (where formScopeObject is greated based upon the ng-models specifications you set in your form OR if you are editing this would be the object storing the item being edited i.e. a user account)
<form name="formExample" novalidate>
<!-- some form stuff here -->
Name
<input type="text" name="aField" ng-model="aField" required="" />
<br /><br />
<input type="button" ng-click="Save(formExample,formScopeObject)" />
</form>
To expand on the answer by user1338062: A solution I have used multiple times to initialize something in my controller but had to wait until it was actually available to use:
var myVarWatch = $scope.$watch("myVar", function(){
if(myVar){
//do whatever init you need to
myVarWatch(); //make sure you call this to remove the watch
}
});
For those using Angular 1.5, my solution was $watching the form on the $postlink stage:
$postLink() {
this.$scope.$watch(() => this.$scope.form.$valid, () => {
});
}

Resources