Disable remaining checkboxes when n checkboxes have been checked with AngularJS - angularjs

I'm just starting to learn AngularJS, so bear with me.
I have an app with a list that's bound to an array of objects (D&D spells). That list displays all the contents of the objects, by binding to the various values of the object. They filter on the value of a search textbox (query) and are sorted by two properties of the spell objects (level and name). That all works fine.
<div ng-repeat="(spellKey, spellValue) in spells | groupBy:'level'">
<a id="level-{{spellKey}}" class="anchor"></a>
<div ng-repeat="spell in spellValue | filterEach:query | orderBy:['level','name']">
<a id="spell-{{spell.name | replaceSpaces}}" class="anchor"></a>
...
</div>
</div>
I have a second list that I put in my navbar that's bound to the same array. It displays just the names and a checkbox next to each name; the checkbox determines whether or not to list each object in the first list (by setting spell.prepared to true or false). This also works fine.
<div class="navbar-header">
<form class="form-inline navbar-left col-xs-10" role="search">
...
</form>
<div class="col-xs-2">
<!-- button that displays list of spell links and checkboxes -->
<button type="button" id="btn-toggle-navbar" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#spell-nav">
...
</button>
</div>
<div class="collapse navbar-collapse" id="spell-nav">
<div class="checkbox" style="padding-bottom: 4px;">
<label>
<input type="checkbox" id="cbHideNonPrepared" ng-model="hideNonPrepared" />
Hide non-prepared
</label>
</div>
<div ng-repeat="(linkKey, linkValue) in spells | groupBy:'level'">
...
<ul style="list-style-type: none">
<li ng-repeat="spellLink in linkValue" class="checkbox">
<label>
<input type="checkbox" ng-model="spellLink.prepared" ng-class="{true: 'invis'}[spellLink.alwaysPrepared || spellLink.level == 0]" />
{{spellLink.name}}
</label>
</li>
</ul>
</div>
</div>
</div>
...
<!-- previous code block with added 'ng-hide' -->
<div ng-repeat="(spellKey, spellValue) in spells | groupBy:'level'">
<a id="level-{{spellKey}}" class="anchor"></a>
<div ng-repeat="spell in spellValue | filterEach:query | orderBy:['level','name']"
ng-hide="hideNonPrepared && !spell.prepared">
<a id="spell-{{spell.name | replaceSpaces}}" class="anchor"></a>
...
</div>
</div>
Last, I want to limit the number of checked checkboxes allowed to some value assigned in $scope (happens to be 9, but can be in arbitrary integer). I also want to exclude any values where spell.alwaysPrepared == true, but I assume that's just a filter I can add later.
This is where I'm unsure how to continue. I thought maybe I should set ng-check, but I wasn't sure to what.

You can use ng-disabled to disable your checkboxes under certain conditions.
Below is an example of how this might work for you. The key here is to create a function that will be called every digest cycle to determine whether each checkbox should be disabled.
angular.module("spells", [])
.controller("spellController", function($scope) {
$scope.shouldDisable = function(spell) {
return $scope.numPreparedSpells() == $scope.maxPreparedSpells && !spell.isPrepared;
};
$scope.spells = [
{name: "Magic Missile", isPrepared: false},
{name: "Lightning bolt", isPrepared: false},
{name: "Cure Major Wounds", isPrepared: false}
];
$scope.maxPreparedSpells = 2;
$scope.numPreparedSpells = function() {
var numPrepared = 0, len = $scope.spells.length;
for(var i = 0; i < len; i++) {
if ($scope.spells[i].isPrepared) {
numPrepared++;
}
}
return numPrepared;
};
});
With this setup in place, your spell list HTML might look like:
<div ng-controller="spellController">
<div ng-repeat="spell in spells">
<input type="checkbox" ng-model="spell.isPrepared" ng-disabled="shouldDisable(spell)"/>{{spell.name}}
</div>
</div>
Try it on Plunker.
That showed the basic idea, but it would be nice to have an O(n) solution:
angular.module("spells", [])
.controller("spellController", function($scope) {
$scope.shouldDisable = function(spell) {
return $scope.numPrepared == $scope.maxPreparedSpells && !spell.isPrepared;
};
$scope.spells = [
{name: "Magic Missile", isPrepared: false},
{name: "Lightning bolt", isPrepared: false},
{name: "Cure Major Wounds", isPrepared: false}
];
$scope.maxPreparedSpells = 2;
$scope.numPrepared = 0;
$scope.updateNumPreparedSpells = function(spell) {
if (spell.isPrepared) {
$scope.numPrepared++;
}
else {
$scope.numPrepared--;
}
};
});
The trick is to call updateNumPreparedSpells every time a checkbox is clicked:
<input type="checkbox" ng-click="updateNumPreparedSpells(spell)" ng-model="spell.isPrepared" ng-disabled="shouldDisable(spell)"/>{{spell.name}}
Now, the list of spells doesn't need to be traversed to count the number prepared. This, of course, assumes that the prepared state of each spell can only be changed by a click on the checkbox. If the data can be changed elsewhere, the count could be incorrect. Such a scenario is comparable to the cache-invalidation problem, which is known to be hard.

