Angular 1.x Directive With A Template - angularjs

I'm trying to create an angular directive for forming sentences. The goal is to take a list and iterate through them as necessary. The result of the directive would be something like:
shoes, pants and socks
or
shoes, pants and +5 more
I have the basic directive setup to work with a array of strings - but I'd like to customize it to allow custom templates for each sentence element (i.e. hyperlinks, styling, etc). That is:
<sentence values="article in articles">
<strong>{{article.title}}</strong> by <span>{{article.author}}</span>
</sentence>
The HTML the user sees in the browser needs to be something like:
$scope.articles = [
{ title: '...', author: '...'},
{ title: '...', author: '...'},
...
]
<span><strong>ABC</strong> by <span>123</span></span>
<span>, </span>
<span><strong>DEF</strong> by <span>456</span></span>
<span>and</span>
<span>+5 more</span>
I'm guessing it has something to do with transclude but cannot figure out the API. I've also experimented with using ng-repeat instead of the directive template but wasn't able to find a solution.

Something like this should work where maxArticles is a number defined on your scope
<sentence values="article in articles | limitTo: maxArticles">
<strong>{{article.title}}</strong> by <span>{{article.author}}</span>
<span ng-if="$index < maxArticles - 2">, </span>
<span ng-if="$index === articles.length - 1 && articles.length <= maxArticles">and</span>
</sentence>
<span ng-if="articles.length > maxArticles">
and +{{articles.length - maxArticles}} more.
</span>

Iterating AND providing dynamic content is a common use for a custom directive with the compile function + the $compile service. Watch out: essentially you are repeating the functionality of ng-repeat, you may want to consider alternatives.
E.g. instead of the articles list, use another one (perhaps named articlesLimited). The new list is constructed dynamically and contains the first elements from articles. A flag (e.g. hasMore) indicates whether the original articles has more elements, simply as: $scope.hasMore = articles.length > 5. You use the hasMore flag to show/hide the "+N more" message.
For what it's worth however, below is an implementation of the sentence directive. See the comment for weak points!
app.directive('sentence', ['$compile', function($compile) {
var RE = /^([a-z_0-9\$]+)\s+in\s([a-z_0-9\$]+)$/i, ONLY_WHITESPACE = /^\s*$/;
function extractTrimmedContent(tElem) {
var result = tElem.contents();
while( result[0].nodeType === 3 && ONLY_WHITESPACE.test(result[0].textContent) ) {
result.splice(0, 1);
}
while( result[result.length-1].nodeType === 3 && ONLY_WHITESPACE.test(result[result.length-1].textContent) ) {
result.length = result.length - 1;
}
return result;
}
function extractIterationMeta(tAttrs) {
var result = RE.exec(tAttrs.values);
if( !result ) {
throw new Error('malformed values expression, use "itervar in list": ', tAttrs.values);
}
var cutoff = parseInt(tAttrs.cutoff || '5');
if( isNaN(cutoff) ) {
throw new Error('malformed cutoff: ' + tAttrs.cutoff);
}
return {
varName: result[1],
list: result[2],
cutoff: cutoff
};
}
return {
scope: true, // investigate isolated scope too...
compile: function(tElem, tAttrs) {
var iterationMeta = extractIterationMeta(tAttrs);
var content = $compile(extractTrimmedContent(tElem));
tElem.empty();
return function link(scope, elem, attrs) {
var scopes = [];
scope.$watchCollection(
function() {
// this is (IMO) the only legit usage of scope.$parent:
// evaluating an expression we know is meant to run in our parent
return scope.$parent.$eval(iterationMeta.list);
},
function(newval, oldval) {
var i, item, childScope;
// this needs OPTIMIZING, the way ng-repeat does it (identities, track by); omitting for brevity
// if however the lists are not going to change, it is OK as it is
scopes.forEach(function(s) {
s.$destroy();
});
scopes.length = 0;
elem.empty();
for( i=0; i < newval.length && i < iterationMeta.cutoff; i++ ) {
childScope = scope.$new(false, scope);
childScope[iterationMeta.varName] = newval[i];
scopes.push(childScope);
content(childScope, function(clonedElement) {
if( i > 0 ) {
elem.append('<span class="sentence-sep">, </span>');
}
elem.append(clonedElement);
});
}
if( newval.length > iterationMeta.cutoff ) {
// this too can be parametric, leaving for another time ;)
elem.append('<span class="sentence-more"> +' + (newval.length - iterationMeta.cutoff) + ' more</span>');
}
}
);
};
}
};
}]);
And the fiddle: https://jsfiddle.net/aza6u64p/

