AngularJS: Pause $digest & watchers on hidden DOM elements - angularjs

We're building a single page application which has multiple pages loaded as tabs. Only the content of one tab is visible at any given time (much like a browser), so we want to temporarily pause $digest and watchers from executing on those DOM nodes of the hidden tabs, until the user switches to that tab.
Is there a way to achieve this, so that the model continues to be updated for the background tabs, but the view updates based on a condition.
The following code illustrates the problem:
<div ng-repeat="tab in tabs" ng-show="tab.id == current_tab.id">
<!-- tab content with bindings -->
</div>
The goal is optimization.
I'm already aware of Scalyr directives, but I want a more specific solution without the extra features contained in Scalyr.

After some trial and error I've figured out the following directive which pauses all the children's $$watchers if the expression on the attribute evaluates to true, on false it restores any backed up $$watchers
app.directive('pauseChildrenWatchersIf', function(){
return {
link: function (scope, element, attrs) {
scope.$watch(attrs.pauseChildrenWatchersIf, function (newVal) {
if (newVal === undefined) {
return;
}
if (newVal) {
toggleChildrenWatchers(element, true)
} else {
toggleChildrenWatchers(element, false)
}
});
function toggleChildrenWatchers(element, pause) {
angular.forEach(element.children(), function (childElement) {
toggleAllWatchers(angular.element(childElement), pause);
});
}
function toggleAllWatchers(element, pause) {
var data = element.data();
if (data.hasOwnProperty('$scope') && data.$scope.hasOwnProperty('$$watchers') && data.$scope.$$watchers) {
if (pause) {
data._bk_$$watchers = [];
$.each(data.$scope.$$watchers, function (i, watcher) {
data._bk_$$watchers.push($.extend(true, {}, watcher))
});
data.$scope.$$watchers = [];
} else {
if (data.hasOwnProperty('_bk_$$watchers')) {
$.each(data._bk_$$watchers, function (i, watcher) {
data.$scope.$$watchers.push($.extend(true, {}, watcher))
});
}
}
}
toggleChildrenWatchers(element, pause);
}
}
}
});

Ok, the reason I asked you to show some code was because of the reason #Rouby stated.
For performance purposes, you can use ng-if instead of ng-show. ng-if removes or restores the element from the DOM.
<div ng-repeat="tab in tabs" ng-if="tab.id == current_tab.id">
<!-- tab content with bindings -->
</div>
ng-show is good to use when you want to style the hiding differently. For instance, you might want that a hidden element would only have its "body" hidden, with the header still appearing. It is possible with ng-show, you just have to define a CSS style for the class ng-hide.
If you want to keep the values of your $scope, you can bind those with a parent scope who would keep your variables intact.

Related

How to use a different combination of triggers for uib-popover?