Related

How to get model array after angularjs drag and drop?

I have a list in which data is filled as such:
<ul ui-sortable="sortableOptions" ng-model="hiringstagelist" class="sortable list">
<li ng-repeat="y in hiringstagelist">
<div class="row">
<div class="col-md-1">
<label class="switch">
<input id="{{y.id}}" class="switch-input" type="checkbox" ng-model="y.isDeleted" ng-click="deletehiringstage(y.id)" />
<span id="data_{{y.id}}" class="switch-label" data-on="" data-off=""></span>
<span class="switch-handle"></span>
</label>
</div>
<div class="col-md-11">
{{y.stageName}}
<span style="float:right;margin-right:10px;"><img src="~/Images/sortarrow.png" /></span>
</div>
</div>
</li>
</ul>
As I'm using angularjs drag and drop hence as soon as I drag and drop any item in the list an event gets triggered which is :
$scope.sortableOptions = {
stop: function (e, ui) {
var model = ui.item.model;
console.log(model);
var index = ui.item.sortable.index;
var draggedModel = ui.item.sortable.model;
var newProdArray = ui.item.sortable.resort.$modelValue;
}
}
With index I only get the old value of the item in the list. "newProdArray" doesn't seems to work. Can i use anything so that i can get a model with new index values along with other parameters od the item like id etc ?
Please guide
I think you want ui.item.sortable.dropindex which gives you the index the item was dropped at. See here for documentation.

AngularJS filter is requiring two clicks before it filters results

I am a beginner at angular. I am pretty certain I am doing this the completely incorrect way but because I finally have it "somewhat working" as it works on the second click I am stuck going in this direction and can't seem to figure out another way to do it.
The filter sorts on the second click because it is initialing as "undefined" before the first click and sets it based on that I believe.
In my html:
<div class="col-xs-12 col-sm-4 location-list" ng-repeat="key in careerlist.location">
<div class="locations" ng-click="careerlist.criteriaMatch()">{{key}}
</div>
</div>
<div class="col-xs-12">
<div class="row">
<div class="col-xs-12 col-sm-6 col-md-4 job-container" ng-repeat="job in careerlist.career | filter : searchText | filter: selectExperience | filter: careerlist.criteria.name">
<h2>
{{job.title}}
</h2>
<h3>
{{job.location}}
</h3>
<div class="job-description" ng-bind-html="job.description | limitHtml : 200">
</div>
<br><br>
<button>Read More</button>
</div>
<br><br>
</div>
</div>
In my controller:
cl.criteriaMatch = function( criteria ) {
jQuery(document).on('click', '.locations', function(){
cl.criteria = {};
console.log("just the element text " + jQuery(this).text());
cl.criteria.name = jQuery(this).text();
return function( criteria ) {
return criteria.name === criteria.name;
};
});
};
Use ng-click instead of jQuery#on('click'). Angular does not know that the filter should be updated.
Update
#Makoto points out that the click is bound twice. It very much looks like you should just remove the jQuery binding altogether. I would even go so far as suggesting removing jQuery from you project.

