Here is my html
<li
ng-repeat="myElement in myList">
<input
type="checkbox" value="myElement"
ng-model="checkState"
ng-click="myDirective.updateSelectedElement(myElement, checkState)"/>
<div>
{{myElement.name}}
</div>
</li>
I have a $broadcast event such that
$scope.$on('myEvent', function(event, data){
// Change the checkbox state for the checkbox that have the same name in data
})
The question is since my checkbox are all isolated scopes, I don't have access to it, is there a way to access the checkbox that have specific myElement.name
Thanks
Doing it through the rootscope will solve your problem
app.controller("ControllerA", ["$scope", "$rootScope", function($scope, $rootScope){
$rootScope.$broadcast("myEvent");
}]);
app.controller("ControllerB", ["$scope", "$rootScope", function($scope, $rootScope){
$rootScope.$on('myEvent', function(event, data){
$scope.checkState = true;
})
}]);
Here is a working example of code that I wrote to show how checkboxes could be programmatically checked and unchecked from outside of the directive using events. I use buttons in this example, but you could call the methods from inside the controller as well.
Example in Plunker
Also, as a best practice, if you are dispatching an event from rootScope and listening for it on rootScope as well, use $emit instead of $broadcast so that the event starts and ends at rootScope and is not broadcast down the scope chain.
Angular Code
angular.module('app', [
]).controller('MainController', ['$scope', '$rootScope', function ($scope, $rootScope) {
$scope.list = [{label: 'Camaro'}, {label: 'Chevette'}, {label: 'Corvette'}];
$scope.checkItem = function (label, status) {
$rootScope.$emit('checkItemEvent', {label: label, check: status});
};
}])
.directive('checkboxDirective', function ($rootScope) {
return {
restrict: 'AE',
template: '<input type="checkbox" ng-model="item.checkState"/><span>{{item.label}}</span>',
require: 'ngModel',
scope: {
item: '=ngModel'
},
link: function ($scope) {
console.log('$scope.item', $scope.item);
$rootScope.$on('checkItemEvent', function (event, data) {
console.log('data', data);
if (data.label === $scope.item.label) {
$scope.item.checkState = data.check;
}
});
}
};
});
HTML
<!DOCTYPE html>
<html ng-app="app">
<head>
<link rel="stylesheet" href="style.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular.js"></script>
<script src="script.js"></script>
</head>
<body ng-controller="MainController">
<h1>Checkbox demo</h1>
<ul>
<li ng-repeat="item in list">
<checkbox-directive ng-model="item"></checkbox-directive>
</li>
</ul>
<div>
<button ng-click="checkItem('Corvette', true)">Check Corvette</button>
<button ng-click="checkItem('Corvette', false)">Uncheck Corvette</button>
</div>
</body>
</html>
Related
I'm trying to create a directive which allows me to pass in an attribute string which I then use as the "name" parameter when subscribing to events using $scope.$on. Essentially, the series of events is this:
An object is broadcasted using $rootScope.$broadcast called 'validationResultMessage', in another controller for example.
I have a directive which has an attribute called "subscription" to which I pass the string 'validationResultMessage'.
That directive passes the value of the "subscription" attribute to its scope and subscribes to it with "$scope.$on".
The problem is, it looks like the value of the attribute is "undefined" at the time everything is evaluated, and so when I try to subscribe using $scope.$on, it actually subscribes me to "undefined" rather than "validationResultMessage"
Here is my directive:
app.directive('detailPane', function () {
return {
restrict: 'E',
scope: {
selectedItem: '=',
subscription: '#',
},
templateUrl: 'app/templates/DetailPane.html', //I'm also worried that this is causing my controller to get instantiated twice
controller: 'DetailPaneController'
};
});
which I then use like this:
<td class="sidebar" ng-controller="DetailPaneController" ng-style="{ 'display': sidebarDisplay }">
<detail-pane
selected-item='validationResult'
subscription='validationResultMessage'/>
</td>
And the controller that I'm trying to pass this attribute into:
app.controller('DetailPaneController', ['$scope', '$http', 'dataService', 'toastr', '$uibModal', '$rootScope', '$attrs', function ($scope, $http, dataService, toastr, $uibModal, $rootScope, $attrs) {
$scope.fetching = [];
$scope.validationResult = null;
$scope.sidebarDisplay = 'block';
console.log('subscription is ', $scope.subscription);
var thisSubscription = $scope.subscription;
//if I hardcode the param as 'validationResultMessage', this works
$scope.$on($scope.subscription, function (event, arg) {
$scope.validationResult = arg;
});
}]);
So another way that I managed to solve this particular issue is to only use the internal DetailPaneController as defined in the directive body. Part of my problem was that I was causing the controller to be instantiated twice by having it as both the parent controller using ng-controller= in my html as well as being defined in the directive body. This way I can just use the straightforward "#" binding and everything gets resolved in the right order. I can even have another directive within my template that I can pass my validationResult into.
The new setup looks like this:
DetailPaneController:
app.controller('DetailPaneController', ['$scope', '$http', function ($scope, $http) {
$scope.$on($scope.subscription, function (event, arg) {
$scope.validationResult = arg;
$scope.exception = JSON.parse(arg.Exception);
});
}]);
DetailPane Directive:
app.directive('detailPane', function () {
return {
restrict: 'E',
scope: {
subscription: '#' //notice I am no longer binding to validationResult
},
templateUrl: 'app/templates/DetailPane.html',
controller: 'DetailPaneController'
};
});
Directive as used in HTML:
<div class="sidebar" ng-style="{ 'display': sidebarDisplay }">
<detail-pane subscription='validationResultMessage' />
</div>
Directive Template (for good measure):
<div class="well sidebar-container">
<h3>Details</h3>
<div ng-show="validationResult == null" style="padding: 15px 0 0 15px;">
<h5 class=""><i class="fa fa-exclamation-triangle" aria-hidden="true" /> Select a break to view</h5>
</div>
<div ng-show="validationResult != null">
<table class="table table-striped">
<tr ng-repeat="(key, value) in validationResult">
<td class="sidebar-labels">{{key | someFilter}}</td>
<td >{{value | someOtherFilter : key}}</td>
</tr>
</table>
<another-directive selected-item="validationResult" endpoint="endpoint" />
</div>
I'm going to post my answer 1st, given that it's a bit of code, please let me know if this is the required outcome, so I can provide comments. You should be able to run the provided code snippet.
var app = angular.module('myApp', []);
app.directive('detailPane', function() {
return {
restrict: 'E',
transclude: false,
scope: {
selectedItem: '=',
subscription: '#'
},
link: function(scope, elem, attr) {
scope.$on(scope.subscription, function(e, data) {
scope.selectedItem = data.result;
elem.text(data.message);
});
},
};
});
app.controller('DetailPaneController', function($scope) {
$scope.validationResult1 = "";
$scope.validationResult2 = "";
});
app.controller('SecondController', function($rootScope, $scope, $timeout) {
$timeout(function() {
$rootScope.$broadcast('validationResultMessage1', {
message: 'You fail!',
result: 'Result from 1st fail'
})
}, 2000);
$timeout(function() {
$rootScope.$broadcast('validationResultMessage2', {
message: 'You also fail 2!',
result: 'Result from 2nd fail'
})
}, 4000);
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body ng-app='myApp'>
<div ng-controller="DetailPaneController">
<detail-pane class='hello' selected-item='validationResult1' subscription='validationResultMessage1'></detail-pane>
<br/>
<detail-pane class='hello' selected-item='validationResult2' subscription='validationResultMessage2'></detail-pane>
<hr/>
<span>{{validationResult1}}</span>
<br/>
<span>{{validationResult2}}</span>
</div>
<div ng-controller="SecondController">
</div>
</body>
I think you should set watcher on $scope.subscription and checking if new value is set and then start subscribing passed event.
$scope.$watch('subscription', function(nv, ov){
//this makes sure it won't trigger at initialization
if(nv!==ov){
$scope.$on($scope.subscription, function (event, arg) {
$scope.validationResult = arg;
});
}
});
I have a isolate scope directive that I am using inside ng-repeat, which is iterating over an array from the controller of that template. The template is as follows:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="bootstrap.min.css" />
<script src="angular.min.js"></script>
<script src="script1.js"></script>
</head>
<body ng-app="AddNewTest" ng-controller="AddNewController">
<div class="items" ng-repeat="row in rows">
<add-new-row data="row" index="$index"></add-new-row>
</div>
</body>
</html>
The directive is defined as follows:
angular.module('AddNewTest', []).
directive('addNewRow', function ($timeout) {
return {
controller: 'AddNewController',
link: function (scope, element, attribute) {
element.on('keyup', function(event){
if(scope.index + 1 == scope.rows.length) {
console.log('keyup happening');
$timeout(function () {
scope.rows.push({ value: '' });
scope.$apply();
});
}
})
},
restrict: 'E',
replace: true,
scope: {
index: '='
},
template: '<div class="add-new"><input type="text" placeholder="{{index}}" ng-model="value" /></div>'
}
}).
controller('AddNewController', function ($scope, $timeout) {
$scope.rows = [
{ value: '' }
];
});
But even after adding new row and doing a $apply() the ng-repeat is not rendering the new data added. Please help.
Plnkr Link Here
Pass array of rows to directive as follow:-
scope: {
index: '=',
rows :'='
},
<add-new-row rows="rows" index="$index"></add-new-row>
Working plunker
Each ng-repeat creates an isolated scope than you declare an isolated scope inside your directive that has the same controller as the div. You're swimming in the $scope soup :)
I would personnally make a clean and independent directive with its own controller.
angular.module('AddNewTest', []).
directive('addNewRow', function () {
return {
restrict: 'E',
controller: MyController,
controllerAs: '$ctrl',
template: '{{$ctrl.rows.length}}<div class="add-new"><pre>{{$ctrl.rows | json}}</pre><input type="text" placeholder="0" ng-model="value" /></div>'
}
}).
controller('MyController', MyController);
function MyController($scope) {
var vm = this;
this.rows = [ { value: '' } ];
$scope.$watch("value",function(value){
if(value)
vm.rows.push({ value: value });
});
}
http://plnkr.co/edit/AvjXWWKMz0RKSwvYNt6a?p=preview
Of course you can still bind some data to the directive using bindToController (instead of scope:{}) and if you need an ng-repeat, do it in the directive template directly.
The main point of this post is that I want to avoid using $scope.$watch as it is taught to cause a performance decrease.
So imagine having one shared view partial/template, call it "mypage" with two different directives, call them "directive1" and "directive2", in it that share a data model, lets call it "awesomeData"
Maybe it looks something like this:
<div class="mypage-root">
<directive1 shared-data="awesomeData"></directive1>
<directive2 shared-data="awesomeData"></directive2>
</div>
Now obviously when "awesomeData" changes in either directive or the root view, the data changes in the other parts too (assuming it is two-way bound).
But what if i want something else to happen in directive2 when directive1 has updated the data model, like calling a function in directive2?
I could use a watcher but as mentioned, that is a performance decrease.
What other approaches are there and what is the "true" angular way to do this?
I know you're asking about how to do this without a watcher, but I think in this instance a watcher is the best answer as it gives separation of concerns. directive1 shouldn't have to know about directive2, but only about MySharedService and anything it might broadcast:
$rootScope.$broadcast('myVar.updated');
...
$scope.$on('myVar.updated', function (event, args) {
});
Edit:
Also, depending on the complexity of the model the directives will be using, you could pass this model from their parent controller (The down side to this is that you have to instantiate a model from a parent):
HTML:
<!DOCTYPE html>
<html ng-app="app">
<head>
<script data-require="angularjs#1.4.9" data-semver="1.4.9" src="https://code.angularjs.org/1.4.9/angular.min.js"></script>
<script src="script.js"></script>
</head>
<body ng-controller="MyCtrl">
<directive1 my-model="model"></directive1>
<directive2 my-model="model"></directive2>
</body>
</html>
JS:
var app = angular.module('app', []);
app.controller('MyCtrl', function($scope) {
$scope.model = { info: 'Starting...' };
});
app.directive('directive1', function() {
var controllerFn = function($scope) {
$scope.changeInfo = function() {
$scope.vm.myModel.info = 'Directive 1';
};
};
return {
restrict: 'E',
controller: controllerFn,
template: '<div><span>hello</span> <span ng-bind="vm.myModel.info"></span> <button ng-click="changeInfo()">Click</button></div>',
controllerAs: 'vm',
bindToController: true,
scope: {
myModel: '='
}
}
});
app.directive('directive2', function() {
var controllerFn = function($scope) {
$scope.changeInfo = function() {
$scope.vm.myModel.info = 'Directive 2';
};
};
return {
restrict: 'E',
controller: controllerFn,
template: '<div<span>bye</span> <span ng-bind="vm.myModel.info"></span> <button ng-click="changeInfo()">Click</button></div>',
controllerAs: 'vm',
bindToController: true,
scope: {
myModel: '='
}
}
});
plunker: https://plnkr.co/edit/EwdHFl7eZl2rd43UVG4J?p=preview
Edit 2:
As with method 2, you could have a shared service, that provides this model:
plunker: https://plnkr.co/edit/EwdHFl7eZl2rd43UVG4J?p=preview
I have a parent directive in which its controller makes a call through a service to get some data.
sharedService.getData(options).then(function(data){
$scope.data = data;
});
Now i need this data in my child controller.
What i have already tried are the ff:
1) Through $timeout i get the data after sometime but it doesn't seem a good solution impacting performance
2) watchCollection() - i watched if newValue !== oldValue
problem being the data is huge so it takes a toll of performance
Now the issue i'm getting is the child directive gets executed after parent BUT before the data comes back from the service and i'm not able to get that data in my child directive via $scope.data.
Is there any solution to get data from parent directive to child directive when i have to wait for data to come in parent?
You can include your parent directive controller in your child directive by using require.
angular.module('myApp', [])
.directive('dirParent', function() {
return {
restrict: 'E',
scope: {},
controller: ['$scope', function($scope) {
}],
};
})
.directive('dirChild', function() {
return {
require: '^dirParent', // include directive controller
restrict: 'E',
scope: {
},
link: function(scope, element, attrs, paretCtrl) {
var data = paretCtrl.getMyData();
}
};
})
It's always a best to use service for communication and and business logic. Here is an example. Please check. This might solve your problem.
// Code goes here
angular.module('app', [])
.factory('messageService', function() {
return {
message: null
}
})
.directive('parentDir', function() {
return {
scope: {}, //isolate
template: '<input type="text" ng-model="PDirInput"/><button ng-click="send()">Send</button>',
controller: function($scope, messageService) {
$scope.send = function() {
messageService.message = $scope.PDirInput;
}
}
}
})
.directive('childDir', function() {
return {
scope: {}, //isolate
template: '<code>{{CDirInput.message}}</code>',
controller: function($scope, messageService) {
$scope.CDirInput = messageService;
}
}
})
<!DOCTYPE html>
<html ng-app="app">
<head>
<script data-require="angular.js#*" data-semver="2.0.0-alpha.31" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body>
<HR/>Parent Directive
<parent-dir></parent-dir>
<br/>
<HR/>Child Directive
<child-dir></child-dir>
<HR/>
</body>
</html>
Please consider the following code: it has a directive myItem with isolate scope. Each item will display a button that will call delete() on the directive controller. I'd like this to trigger a refresh in the outer controller (AppController). But of course the refresh() function can not be found, because of the isolated scope.
<html>
<body ng-app="question">
<div ng-cloak ng-controller="AppController">
<my-item ng-repeat="item in list" data="list">
</my-item>
<input type="text" maxlength="50" ng-model="new_item" />
<button ng-click="add(new_item)">+</button>
</div>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
<script>
(function () {
var app;
app = angular.module('question', []);
app.controller('AppController', [
'$scope', '$http', function ($scope, $http) {
$scope.list = [];
function refresh(){
$http.get('/api/items').then(
function(response){
$scope.list = response.data;
}
);
}
$scope.add = function(item){
$http.post('/api/items', { item: item }).then(refresh);
};
refresh();
}
]);
app.directive('myItem', function() {
return {
scope: {
item: '=data',
},
// directive template with delete button
template: '{{ item }} <button ng-click="delete(item)">-</button>',
restrict: 'E',
// directive controller with delete function
controller: [ '$scope', '$http', function($scope, $http) {
$scope.delete = function (card) {
// This is where it goes wrong! refresh does not exist
$http.delete('/api/items' + card.id).then(refresh);
}
}]
};
});
})();
</script>
</body>
</html>
One thing I could do is add ng-change to the myItem directive, but that would involve requiring ngModelController which seems overkill.
Other things I can think of:
Add something like onchange: '#' to the scope attribute of the directive, then set onchange = refresh in the html. Call the onchange expression instead of refresh inside the delete function. But this feels like I'm re-implementing ng-change?
Add require: '^AppController' to the directive. Then I guess I could call refresh on the parent controller directly. That seems like it violates loose coupling.
Don't use isolate scope at all. That would mean we inherit from the parent scope and refresh is available. But then my directive implicitly assumes that the scope will hold an item. Which also violates loose coupling, but in an implicit way.
So my question is: which is the correct way to let the parent controller know it should refresh its contents?
IMO, the first way would be the best way. The directive receives a function callback from outside which is executed by the directive when necessary. Like this the two directives are loosely coupled. It's similar to ng-change which is an attribute that is used by ng-model directive.
Example: Directive
app.directive('myItem', function() {
return {
restrict: 'E',
scope: {
item: '=data',
myItemDeleteCallback: '&myItemDeleteCallback'
},
template: '{{ item }} <button ng-click="delete(item)">-</button>',
controller: [ '$scope', '$http', function($scope, $http) {
$scope.delete = function (card) {
// This is where it goes wrong! refresh does not exist
$http.delete('/api/items' + card.id).then(function () {
$scope.myItemDeleteCallback();
});
}
}]
};
});
Usage: Controller
app.controller('AppController', ['$scope', '$http', function ($scope, $http) {
$scope.list = [];
$scope.refresh = function (){
$http.get('/api/items').then(
function(response){
$scope.list = response.data;
}
);
};
$scope.add = function(item){
$http.post('/api/items', { item: item })
.then($scope.refresh);
};
refresh();
}]);
Usage: Template
<div ng-cloak ng-controller="AppController">
<my-item my-item-delete-callback="refresh()" ng-repeat="item in list" data="list">
</my-item>
<input type="text" maxlength="50" ng-model="new_item" />
<button ng-click="add(new_item)">+</button>
</div>