The official documentation at :https://angular-ui.github.io/bootstrap/#/popover says that the following trigger combos can be passed as param to the popover-trigger attribute :
mouseenter: mouseleave
click: click
outsideClick: outsideClick
focus: blur
none
I want to use a combination of
mouseenter: outsideClick
How to achieve this without using the popover-is-open attribute?
You can't, the docs state
The outsideClick trigger will cause the popover to toggle on click, and hide when anything else is clicked.
"anything else" includes the element itself, so toggeling the element using outsideClick on or off and will interfere with the natural behavior of other triggers.
for example if state your triggers like so popover-trigger="mouseleave outsideClick"
, the trigger mouseleave will hide the popover instead of showing it if you have already clicked the element, otherwise it will just show it on leave. (plunk).
If you can hack it using popover-is-open then continue doing so, if it bothers you too much you can always request a feature.
popover-trigger="mouseenter outsideClick" for the uib-popover directive does not seem to work as one would think.
Initially, I thought it meant the following:
On mouse enter show the popover
On mouse leave hide the popover
On click keep popover open in an active state
On outside click close popover if it is in an active state
Since it does not I needed a manual approach, the following is stated in the documentation.
For any non-supported value, the trigger will be used to both show and hide the popover. Using the 'none' trigger will disable the internal trigger(s), one can then use the popover-is-open attribute exclusively to show and hide the popover.
So I created some HTML like:
<span class="glyphicon glyphicon-info-sign"
ng-class="{'text-primary' : isInfoPopoverClicked}"
ng-click="toggleInfoPopoverClicked()"
ng-mouseenter="enterInfoPopover()"
ng-mouseleave="leaveInfoPopover()"
custom-click-outside="closeInfoPopover()"
uib-popover-template="'info.html'"
popover-trigger="'none'"
popover-is-open="isInfoPopoverOpen()"
popover-placement="auto top"
popover-append-to-body="true" >
</span>
The JS in the controller:
// Toggle popover's clicked active state
$scope.toggleInfoPopoverClicked = function() {
$scope.isInfoPopoverClicked = !$scope.isInfoPopoverClicked;
};
// Close the popover, used for outside click and close action inside the template
$scope.closeInfoPopover = function() {
delete $scope.isInfoPopoverClicked;
};
// On mouse enter, show the popover
$scope.enterInfoPopover = function() {
$scope.isInfoPopoverMouseEnter = true;
};
// On mouse leave, close the popover.
// If clicked active state is false set to undefined.
// This supports when the user clicks the icon to close,
// that mouse enter does not immediately display the popover again.
$scope.leaveInfoPopover = function() {
$scope.isInfoPopoverMouseEnter = false;
if(false === $scope.isInfoPopoverClicked) {
delete $scope.isInfoPopoverClicked;
}
};
// Expression used in the popover-is-open attribute
$scope.isInfoPopoverOpen = function() {
if($scope.isInfoPopoverClicked) {
return true;
} else if(false === $scope.isInfoPopoverClicked){
return false;
}
return $scope.isInfoPopoverMouseEnter;
};
The template for the uib-popover-template I used:
<div custom-stop-event="click" class="pull-right">
<span ng-click="closeInfoPopover()" class="glyphicon glyphicon-remove"></span>
<section>{{info}}</section>
</div>
Now the trickier part was that this solution required me to create two more directives.
One to close the popover when clicking outside the element.
Another to stop the click event fired inside the pop-up. Preventing it from closing the popover.
The custom-click-outside directive:
angular.module('LSPApp').directive('customClickOutside', ['$document', function ($document) {
return {
restrict: 'A',
scope: {
clickOutside: '&customClickOutside'
},
link: function (scope, element) {
var handler = function (event) {
if (element !== event.target && !element[0].contains(event.target)) {
scope.$applyAsync(function () {
scope.clickOutside();
});
}
};
// Might not work on elements that stop events from bubbling up
$document.on('click', handler);
// Clean up event so it does not keep firing after leaving scope
scope.$on('$destroy', function() {
$document.off('click', handler);
});
}
};
}]);
The custom-stop-event directive called from the template's HTML:
angular.module('LSPApp').directive('stopEvent', function () {
return {
restrict: 'A',
link: function (scope, element, attr) {
element.on(attr.stopEvent, function (e) {
e.stopPropagation();
});
}
};
});
Hopefully, this helps someone, my final solution had all this encapsulated in it's own directive to promote reuse.

Using $compile to manipulate angular directives from a custom directive

