AngularJS : ng-repeat inside a directive masks the directive's controller - javascript

I have an isolated scope directive that has its own controller.
The directive's template has access to that controller, except inside an ng-repeat where the entire controller is out of scope.
I don't know how to fix it.
If you have an ng-repeat inside an ng-controller, children of the ng-repeat inherit the functions of the ng-controller.
I would have thought that would be the case with custom directives, too - children inside the ng-repeat could still see the directive's controller - but as far as I can tell, they can't.
I'm still getting up to speed with Angular, so a nudge in the right direction would be appreciated. A downright answer would be appreciated even more. Something to do with transclusion? With which is getting compiled first?
Here is the directive:
angular.module('surverApp')
.directive('surveyItems', function () {
return {
restrict: 'E',
scope: {
itemList: '=',
query: '='
},
templateUrl: 'views/directives/survey-items.html',
replace: true,
controller: 'SurveyItemsCtrl',
controllerAs: 'ctrl',
bindToController: true
};
})
.controller('SurveyItemsCtrl', function(){
var ctrl = this;
ctrl.disableEdit = true;
ctrl.disableToolbar = false;
ctrl.showForm = false;
ctrl.showDuplicate = false;
ctrl.showTrash = false;
ctrl.deleteItem = function(item) {
console.log('ctrl.item in DELETE ITEM' ,item)
.... functions removed for brevity
};
});
And here is its template. None of its functions fire.
<div>
<pre>ctrl = {{ctrl | json}}</pre> <<<<<=== THIS SHOWS THE CONTROLLER IS IN SCOPE
<div ng-repeat="item in ctrl.itemList | filter:ctrl.query" class="ubi-box container-fluid">
<pre>ctrl = {{ctrl | json}}</pre> <<<<<=== THIS SHOWS THE CONTROLLER IS NOT IN SCOPE
<standard-right-toolbar ctrl="ctrl"></standard-right-toolbar>
<h4>{{item.name}}</h4>
<div ng-show="ctrl.showForm" class="ubi-box container-fluid">
<!-- <survey-form model="item" disable-edit="ctrl.disableEdit" reset-fn="item = ctrl.resetUpdateFn(item)" submit-fn="ctrl.submitUpdateFn()" close-fn="ctrl.hideFormFn()"></survey-form> -->
<survey-form model-el="item" disable-edit-el="ctrl.disableEdit" reset-fn="ctrl.resetUpdateFn(form,model)" submit-fn="ctrl.submitUpdateFn(form)" close-fn="ctrl.hideFormFn()"></survey-form>
<pre>model = {{item | json}}</pre>
</div>
<div ng-show="ctrl.showDuplicate" class="ubi-box container-fluid">
<standard-right-close-bar close-fn="ctrl.hideDuplicateFn()"></standard-right-close-bar>
<h4 class="col-xs-12">Duplicating a survey will copy all the details and questions over to a new survey.</h4>
<h3 class="col-xs-10">Click the copy button to procede.</h3>
<button class="btn btn-lg btn-primary" type="button" ng-click="ctrl.copy(item)" title="Duplicate">
<span class="glyphicon glyphicon-duplicate"></span>
</button>
</div>
<!-- <div ng-show="ctrl.showTrash" class="ubi-box container-fluid"> -->
<div ng-show="ctrl.showTrash" class="ubi-box container-fluid">
<standard-right-close-bar close-fn="ctrl.hideTrashFn()"></standard-right-close-bar>
<h4 class="col-xs-12">Deleting a survey is a very serious matter. It will permanently remove every question and every answer in every questionnaire in every edition in the survey.</h4>
<h3 class="col-xs-10">Click the trashcan only if you are sure!</h3>
<button class="btn btn-lg btn-danger" type="button" ng-click="ctrl.deleteItem(item)" title="Delete">
<span class="glyphicon glyphicon-trash"></span>
</button>
</div>
</div>
<div>
PS I had it working fine inside an ng-controller. But then I read ng-controllers are being outlawed in Angular 2.0, so I thought I'd get in some practice using directive controllers rather than ng-controllers and I wrapped the ng-repeat in a directive instead of an ng-controller.
So far, so stumped . . .