This is a tricky problem. Transclude is used to wrap elements but when using transclude you don't have access to the directive scope, only to the scope of where the directive is being used:
AnglularJS: Creating Custom Directives
What does this transclude option do, exactly? transclude makes the contents of a directive with this option have access to the scope outside of the directive rather than inside.
So a solution is to create another component to inject the template's scope inside the directive, like this:
.directive('myList', function() {
return {
restrict: 'E',
transclude: true,
scope: { items: '=' },
template: '<div ng-repeat="item in items" inject></div>'
};
})
.directive('inject', function() {
return {
link: function($scope, $element, $attrs, controller, $transclude) {
$transclude($scope, function(clone) {
$element.empty();
$element.append(clone);
});
}
};
})
<my-list items="articles">
<strong>{{item.title}}</strong> by <span>{{item.author}}</span>
</my-list>
This was taken from this discussion: #7874
And I made a Plnkr.

Related

Angular directive to detect scrolling within an ng-repeat container

In my Angular app, I've created a simple directive to do something on scroll, based on the answer provided to a similar question here.
Here is the code to my onScroll directive:
(function() {
'use strict';
angular
.module('proverbial')
.directive('onScroll', Directive);
Directive.$inject = [ '$document' ];
function Directive($document) {
var directive = {
link: link,
restrict: 'A'
};
return directive;
function link(scope, element, attrs) {
element.bind('scroll', function(e) {
console.warn(e)
if (element[ 0 ].scrollTop + element[ 0 ].offsetHeight >= element[ 0 ].scrollHeight) {
scope.$apply(attr.onScroll);
}
});
}
}
})();
This is to be used in the following HTML template:
<div class="absolute" on-scroll="vm.totalDisplayed += 100">
<span class="card hover"
ng-repeat="proverb in vm.proverbs | limitTo: vm.totalDisplayed">
<a>{{proverb.text | maxLength: 90}}</a>
</span>
</div>
The intention is to keep increasing vm.totalDisplayed on page scroll, to create an effect similar to lazy-loading.
However, when I scroll down to the end of what has been already loaded with ng-repeat, nothing happens. The console.warn(e) never triggers.
On the browser console I can see that the containing <div class="absolute" on-scroll="vm.totalDisplayed += 100"> does indeed scroll up past the browser's window.
What's missing?
You have a minor mistake in your code:
if (element[ 0 ].scrollTop + element[ 0 ].offsetHeight >= element[ 0 ].scrollHeight) {
scope.$apply(attr.onScroll);
}
attr is undefined. Should be attrs.
Furthermore, I can't reproduce the problem. I've created a Plunker from your code and the warning shows just fine. Only thing what I find weird is that I couldn't get vm.totalDisplayed += 100 to work. vm.totalDisplayed = vm.totalDisplayed + 100 worked though.
http://plnkr.co/edit/ooMFG4ZAZzxJf9zuChyA
Despite #Tom pointing out correctly that I had a syntatic error in my directive, it turns out that that wasn't the issue.
The issue was that the element to which I attached my directive was not actually being scrolled due to its CSS styling, so I had to tweak the code slightly, by binding scroll to $document instead:
( function () {
'use strict';
var angular = window.angular || {};
angular
.module( 'proverbial' )
.directive( 'onDocumentScroll', Directive );
Directive.$inject = ['$document'];
function Directive( $document ) {
var directive = {
link: link,
restrict: 'A'
};
return directive;
function link( scope, element, attrs ) {
$document.bind( 'scroll', function () {
if ( $document.scrollTop() - scope.vm.latestThreshold >= element[0].offsetHeight ) {
scope.vm.latestThreshold += $document.scrollTop();
scope.$apply( attrs.onDocumentScroll );
}
} );
}
}
} )();
This solved my problem.