Using href Within ng-repeat

I am using AngularFire and Firebase to store objects which contain a title and a url. I call the object list. I want to:
ng-repeat through my array of objects.
Use the list.url to download the URL's contents. All of the URL's are links to either a .js or a .css file.
Apply the download link/function to a button that is nested inside an input-group (Bootstrap 3.3.6).
All of this to be done within a routed view (ui-router)
What the buttons look like within the input-group
Here's my div. It's nested within a routed view. I am working on the first button within input-group-btn.
<div class="list-group">
<a class="list-group-item" ng-repeat="(id, link) in links | filter: search | orderBy: '-downloadCount' | limitTo: 5">
<h4 class="align-left list-group-item-heading">{{ link.name }}</h4>
<h4 class="align-right list-group-item-heading">{{ link.downloadCount }}</h4>
<br>
<br>
<div class="list-group-item-text">
<div class="input-group">
<input type="text" class="form-control" readonly value="{{ link.url }}" />
<div class="input-group-btn">
// *** IT'S THIS FIRST LINK THAT ISN'T WORKING ***
<button class="btn btn-default" ng-href="{{link.url}}"><i class="glyphicon glyphicon-download"></i></button>
<button class="btn btn-primary" ng-click="copyToClipboard(link)" tooltip-placement="bottom" uib-tooltip="Copy"><i class="glyphicon glyphicon-copy"></i></button>
</div>
</div>
</div>
</a>
</div>
When I use anchor tags instead of button tags, the ng-repeat goes crazy, the links get all kinds of messed up, formatting goes haywire, and there ends up being only one copy of the button that doesn't even work.
I'm not sure if I'm supposed to interpolate the link that ng-href is referring to, but I've tried it with and without and I still don't have anything.
Here's my JS to answer a comment.
var ref = new Firebase('https://myApp.firebaseio.com/links');
$scope.links = $firebaseArray(ref);
$scope.addLink = function () {
$scope.links.$add({
name: $scope.newLinkName,
url: $scope.newLinkUrl,
downloadCount: 1
});
$log.info($scope.newLinkName + ' added to database');
$scope.newLinkName = '';
$scope.newLinkUrl = '';
};
$scope.incrementDownloadCount = function (link) {
link.downloadCount += 1;
var tempCount = link.downloadCount;
var currentRef = new Firebase('http://myApp.firebaseio.com/links/' + link.$id);
// update the downloadCount
currentRef.update({ downloadCount: tempCount });
// update priority for sorting
currentRef.setPriority(tempCount);
$log.info(link.name + ' downloadCount changed to ' + link.downloadCount);
};
// Possibly relevant includes
ui.router
ui.bootstrap
firebase

Why am I getting "Error: 'undefined' is not a function (evaluating 'items.split(',')')" here?

I'm getting the following error while typing into the field filtered by 'completeList'. Why is this happening?
JavaScript
angular.module('myApp', ['timer'])
.controller('AppCtrl',['$scope', function($scope){
$scope.gameOn = true;
$scope.leaders = true;
$scope.myScore = true;
}])
.filter('completeList', function() {
return function(items) {
var list = [];
if(items) {
list = items.split(',');
var last = list[list.length - 1];
if(items.charAt(items.length - 1) != ',' || last.length === 0)
list.pop();
}
return list;
};
});
HTML
<div ng-show="gameOn" ng-controller="LabelCtrl" class="row marketing">
<div class="col-lg-4">
<h4>Enter comma-separated labels for this image</h4>
<form role="form" class="form-inline" >
<input ng-list ng-model="labels" placeholder="Enter labels" class="form-control" type="text" >
<button class="form-control" class="btn btn-xs btn-success">Submit</button>
</form>
</div>
<div class="col-lg-2">
<h4>Labels</h4>
<div>
<ol>
<li ng-repeat="label in labels track by $index | completeList">
{{ label }}
</li>
</ol>
</div>
</div>
The good news is that you only have a minor Angular syntax error. It is actually mentioned in the documentation:
Filters should be applied to the expression, before specifying a
tracking expression.
...
For example: 'item in items | filter:searchText track by item.id' is a pattern that might be used to apply a filter to items in conjunction with a tracking expression.
Given this knowledge, just change your ngRepeat line to the following, it does work exactly as you intended and works perfectly on my side:
<li ng-repeat="label in labels | completeList track by $index">
I do not know what the data structure of labels so here is my best stab at it. Filters are applied onto the instance of your iteration of your loop. It looks like you may be trying to apply the filter to the entire collection instead of that index of the loop. The filter is applied to label not labels. In this case you cannot split it. Again I dont know your data structure so I am kind of guessing here. It would be helpful if you could reveal what labels is.
Thanks,
Jordan