Related

Show a div with AngularJS

I am searching how to show a div with AngularJS. I read some topic on StackOverflow but when I try to apply them, it doesn't works for my case...
This is my HTML code :
<div id="myPanel" ng-controller="controllerDependance" ng-show="myvalue" class="ng-cloak">
Blablabla
</div>
<div id="DivWhereIsMyButton" class="clearfix" ng-controller="controllerBubble">
Another div where is my button
<div id="containerButton" ng-controller="controllerDependance">
<button class="btn btn-danger btn-lg pull-right"
ng-click="showAlert()">View dependances
</button>
</div>
</div>
This is the controller :
d3DemoApp.controller('controllerBubble', function () {
});
d3DemoApp.controller('controllerDependance', function ($scope) {
$scope.myvalue = false;
$scope.showAlert = function(){
$scope.myvalue = true;
};
});
I initially thought, it is controllerOther take the hand and cancel the controllerDiv but even if I separate the both, it doesn't works. The problem is I am obligated to put the both in two differents controllers.
I have two controllers, controllerDependance and controllerBubble. My div to show is in the controllerDependance. My button is in a div controllerBubble and I can't move it. So I would like to wrap it in a div controllerDependance.
I make a Plunker to show you the problem : https://plnkr.co/edit/z1ORNRzHbr7EVQfqHn6z?p=preview
Any idea ?
Thanks.
Put the div you want to show and hide inside the controller. It needs to be within the scope of the controller, otherwise your controller function cant see it. Also, consider what you are trying to accomplish with the nested controllers, I often find them unnecessary.
<div id="divButton" class="clearfix" ng-controller="controllerOther">
<div id="buttonToShowDiv" ng-controller="controllerDiv">
<button class="btn btn-danger btn-lg pull-right" ng-click="showAlert()">Show my div</button>
<div id="myDiv"ng-show="myvalue" class="ng-cloak">
Blablabla
</div>
</div>
</div>
I notice in your original example you are declaring ng-controller="controllerDependance" twice in the DOM. I have never tried this before, but I can imagine this will cause problems. From the angular documentation on controllers
When a Controller is attached to the DOM via the ng-controller directive, Angular will instantiate a new Controller object, using the specified Controller's constructor function. A new child scope will be created and made available as an injectable parameter to the Controller's constructor function as $scope
I imagine that this is what is causing you problems. You have to have the div you want to show/hide within the scope of your controller.
I got your plunkr working, you can see my version here: https://plnkr.co/edit/NXbsVFMNHR8twtL8hoE2?p=preview
The problem was stemming from you declaring the same controller twice, and more importantly, the div to show/hide was using ng-show with a value from your mainController. But your div was outside that controller. So ng-show cant see the value. The div has to be withing the scope of the controller
You are using two different controllers which have different $scopes therefore their values are not connected! To show or hide a div is really simple in angular:
<div id="divButton" class="clearfix" ng-controller="myController">
<div id="buttonToShowDiv">
<button class="btn btn-danger btn-lg pull-right" ng-click="showAlert()">Show my div</button>
</div>
<div id="myDiv" ng-show="myvalue" class="ng-cloak">
Blablabla
</div>
</div>
And the script side just almost the same:
d3DemoApp.controller('myController', function AppCtrl ($scope) {
$scope.myvalue = false;
$scope.showAlert = function(){
$scope.myvalue = true;
};
});
Since you question was how to show elements using angular, I took the liberty of using just one controller.
Create a factory that will return an object and let your controllers work with a reference to the same object:
var d3DemoApp = angular.module('app', [])
d3DemoApp.factory('MyValue', function () {
return { value: false };
});
d3DemoApp.controller('controllerBubble', function ($scope, MyValue) {
$scope.myvalue = MyValue;
});
d3DemoApp.controller('controllerDependance', function ($scope, MyValue) {
$scope.myvalue = MyValue;
$scope.showAlert = function(){
$scope.myvalue.value = true;
};
});
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
</head>
<body>
<div ng-app="app">
<div ng-controller="controllerBubble" class="clearfix">
<div id="myPanel" ng-controller="controllerDependance" ng-show="myvalue.value" class="ng-cloak">
Blablabla
</div>
</div>
<div id="DivWhereIsMyButton" class="clearfix" ng-controller="controllerBubble">
Another div where is my button
<div id="containerButton" ng-controller="controllerDependance">
<button class="btn btn-danger btn-lg pull-right" ng-click="showAlert()">View dependances</button>
</div>
</div>
</div>
</body>
</html>