I'm creating a directive which would disable all elements inside the element to which it is applied. For simplicity, lets assume only buttons are disabled. In the directive link function I'm just setting the disabled attribute for the elements to be disabled. This is working fine, but the problem comes in scenarios where some of the buttons have ng-disabled attributes, In such cases, the buttons become disabled/enabled based on the condition in ng-disabled, thus ignoring the directive logic.
So I thought of making use of $compile and fix it with the following approach:
When buttons need to be disabled,
1) Add disabled attribute to the buttons.
2) Check if buttons have 'ng-disabled' attribute. If so,keep its value in the same DOM element under a different attribute name.
3) Delete the ng-disabled attribute.
4) Recompile to reflect the change in attributes (So there will not be any ngDisabled checks on recompilation).
When buttons are re-enabled,
1) Remove disabled attributes
2) Check for attribute created in step (2) of above and add it to the ng-disabled attribute
3) Remove the backup attribute
4) Recompile
But this is not working.The ngDisabled conditions are still evaluated even when the attribute is not there.
Directive Code:
function disableElements($compile) {
return {
restrict: "A",
scope: {
disableElements: "="
},
link: function (scope, elem, attr) {
scope.$watch('disableElements', function (newVal) {
var buttons;
var ngDisabled;
var backup;
buttons = elem.find('button');
if (newVal) {
buttons.attr('disabled', 'disabled');
for (var i = 0, j = buttons.length; i < j; i++) {
ngDisabled = $(buttons[i]).attr('ng-disabled');
if (typeof ngDisabled !== typeof undefined && ngDisabled !== false) {
$(buttons[i]).attr('backup', ngDisabled).removeAttr('ng-disabled');
}
}
$compile(elem.contents())(scope);
} else {
buttons.removeAttr('disabled');
for (var i = 0, j = buttons.length; i < j; i++) {
backup = $(buttons[i]).attr('backup');
if (typeof backup !== typeof undefined && backup !== false) {
$(buttons[i]).attr('ng-disabled', backup).removeAttr('backup');
}
}
$compile(elem.contents())(scope);
}
});
}
};
}
Sample View and Controller
<input type="text" ng-model="vm.val">
<div disable-elements="vm.disableAll">
<button type="button" ng-disabled="vm.val.length===0" ng-click="vm.buttonClicked()">With ng disabled</button>
<button type="button" ng-click="vm.buttonClicked()">Without ng disabled</button>
</div>
<label><input type="checkbox" ng-model="vm.disableAll">Disable all</label>
function SampleController() {
this.disableAll = true;
this.val = '';
this.buttonClicked = function () {
alert("click");
}
}
jsfiddle here
Please let me know what is wrong with this code or if it is the right approach to this issue.
re-compiling the content doesnt unlink the previously compiled directive.
There is, actually, no easy way to do what you wanna do. The only correct approach will be to stop using ng-disable to use your own custom-disable directive which would be aware of it's parent directive "globallyDisabled" state.

AngularJS scroll directive - How to prevent re-rendering whole scope

I have an AngularJS Application with a scroll directive implemented as the following:
http://jsfiddle.net/un6r4wts/
app = angular.module('myApp', []);
app.run(function ($rootScope) {
$rootScope.var1 = 'Var1';
$rootScope.var2 = function () { return Math.random(); };
});
app.directive("scroll", function ($window) {
return function(scope, element, attrs) {
angular.element($window).bind("scroll", function() {
if (this.pageYOffset >= 100) {
scope.scrolled = true;
} else {
scope.scrolled = false;
}
scope.$apply();
});
};
});
The HTML looks the following:
<div ng-app="myApp" scroll ng-class="{scrolled:scrolled}">
<header></header>
<section>
<div class="vars">
{{var1}}<br/><br/>
{{var2()}}
</div>
</section>
</div>
I only want the class scrolled to be added to the div once the page is scrolled more than 100px. Which is working just fine, but I only want that to happen! I don't want the whole scope to be re-rendered. So the function var2() should not be executed while scrolling. Unfortunately it is though.
Is there any way to have angular only execute the function which is bound to the window element without re-rendering the whole scope, or am I misunderstanding here something fundamentally to AngularJS?
See this fiddle:
http://jsfiddle.net/un6r4wts/
Edit:
This seems to be a topic about a similar problem:
Angularjs scope.$apply in directive's on scroll listener
If you want to calculate an expression only once, you can prefix it with '::', which does exactly that. See it in docs under One-time binding:
https://docs.angularjs.org/guide/expression
Note, this requires angular 1.3+.
The reason that the expressions are calculated is because when you change a property value on your scope, then dirty check starts and evaluates all the watches for dirty check. When the view uses {{ }} on some scope variable, it creates a binding (which comes along with a watch).