ng-model doesnt communicate with ng-repeat

I've been working on an app for trainers and I have encountered possibly simple
problem, despite that I dont know how to fix this and I tried many different solutions.
What i've noticed is that it works pretty fine when input is above the list generated by ng-repeat but i want the list to be under the input.
Any suggestions will be appreciated.
Here is the html code as it is now:
<html ng-app="trainingSupport">
<form method="post" enctype="multipart/form-data" action="" ng-controller="addOffer">
<div class="span40"><input type="text" ng-model="newOffers.offerName" name="offer" class='span48 offer-in'></div>
<div class="span8 options-btn">
<div class="pencil-offer"><i class="icon-pencil icon-offer"></i></div>
<button ng-click="newOffer()" type='submit' class="btn save-offer"><i class="icon-save"></i></button>
<button type="submit" class="btn trash-offer"><i class="icon-trash"></i></button>
</div>
</form>
</div>
<div class="row-fluid">
<ol class="span48" ng-controller="addOffer">
<li ng-repeat="offer in offers" ng-bind='offer.offerName' class="unbold f-pt-sans offer-list"></li>
</ol>
</html>
and here is the tha angular code:
var trainingSupport = angular.module('trainingSupport', []);
function addOffer($scope){
$scope.offers=[
{id:0, offerName:"szkolenie z pieczenia indyka"},
{id:1, offerName:"szkolenie z gaszenia wodÄ…"},
{id:2, offerName:"szkolenia z bicia konia"}
];
$scope.newOffer = function(){
$scope.offers.push({
offerName: $scope.newOffers.offerName
});
$scope.newOffers.offerName='';
}
}
trainingSupport.controller("addOffer", addOffer);
I created a jsFiddle for this, and chiseled your code down to the basics, for better readability.
http://jsfiddle.net/yhx8h/1/
I refactored your controller quite a bit, its much cleaner now.
var trainingSupport = angular.module('trainingSupport', []);
trainingSupport.controller("addOfferCtrl",
function AddOfferCtrl($scope){
//this variable is bound to your input
$scope.newOfferName = '';
$scope.offers=[
{id:0, offerName:"szkolenie z pieczenia indyka"},
{id:1, offerName:"szkolenie z gaszenia wodÄ…"},
{id:2, offerName:"szkolenia z bicia konia"}
];
$scope.newOffer = function(){
//handy little method to grab the max id in your array
var newId = Math.max.apply(null,$scope.offers.map(function(item){return item.id}));
//increment the id, and add new entry to the array
$scope.offers.push(
{id: newId + 1, offerName: $scope.newOfferName }
);
};
});
And the HTML:
<div ng-app="trainingSupport" ng-controller="addOfferCtrl">
<input type="text" ng-model="newOfferName" />
<button ng-click="newOffer()" type="button" text="click me to add offer" ></button>
<ol>
<li ng-repeat="offer in offers">{{offer.offerName}}</li>
</ol>
</div>
I bound your input to a separate variable, newOfferName, and got rid of the extra submit button and the <form> tag. Judging from the code you posted, I don't see why you would need to use a <form> tag in this implementation, or a submit button. Instead, you can just bind an ng-click function to a button (or almost any other type of element really) which will handle updating your array and re-binding your list. Finally, I dont see why you need an ng-bind on your <li ng-repeat>, I removed that as well. Hopefully this refactored code helps you out!

Resources