accessing other scope variables inside custom template

I'm trying to display files inside each folder/directory.
I'm using custom directive to disply each of the directories as follows (this part works).
But it fails to resolve the {{file}} variable inside custom template folderListing.html. Can somebody please correct me where I'm going wrong ?
folderListing.js
app.directive('folderListing', function(){
return {
restrict: 'E',
scope: {
listing:'='
},
templateUrl: 'js/directives/folderListing.html'
};
});
RdaController.js
app.controller('RdaController', ['$scope','RdaService', function($scope,RdaService) {
$scope.folders = ['RDA Server Folder','CTP Outbox', 'CTP Inbox', 'CTP Jobs'];
$scope.sendToCTP = function(file){
return RdaService.SendFileToCTP(file);
};
$scope.listOfFiles = ['learn_javascript.pdf', 'HTML Made Easy.pdf', 'AngularJS for everybody.pdf'];
}]);
index.html
<folder-listing listing="folder" ng-repeat="folder in folders"></folder-listing>
folderListing.html
<div class="row">
<div class="col-md-3" id="{{listing}}">
<table class="table table-striped">
<h3> {{ listing }} </h3>
<div class="row">
<div class="col-md-3" ng-repeat="file in listOfFiles">
{{file}}
</div>
</div>
<td><span class="btn-group" role="group"><button type="button" class="btn btn-default" ng-click="sendToCTP(file)">Execute</button></span></td>
</table>
</div>
</div>
With this:
scope: {
listing:'='
},
You have created an isolate scope passing only listing to the directive. You need to change this to:
scope: {
listing: '=',
listOfFiles: '=',
sendToCTP: '&sendToCtp'
},
To pass the function you'll have to add a send-to-ctp="sendToCTP(file)" attribute on your directive. However, in your template your button with ng-click="sendToCTP(file)" is outside your ng-repeat so file will always be undefined.

Angular, share directive template between click functions