Angular Directive: use $watch or pass in binded value

I have a controller which calls a service returning a list of courses.
Each course has an property called 'percentageDone'
I want to assign some CSS to my div depending on this value.
there are 2 ways I have found to do this. There are probably more, I am a newbie at Angular. Thing is I am not sure what is the best way. In terms of performance and general best Angular practice. I realize I could use ng-class, but I am using this exercise to learn Angular.
So my Div
<div ng-repeat="e in courses">
<div completion-color="{{e.percentageDone}}"></div>
</div>
with this directive
myApp.directive('completionColor', function () {
return {
restrict: 'A',
link: function (scope, el, attrs) {
var donePerc = attrs['completionColor'];
if (donePerc < 33) {
el.addClass('progress-bar-danger');
}
else if (donePerc >= 33 && newVal < 66) {
el.addClass('progress-bar-warning');
}
else if (donePerc >= 66) {
el.addClass('progress-bar-success');
}
}
};
});
or another way
<div completion-color="e.percentageDone"></div>
and js
myApp.directive('completionColor', function () {
return {
restrict: 'A',
link: function (scope, el, attrs) {
scope.$watch(attrs['completionColor'], function (newVal) {
if (newVal < 33) {
el.addClass('progress-bar-danger');
}
else if (newVal >= 33 && newVal < 66) {
el.addClass('progress-bar-warning');
}
else if (newVal >= 66) {
el.addClass('progress-bar-success');
}
});
}
};
});
I like that I don't have to put the {{}} in my directive, but in general the data will not change much. So not sure I need to use a $.watch.
I'd say $watch is the last thing you should use here.
I myself see no problem with using {{}}, that's what they are for. But you have another option if you want, you could use One Time Binding (https://docs.angularjs.org/guide/expression) if you're using angular 1.3.X and that way you're improving performance, since angular doesn't keep watch for that:
<div ng-repeat="e in courses">
<div completion-color="{{::e.percentageDone}}"></div>
</div>
And work with a scope in your directive (you can use isolated as well):
scope: {
completionColor: "#"
},
Now you can use scope.completionColor in your link function.

AngularJS : directive two way data binding not working