Applying animation outside of the ngApp

My goal is to create a directive that can be applied to a given element, so that when the element is clicked, a modal is created. I would like to have the modal created and appended to the body node, which is outside of my ng-app element. Due to requirements of more than one app on a page, I can't put ng-app on the <html> or <body> tags. Yet for proper z positioning, I would to place the modal element as high up in the body as I can.
My directive looks like this:
var module = angular.module('Test', ['ngAnimate']);
module.directive('modal', function($compile, $animate) {
function link(scope, element, attr) {
element.on('click', function () {
var modal = $compile('<div class="modal"></div>')(scope);
scope.$apply(function () {
$animate.enter(modal, angular.element(document.body));
});
});
}
return {
link: link,
scope: {}
};
});
When I use $animate.enter to append the modal to the body, it is appended but the animation does not run. My HTML looks like this:
<body>
<div ng-app="Test">
<button modal>Open Modal</button>
</div>
</body>
If I move the ng-app from the div to the body, then the animation works. But I can't do this because I need to have the option of placing more than one ng-app on a given page.
Is it possible?
Working (or not-working) example:
http://plnkr.co/edit/vUi2PmLjea36nrJ9i3R2?p=preview
The short answer is: No you can't.
(At least Angular seems not to be designed to allow it).
The somewhat longer answer is: No, you can't. Here is why:
The reason is how $animate is currently implemented:
In your case, it adds the elements= to the DOM (to document.body in particular) and then checks to see if it should proceed with the animation specific stuff (or if animations are "disabled").
According to the source code, the function that checks if animations are "disabled" is:
function animationsDisabled(element, parentElement) {
if (rootAnimateState.disabled) return true;
if(isMatchingElement(element, $rootElement)) {
return rootAnimateState.disabled || rootAnimateState.running;
}
do {
//the element did not reach the root element which means that it
//is not apart of the DOM. Therefore there is no reason to do
//any animations on it
if(parentElement.length === 0) break;
var isRoot = isMatchingElement(parentElement, $rootElement);
var state = isRoot ? rootAnimateState : parentElement.data(NG_ANIMATE_STATE);
var result = state && (!!state.disabled || state.running || state.totalActive > 0);
if(isRoot || result) {
return result;
}
if(isRoot) return true;
}
while(parentElement = parentElement.parent());
return true;
}
As you can see, since your modal is not a child (but a sibling) of the $rootElement, isRoot will always be false and the do-while loop will run until there is no parent-element. At that point if(parentElement.length === 0) break; will break the loop and the function will return true, thus cancelling the animation.
The longest answer is: Depends (on how badly you need it).
The available options (I can think of) are:
Accept your fate and find some other way (besides $animate) to perform your animations.
Create your own fork of angular-animate and change one line of code, so that animations are not cancelled even if the target-element is not a child of the $rootElement.
If you like to live dangerously: What if we "temporarily swapped the $rootElement" ?
I tried various approaches (which outside of the scope of this answer) and the only one I could make work(?), was swapping the HTML element associated with the jqLite object (i.e. $rootElement[0]).
I wrapped the functionality in a service (along with some convenience features, e.g. restoring the original element after a set period of time):
module.factory('myRootElement', function ($rootElement, $timeout) {
/* Save the original element for reference */
var original = $rootElement[0];
/* Fake the $rootElement for the specified
* period of time (in milliseconds) */
function fakeForMillis(millis) {
$rootElement[0] = document.body;
$timeout(function () {
$rootElement[0] = original;
}, millis || 0);
}
/* Return the Service object */
return {
fakeForMillis: fakeForMillis
};
});
Finally, you only need to temporarily swap the $rootElement fo the animation to take place. (Unfortunately, you have to specify the time required for the animation, but I bet there are better ways to find it out programmatically - again outside the scope of this answer.)
myRootElement.fakeForMillis(1000);
$animate.enter(modal, angular.element(document.body));
See, also, this short demo.
I have no idea what I am talking about and I have by no means investigated the consequences of this approach, so use at your own risk and don't be surprised if strange things come your way.