I have a directive which, when called, passes in a controller and an array.
In the controller I pass in, there is an object I want to loop over.
my html looks like this:
<div class="row">
<div class="col-md-6">
<div class="jumbotron" ng-controller="protocolCtrl as pctrl">
<button type="button" id="protocol" class="btn btn-primary btn-lg" ng-click="pctrl.getUpdatedList()"
data-toggle="modal" data-target="#modal">Modify Current Protocols</button>
<!--IN THIS MODAL YOU CAN ADD/CHANGE/DELETE DATA-->
<modal-directive list="pctrl" headers="['ID', 'Protocol']"></modal-directive>
</div>
</div>
<div class="col-md-6">
<div class="jumbotron" ng-controller="categoryCtrl as cctrl">
<button type="button" id="category" class="btn btn-primary btn-lg" ng-click="cctrl.getUpdatedList()"
data-toggle="modal" data-target="#modal">Modify Current Categories</button>
<!--IN THIS MODAL YOU CAN ADD/CHANGE/DELETE DATA-->
<modal-directive list="cctrl" headers="['ID', 'Category']"></modal-directive>
</div>
</div>
</div>
My problem is that no matter what I do, it's always the FIRST directive in the html that showes up, no matter what button I press.
My directive looks like this:
.directive('modalDirective', function(){
return {
restrict: 'E',
templateUrl: '/directives/modal-directive.html',
scope: {
list: '=',
headers: '='
},
link: function(scope, element, attrs){
console.log(attrs.list + ' | ' + attrs.headers);
}
};
});
My modal-directive.html looks like this:
<table class="table table-striped">
<thead>
<tr>
<th ng-repeat="h in headers"> {{ h }} </th>
</tr>
</thead>
<tbody>
<!-- Loop through -->
<tr ng-repeat="l in list.list">
<!--Access the actual values inside each of the objects in the array-->
<td ng-repeat="data in l"> {{ data }} </td>
<td>
<button type="button" class="btn btn-primary btn-sm"
data-toggle="modal">Edit</button>
</td>
<td>
<button type="button" class="btn btn-danger btn-sm" ng-click="list.removeData(l)"
data-dismiss="modal">Remove</button>
</td>
</tr>
</tbody>
</table>
Am I using isolated scopes wrong, or is it something else I need to change in order to make this work?
Update
Here is a fiddle, that demonstrates the problem.
No matter which button i click, it displays the same text in the modal body.
You don't really need two controllers and two directives to achieve this. Below is an example of how you can do this. Notice I moved the controller to the row instead of having separate controllers for each column. The controller myCtrl now handles the click functions which are bound to the buttons using the ng-click attribute. This then determines the which text should be placed where by calling there respective functions. IE proto() and cat()
Now this may not be ideal for your situation depending on how you plan on the architecture of your application. But it works for your current problem in terms of what you have provided.
HTML
<body ng-app="TM">
<div class="row" ng-controller="myCtrl as modalControl">
<div class="col-md-6">
<div class="jumbotron" >
<button
ng-click='proto()'
type="button" id="protocol"
class="btn btn-primary btn-lg"
data-toggle="modal"
data-target="#modal">Modify Current Protocols
</button>
</div>
</div>
<div class="col-md-6">
<div class="jumbotron">
<button
ng-click='cat()'
type="button"
id="category"
class="btn btn-primary btn-lg"
data-toggle="modal"
data-target="#modal">Modify Current Categories
</button>
</div>
</div>
<!--IN THIS MODAL YOU CAN ADD/CHANGE/DELETE DATA-->
<modal-directive ctrl="modalControl"></modal-directive>
</div>
</body>
Angular JS
angular.module('TM', [])
.controller('myCtrl', function($scope){
$scope.text ='default';
$scope.proto = function() {
this.text = 'Now looking at the protocol part'
}
$scope.cat = function() {
this.text = 'Now looking at the category part'
}
})
.directive('modalDirective', function(){
return {
restrict: 'E',
scope: true,
template: ['<div id="modal" class="modal fade" role="dialog">',
'<div class="modal-dialog">',
'<div class="modal-content">',
'<div class="modal-header">',
'<h4 class="modal-title">Modal Header</h4>',
'</div>',
'<div class="modal-body">',
'<p> {{ text }} </p>',
'</div>',
'<div class="modal-footer">',
'<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>',
'</div>',
'</div>',
'</div>',
'</div>'].join('')
}
});
Demo:
https://jsfiddle.net/DTcHh/10193/
UPDATE:
Okay, I took another look. And even though the above example works. I noticed that I have a few extra things that I didn't necessarily need. For example myCtrl as modalControl doesn't need the as modalControl part. Below is an updated example. I did this one with some different simplified markup.
HTML:
<body ng-app="TestApp">
<div ng-controller="myCtrl">
<button ng-click="one()">One</button>
<button ng-click="two()">Two</button>
<test-directive></test-directive>
</div>
</body>
Angular Example (without Isolated Scope)
angular.module('TestApp', [])
.controller('myCtrl', function($scope){
$scope.text ='default';
$scope.one = function() {
this.text = 'this is one'
}
$scope.two = function() {
this.text = 'this is two'
}
})
.directive('testDirective', function(){
return {
template: "<div id='test'>{{text}}</div>"
}
});
Demo 2:
https://jsfiddle.net/krishollenbeck/v8tczaea/12/
Note this..
restrict: 'E',
scope: true
Was also not needed because I am not using Isolated scope in this example. More info here https://docs.angularjs.org/guide/directive
Please check this JSFiddle.
The reason is that data-target value points to the DOM element id of the modal. If you fixed this id in the directive template, clicking on the button will always initiate the modal with id modal. So you need to make the modalId as another parameter of the directive.
By the way, you can pass a controller to a directive. Just like this JSFiddle:
angular.module('Joy', [])
.controller('MyCtrl', ['$scope', function ($scope) {
this.value = 'Joy';
}])
.directive('passMeContrller', [function () {
return {
restrict: 'A',
scope: {
ctrl: '=',
},
template: '<div>Value: {{ctrl.value}}</div>'
};
}]);
The HTML:
<div ng-app="Joy" ng-controller="MyCtrl as c">
<div pass-me-contrller ctrl="c"></div>
<hr>
<div ng-bind="c.value"></div>
</div>
Because the controller itself is just a JavaScript object.
Just a reminder: you are using protocolCtrl as pctrl, so you need to specify like this.list=....
If you want to pass in a function to the isolated scope, use &.
However, I suggest not to pass in the whole controller to a directive. A controller is designed to:
Set up the initial state of the $scope object.
Add behavior to the $scope object.
Controllers are not supposed to be reused. Usually there are many properties on the $scope, while some of them passed to the directive will not be used by it.