I have a controller with following code snippet,
...
$scope.selected_contents = [];
$scope.$watch('selected_contents', function (sel_contents) {
console.log(sel_contents, 'selected contents');
}, true);
...
a directive,
commonDirectives.directive('chkbox', function() {
return {
restrict: 'A',
require: '?ngModel',
scope : {
item : '=item',
selection_pool: '=selectionPool'
},
link: function(scope, elem, attrs, ngModel) {
console.log('selected contents are', scope.selection_pool);
// watch selection_pool
scope.$watch('selection_pool', function (pool) {
console.log(pool, scope.selection_pool, 'pool updated');
if (_.contains(pool, scope.item)) {
elem.prop('checked', true);
}
else {
elem.prop('checked', false);
}
});
// toggle the selection of this component
var toggle_selection = function () {
if(_.indexOf(scope.selection_pool, scope.item) != -1) {
scope.selection_pool = _.without(scope.selection_pool , scope.item);
}
else {
scope.selection_pool.push(scope.item);
}
};
elem.on('click', toggle_selection);
}
};
});
and a template which uses the directive,
<tr ng-repeat="content in contents">
<td><input type="checkbox" selection_pool="selected_contents" item="content" chkbox></td>
</tr>
The problem is, changes in selection_pool in the directive is not reflected to selected_contents in the controller. What am i missing?
Update 1:
Following the suggestion from #mohamedrias I wrapped the changes in scope with scope.$apply. Doing so updates selected_contents in controller only while adding the content but not while removing it.
...
// toggle the selection of this component
var toggle_selection = function () {
if(_.indexOf(scope.selection_pool, scope.item) != -1) {
scope.$apply(function () {
scope.selection_pool = _.without(scope.selection_pool , scope.item);
});
}
else {
scope.$apply(function () {
scope.selection_pool.push(scope.item);
});
}
};
...
Angular uses name-with-dashes for attribute names and camelCase for
the corresponding directive name
From here.
The variable should be changed from this selection_pool:
<input type="checkbox" selection_pool="selected_contents" item="content" chkbox>
to selection-pool:
<input type="checkbox" selection-pool="selected_contents" item="content" chkbox>
And this selectionPool into the directive:
scope : {
item : '=item',
selectionPool: '=selectionPool'
}
EDIT: Because the selectionPool is an array, you should use $watchCollection:
scope.$watchCollection('selectionPool', function (pool)
And when you add/remove values from the array in toggle_selection function, should be wrapped within the $timeout function:
$timeout(function () {
if (_.indexOf(scope.selectionPool, scope.item) != -1) {
scope.selectionPool = _.without(scope.selectionPool, scope.item);
} else {
scope.selectionPool.push(scope.item);
}
});
This is to assure that a digest cycle is going to be applied afterwards.
Here's the code working on a jsfiddle: http://jsfiddle.net/0rvcguz0/3/
After researching for entire day, I ended up here. If someone is having any trouble with Angularjs scope, I highly encourage to read it.
The proper solution in my case was to wrap selected_contents by an object. e.g.
$scope.selected = {};
$scope.selected.contents = [];
Then in the template replace selcted_contents with selected.contents.
But still what I don't understand is, [] or an Array is also an object. My earlier code should have worked according to the information I found in the wiki. If anyone could explain me why I would really appreciate it :).

Angularjs TreeView Directive With Isolated Scope

I am trying to create treeview directive with isolated scope.
But node selection not working properly:
Directive
(function(l) {
l.module("angularTreeview", []).directive("treeModel",
function($compile) {
return {
restrict: "A",
scope: {
treeModel: '=',
currentNode: '='
},
link: function(scope, element) {
k = '<ul><li data-ng-repeat="node in treeModel"><i class="collapsed" data-ng-show="node.children.length && node.collapsed" data-ng-click="selectNodeHead(node)"></i><i class="expanded" data-ng-show="node.children.length && !node.collapsed" data-ng-click="selectNodeHead(node)"></i><i class="normal" data-ng-hide="node.children.length"></i> <span data-ng-class="node.selected" data-ng-click="selectNodeLabel(node)">{{node.roleName}}</span><div data-ng-hide="node.collapsed" data-tree-model="node.children" data-node-id="roleId" data-node-label="roleName" data-current-node="currentNode" data-node-children="children"></div></li></ul>';
scope.$watch('treeModel', function() {
element.empty().html($compile(k)(scope))
}),
scope.selectNodeLabel = function(q) {
scope.currentNode && scope.currentNode.selected && (scope.currentNode.selected = false);
q.selected = "selected";
scope.currentNode = q
};
}
}
})
})(angular);
Problem Fiddle
Any help would appreciated, Thanks.
Problem is because you compile the directive multiple times, you have different child scopes for each time you compile the directive. Now it's not a real problem but every child node change to currentNode changes the reference but does not update all of its parents scopes, meaning the original currentNode is not updated.
Instead you can access the currentNode through an object which will guarantee that every change will keep the referencs as they are:
// controller
$scope.obj = { currentNode :{} };
// directive
scope.currentNode.currentNode=q; // every line with scope.currenNode becomes scope.currentNode.currentNode for example.
See this fiddle

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