Pro/con of using Angular directives for complex form validation/ GUI manipulation

I am building a new SPA front end to replace an existing enterprise's legacy hodgepodge of systems that are outdated and in need of updating. I am new to angular, and wanted to see if the community could give me some perspective. I'll state my problem, and then ask my question.
I have to generate several series of check boxes based on data from a .js include, with data like this:
$scope.fieldMappings.investmentObjectiveMap = [
{'id':"CAPITAL PRESERVATION", 'name':"Capital Preservation"},
{'id':"STABLE", 'name':"Moderate"},
{'id':"BALANCED", 'name':"Moderate Growth"},
// etc
{'id':"NONE", 'name':"None"}
];
The checkboxes are created using an ng-repeat, like this:
<div ng-repeat="investmentObjective in fieldMappings.investmentObjectiveMap">
...
</div>
However, I needed the values represented by the checkboxes to map to a different model (not just 2-way-bound to the fieldmappings object). To accomplish this, I created a directive, which accepts a destination array destarray which is eventually mapped to the model. I also know I need to handle some very specific gui controls, such as unchecking "None" if anything else gets checked, or checking "None" if everything else gets unchecked. Also, "None" won't be an option in every group of checkboxes, so the directive needs to be generic enough to accept a validation function that can fiddle with the checked state of the checkbox group's inputs based on what's already clicked, but smart enough not to break if there is no option called "NONE". I started to do that by adding an ng-click which invoked a function in the controller, but in looking around stack overflow, I read people saying that its bad to put DOM manipulation code inside your controller - it should go in directives. So do I need another directive?
So far:
(html):
<input my-checkbox-group
type="checkbox"
fieldobj="investmentObjective"
ng-click="validationfunc()"
validationfunc="clearOnNone()"
destarray="investor.investmentObjective" />
Directive code:
.directive("myCheckboxGroup", function () {
return {
restrict: "A",
scope: {
destarray: "=", // the source of all the checkbox values
fieldobj: "=", // the array the values came from
validationfunc: "&" // the function to be called for validation (optional)
},
link: function (scope, elem, attrs) {
if (scope.destarray.indexOf(scope.fieldobj.id) !== -1) {
elem[0].checked = true;
}
elem.bind('click', function () {
var index = scope.destarray.indexOf(scope.fieldobj.id);
if (elem[0].checked) {
if (index === -1) {
scope.destarray.push(scope.fieldobj.id);
}
}
else {
if (index !== -1) {
scope.destarray.splice(index, 1);
}
}
});
}
};
})
.js controller snippet:
.controller( 'SuitabilityCtrl', ['$scope', function ( $scope ) {
$scope.clearOnNone = function() {
// naughty jQuery DOM manipulation code that
// looks at checkboxes and checks/unchecks as needed
};
The above code is done and works fine, except the naughty jquery code in clearOnNone(), which is why I wrote this question.
And here is my question: after ALL this, I think to myself - I could be done already if I just manually handled all this GUI logic and validation junk with jQuery written in my controller. At what point does it become foolish to write these complicated directives that future developers will have to puzzle over more than if I had just written jQuery code that 99% of us would understand with a glance? How do other developers draw the line?
I see this all over stack overflow. For example, this question seems like it could be answered with a dozen lines of straightforward jQuery, yet he has opted to do it the angular way, with a directive and a partial... it seems like a lot of work for a simple problem.
I don't want this question to violate the rules, so specifically, I suppose I would like to know: how SHOULD I be writing the code that checks whether "None" has been selected (if it exists as an option in this group of checkboxes), and then check/uncheck the other boxes accordingly? A more complex directive? I can't believe I'm the only developer that is having to implement code that is more complex than needed just to satisfy an opinionated framework. Is there another util library I need to be using?
I posted this on Programmers.StackExchange.com as per Jim's suggestion. In the meantime, I settled on a solution for handling all the tricky DOM manipulations.
I tried it both ways - handling the DOM event in the controller, and handling it via a directive:
(Via Controller) - .js code:
$scope.clearOnNone = function(groupName, $event) {
var chkboxArr = $('input[name^=' + groupName + ']'),
nonNoneValChecked = false,
targetElem = null,
labelText = "";
// get the target of the click event by looking at the <label> sibling's text
targetElem = event.target.nextElementSibling.textContent.trim();
// if target was the None option, uncheck all others
if (targetElem === "None") {
chkboxArr.each(function() {
labelText = this.nextElementSibling.textContent.trim();
if (labelText !== "None") {
this.checked = false;
}
});
}
// if the target was anything BUT the None option, uncheck None
else {
chkboxArr.each(function() {
labelText = this.nextElementSibling.textContent.trim();
if (labelText === "None") {
this.checked = false;
}
});
}
};
(Via Controller) - html code:
<div ng-repeat="investmentObjective in fieldMappings.secondaryInvestmentObjectiveMap">
<input checkbox-group
type="checkbox"
name="secondaryInvestmentObjective"
ng-click="validationfunc('secondaryInvestmentObjective', $event)"
validationfunc="clearOnNone('secondaryInvestmentObjective', $event)"
fieldobj="investmentObjective"
destarray="suitabilityHolder.suitability.secondaryInvestmentObjective" />
<label class="checkbox-label"
popover-title="{{investmentObjective.name}}"
popover="{{investmentObjective.help}}"
popover-trigger="mouseenter">{{investmentObjective.name}}
</label>
</div>
(Via Controller) - directive code:
.directive("checkboxGroup", function () {
return {
restrict: "A",
scope: {
destarray: "=", // the source of all the checkbox values
fieldobj: "=", // the array the values came from
validationfunc: "&" // the function to be called for validation (optional)
},
link: function (scope, elem, attrs) {
if (scope.destarray.indexOf(scope.fieldobj.id) !== -1) {
elem[0].checked = true;
}
elem.bind('click', function () {
var index = scope.destarray.indexOf(scope.fieldobj.id);
if (elem[0].checked) {
if (index === -1) {
scope.destarray.push(scope.fieldobj.id);
}
}
else {
if (index !== -1) {
scope.destarray.splice(index, 1);
}
}
});
}
};
})
I then decided that I hated the event.target.nextElementSibling.textContent.trim() lines... I feel like I should be double checking that all of those methods exist, or using a try/catch. So I rewrote the directive to include the logic from the controller:
(via directive) - html code:
<div ng-repeat="otherInvestment in fieldMappings.otherInvestmentsMap">
<input type="checkbox"
checkbox-group
groupname="otherInvestment"
labelvalue="{{otherInvestment.name}}"
fieldobj="otherInvestment"
destarray="suitabilityHolder.suitability.otherInvestment" />
<label class="checkbox-label"
popover-title="{{otherInvestment.name}}"
popover="{{otherInvestment.help}}"
popover-trigger="mouseenter">{{otherInvestment.name}}
</label>
</div>
(via directive) - directive code:
.directive("checkboxGroup", function () {
return {
restrict: "A",
scope: {
destarray: "=", // the source of all the checkbox values
fieldobj: "=", // the array the values came from
groupname: "#", // the logical name of the group of checkboxes
labelvalue: "#" // the value that corresponds to this checkbox
},
link: function (scope, elem, attrs) {
// Determine initial checked boxes
// if the fieldobj.id exists in the destarray, check this checkbox
if (scope.destarray.indexOf(scope.fieldobj.id) !== -1) {
elem[0].checked = true;
}
// Update array on click
elem.bind('click', function () {
// store the index where the fieldobj.id exists in the destarray
var index = scope.destarray.indexOf(scope.fieldobj.id),
// get the array of checkboxes that form this checkbox group
chkboxArr = $('input[groupname^=' + scope.groupname + ']');
// Add if checked
if (elem[0].checked) {
if (scope.labelvalue === "None") {
// loop through checkboxes and uncheck all the ones that are not "None"
chkboxArr.each(function() {
// have to noodle through the checkbox DOM element to get at its attribute list
// - is there a cleaner way?
var tmpLabelValue = this.attributes.labelvalue.nodeValue.trim();
if (tmpLabelValue !== "None") {
this.checked = false;
}
});
}
// if the target was anything BUT the None option, uncheck None
else {
chkboxArr.each(function() {
var tmpLabelValue = this.attributes.labelvalue.nodeValue.trim();
if (tmpLabelValue === "None") {
this.checked = false;
}
});
}
if (index === -1) {
// add the id to the end of the dest array
// **will not maintain original order if several are unchecked then rechecked**
scope.destarray.push(scope.fieldobj.id);
}
}
// Remove if unchecked
else {
if (index !== -1) {
scope.destarray.splice(index, 1);
}
}
});
}
};
})
In retrospect, I suppose I prefer to house all the code in a directive, even though I think it's less intuitive and more complex than tossing all the handling in the controller via jQuery. It cuts out the clearOnNone() function from the controller, meaning that all the code that deals with this functionality it in the html markup and the directive.
I am not a fan of code like this.attributes.labelvalue.nodeValue.trim(), which I still ended up with in my directive. For scenarios like mine, where the business unit has certain requirements that (no other way to put it) are tedious and cumbersome, I don't know that there is really a 'clean' way to code it all up.
I'm still new to AngularJS but I this case I think I would solve it by using either an ng-click handler or $scope.$watch to update the state of the NONE model whenever the other models change.
Using ng-click
I've whipped up a jsFiddle that shows how it could work with ng-click:
http://jsfiddle.net/Dzj6K/1/
HTML:
<div ng-controller="myCtrl">
<div ng-repeat="objective in objectives">
<label><input type="checkbox" ng-model="objective.selected" ng-click="click(objective)" /> {{objective.name}}</label>
</div>
</div>
JavaScript:
var app = angular.module('myApp', []);
function myCtrl($scope) {
$scope.objectives = [
{'id':"CAPITAL PRESERVATION", 'name':"Capital Preservation"},
{'id':"STABLE", 'name':"Moderate"},
{'id':"BALANCED", 'name':"Moderate Growth"},
{'id':"NONE", 'name':"None"}
];
$scope.click = function(objective) {
if (objective.id === "NONE") {
if (objective.selected) {
angular.forEach($scope.objectives, function(objective) {
if (objective.id !== "NONE") {
objective.selected = false;
}
});
}
} else {
angular.forEach($scope.objectives, function(objective) {
if (objective.id === "NONE") {
objective.selected = false;
}
});
}
};
}
Using $scope.$watch
And a version of the jsFiddle that shows how it could work with $scope.$watch:
http://jsfiddle.net/Dzj6K/
HTML:
<div ng-controller="myCtrl">
<div ng-repeat="objective in objectives">
<label><input type="checkbox" ng-model="objective.selected" /> {{objective.name}}</label>
</div>
</div>
JavaScript:
var app = angular.module('myApp', []);
function myCtrl($scope) {
$scope.objectives = [
{'id':"CAPITAL PRESERVATION", 'name':"Capital Preservation"},
{'id':"STABLE", 'name':"Moderate"},
{'id':"BALANCED", 'name':"Moderate Growth"},
{'id':"NONE", 'name':"None"}
];
$scope.$watch('objectives', function() {
var anySelected = false;
var noneModel = null;
angular.forEach($scope.objectives, function(objective) {
if (objective.id === "NONE") {
noneModel = objective;
} else {
anySelected = anySelected || objective.selected;
}
});
if (noneModel) {
noneModel.selected = !anySelected;
}
}, true);
}

Resources