How to use the moment library in angular in a directive that uses compile instead of link

I have a tree of connected documents (parent to child) in my database from a single model called Actions, they're recursively compiled in an angular directive so that they're nested inside their parents.
I have the following code:
angular.module('crmDashboardApp')
.directive('actionDirective', function ($http, $compile, RecursionHelper) {
return {
scope: {
actionId: '=', // html:action-node, js:actionNode
actionList: '='
},
templateUrl: 'app/main/directive/action/action.directive.html',
replace: true,
restrict: 'E',
compile: function (element) {
return RecursionHelper.compile(element, function(scope, iElement, iAttrs, controller, transcludeFn){
scope.deleteAction = function (_action) {
var id = _action._id;
$http.delete('/api/actions', {
data: {'id':id},
headers: {"Content-Type": "application/json;charset=utf-8"} // we need to do this if we want to send params, otherwise we need to do traditional REST in URL
});
};
// Find for already called action list
scope.findAction = function (_id, _list) {
scope.actionNode = _.findWhere(_list, {_id:_id})
};
scope.findAction(scope.actionId, scope.actionList);
function calculateTimeSince(){
scope.fromNow = moment(scope.actionNode.content).fromNow(true);
}
setInterval(calculateTimeSince, 1000);
scope.fromNow = moment(scope.actionNode.content).fromNow(true);
});
}
};
});
This only compiles once on load and changing anything in the scope after does nothing. I want the setInterval function to change a variable scope.fromNow to be updated every second and update the view (the HTML references this with a simple {{fromNow}})
I believe I'll have to re-compile the directive somehow but doing something like:
$compile(element.contents())(scope)
within the setInterval function doesn't work.
My directive's HTML looks like this:
<div class="action-node">
<header>{{ actionNode.name }}</header>
<div class="row">
<h3 class="col-md-12">{{ actionNode.title }}</h2>
<h5 class="col-md-12">{{ actionNode.description }}</h5>
</div>
<div class="row">
<div class="col-md-3">Time Since: {{fromNow}}</div>
<div class="col-md-3">Content: {{ actionNode.content}}</div>
<div class="col-md-3">Duration Type:{{ actionNode.duration_type }}</div>
<div class="col-md-3">Type: {{ actionNode.type }}</div>
</div>
<div class="row">
<div class="col-md-4">
{{actionNode.children.length > 0 ? actionNode.children : "No children" }}
</div>
<form class="pull-right" ng-submit="deleteAction(actionNode)">
<input class="btn btn-primary" type="submit" value="Delete">
</form>
</div>
<div class="action-wrapper" ng-repeat="child in actionNode.children" ng-if="actionNode.children.length > 0">
<!-- <div class="row" ng-repeat="child in actionNode.children" ng-if="actionNode.children.length > 0" ng-style="{'margin-left': ({{actionNode.nest_level}}+1)*30+'px'}"> -->
<action-directive action-ID="child" action-list="actionList" />
</div>
</div>
You can see that it calls itself again right at the bottom. I am also using RecursionHelper so infinite loop isn't an issue.
Instead of using setInterval, you need to use the Angular wrapper service $interval.
$interval service synchronizes the view and model by internally calling $scope.$apply which executes a digest cycle.

angular bootstrap modal masks forms

I am trying to get at an angular form in scope to verify validations etc.
Base Case
Let us say I have the following HTML:
<body ng-controller='MyAwesomeController'>
<form name="fooForm">
<textarea ng-model="reason" required=""></textarea>
</form>
<div class='btn btn-primary' ng-click="submit()" ng-class="{'btn-disabled': true}">Awesome Submit Button</div>
</body>
And the following controller
angular.module('MyAwesomeController', '$scope', function(scope){
scope.submit = function(){
console.debug(scope)
}
})
This will work, and upon inspection, scope will contain a fooForm key.
Let us now say that I introduce an angular ui bootstrap modal into the mix like so:
Broken Case
<body ng-controller="AwesomeParentController">
<div class="btn btn-primary" ng-click="open()">Open the Modal</div>
</body>
with the following two controllers:
.controller('AwesomeParentController', ['$scope', '$modal', function(scope, modal){
scope.open = function(){
options = {
windowClass: 'modal discontinue-modal',
templateUrl: 'modal.html',
controller: 'AwesomeModalController'
}
modalInstance = modal.open(options)
modalInstance.result.then(function(){
console.debug("result!")
})
}
}])
.controller("AwesomeModalController", ['$scope', '$modalInstance', function(scope, modalInstance){
scope.submit = function(){
console.debug(scope)
}
}])
with the following modal.html:
<form name="fooForm">
<textarea ng-model="reason" required=""></textarea>
</form>
<div class='btn btn-primary' ng-click="submit()">Awesome Submit Button</div>
When the first button is clicked, a modal opens, the second button click prints a scope, which does NOT contain fooForm, rather fooForm resides on scope.$$childTail
Plunkr: http://embed.plnkr.co/jFGU0teIbL3kUXdyTPxR/preview
Possible Fix
<form name="fooForm">
<div ng-controller ="JankyFormController">
<textarea ng-model="reason" required=""></textarea>
<div class='btn btn-primary' ng-click="submit()">Awesome Submit Button</div>
</div>
</form>
.controller('JankyFormController', ['$scope', function(scope){
scope.models['fooForm'] = scope.fooForm
}])
Plunkr: http://embed.plnkr.co/BAZFbS7hFRhHm8DqOpQy/preview
Why is the form being masked? What would be a clean way to get at it without having to create a nested child controller? what if I want to bind ng-class to the forms validity? Would I now have to construct a watch around ($$childTail).fooForm.$valid?
Update: angular ui-bootstrap 0.12.0 fixes the problem - transclusion scope becomes the same as controller's scope, no need for $parent.. Just upgrade.
Before 0.12.0:
Angular-UI modals are using transclusion to attach modal content, which means any new scope entries made within modal are created in child scope. This happens with form directive.
This is known issue: https://github.com/angular-ui/bootstrap/issues/969
I proposed the quick workaround which works for me, with Angular 1.2.16:
<form name="$parent.userForm">
The userForm is created and available in modal's controller $scope. Thanks to scope inheritance userForm access stays untouched in the markup.
<div ng-class="{'has-error': userForm.email.$invalid}"}>

Categories

Resources