Angular ng-repeat with Search not working when search is empty - angularjs

I have the following:
<div ng-repeat="foo in foos | filter:search | orderBy:'fooName'">
<input type="checkbox" id="cb-{{$index}}" ng-click="diseaseCheckboxClick($index)"/>
<input type="hidden" id="foo-{{$index}}" value="{{foo.id}}"/>
<label id="label-{{$index}}">{{foo.fooName}}</label>
</div>
The collection of 'foos' comes from a ReST call.
A person has a collection of 'foos', and I have a function that loops over each 'foos' collection, and the job of the function is to checkmark the 'foo' or not. If the person has that particular 'foo', checkmark it.
I use the a hidden text field to hold the fooIds, and I use it for comparison.
for (var i = 0; i < $scope.person.foos.length; i++) {
var outerFooId = $scope.person.foos[i].id;
for (var j = 0; j < $scope.foos.length; j++) {
var fooElem= '#foo-' + j;
var fooIdValue = $(fooElem).val();
if (outerFooId === fooIdValue) {
var cbId= '#cb-' + j;
$(cbId).attr('checked', true);
break;
}
}
}
The only problem I have is that when I enter a search item that brings back zero results (and that works), but then when I backspace back and remove the search items that brought back no results, the function that is used to checkmark the checkboxes does not work, it is due to the call '$(fooElem).val();' in the function returns undefined.
I see the list in the page, and when i view source i see all of the fields, including the hidden text field that holds the fooId, so I do not know why the function does not 'see' it.
Note that this only occurs when I enter a search that brings back no results, then backspace over to clear out the search field.
The search and marking the checkboxes work otherwise.
Any idea why this is happening, or how I can make the function 'see' the hidden text field. I'll note that I also tried to use the 'label' element, but same result, the text of the label is undefined.

This will not completely solve your problem, but I have to point out that you are making the classic mistake of mixing methodologies. In Angular, you should never, EVER have any code that manipulates the DOM outside of a directive, and even then it is rarely necessary.
There are two classic clues here - you are using $() (jquery), and you are using HTML DOM id attributes. The only real use for id's in Angular is to associate labels with input elements (which you are not doing here), and even then in most cases you can get away with encasing the input inside the label instead of using the "label for" syntax.
Having said all of that, and without seeing the rest of your code, here is how you "angularize" what you are doing:
I am going to make a few assumptions (that should probably be answered by your question):
1) foos is a list of all foos. Each foo has a unique id and name.
2) a person can have some or all foos.
3) The purpose of your code is to allow someone to see which foos they have, and filter the list by name.
So first, the model:
To make things fast (avoid looping when checking each foo, foos should be indexed by id. To when you load them from your for a person, you should create a map by id:
var fooMap = {};
foreach(var i in person.foos) {
fooMap[person.foos[i].id] = person.foos[i];
}
Note that this could be simplified if person.foos is already a map by id!
You also need a few methods in your controller:
$scope.hasFoo(foo) {
return fooMap[foo.id];
}
And your template becomes:
<div ng-repeat="foo in foos | filter:search | orderBy:'fooName'">
<label>{{foo.fooName}}<input type="checkbox" ng-click="diseaseCheckboxClick(foo)" ng-checked="hasFoo(foo)"/></label>
</div>
Note no hidden input, no id's - just a template that is driven by the state of your $scope.
This will solve the immediate problem (losing foos after a search).
The last part of the puzzle is implementing your click function. Without knowing more about your actual implementation, here is the general idea (you'll have to figure the details out yourself).
$scope.diseaseCheckboxClick = function(foo) {
if(fooMap[foo.id]) {
//remove the foo from $scope.person.foos
// and update fooMap
delete fooMap[foo.id];
}
else {
//add the foo to $scope.person.foos and update fooMap
$scope.person.foos.push(foo);
fooMap[foo.id] = foo;
}
}
Hopefully this points you in the right direction. I'm inferring a lot, so I'm sure I've got a few things wrong, but the key problem you have right now is mixing jQuery with Angular.

Related

AngularJS - Validate a part of a form

I'm using Angular 1.5.9
I have a big form, which I scattered through different Bootstrap Accordion.
When there is an error in the form, I want to be able to change the class of my accordions to show in which accordions the error is located.
To check for errors in a whole form, I can check
myFormName.$error
And to check errors for an element, I can simply do
myFormName.myInputName.$error
But I don't know if there is a way to do this for multiple element at once, without having to check each element individually.
My first thought was to change the name of my inputs like so:
<input name="accordion1.fieldName">
But this didn't give me the expected result: I don't have myFormName.accordion1.$error, actually, I don't even have myFormName.accordion1.fieldName, since my data is actually stored in myFormName['accordion1.fieldName'] which is pretty much useless.
Has anyone found an answer to this problem? I think I'll have to check each field, which is kinda ugly, and a mess to maintain whenever we add / remove fields...
Maybe there is a directive out there that could do that for me, but as a non-native English speaker, I can't find which key words to use for my search in this situation.
One approach is to nest with the ng-form directive:
<form name=form1>
<div ng-form=set1>
<input name=input1 />
<input name=input2 />
</div>
</form>
{{form1.set1.$error}}
You could name the fields with a prefix such as 'accordion1_' then add a controller function that will filter your fields.
ctrl.fieldGroup = function(form, fieldPrefix) {
var fieldGroup = {};
angular.forEach(form, function(value, key) {
if (key.indexOf(prefix) === 0) {
fieldGroup[key] = value;
}
});
return fieldGroup;
}
Then ctrl.fieldGroup('accordion1') will return an object with the appriopriate fields on it. You could extend the function further to add an aggregate $error property to the resulting fieldGroup object.

How do I keep results filtered with new results added dynamically?

I'm using an infinite scroller package which loads results dynamically. I have a set of filters which display the results I want depending on what filter is selected.
The results will not be returned to the front end in any sort of filtered order. What I want to achieve is get the unfiltered results being returned into filtered state that I may already have selected.
For example one filter is a star rating and lets say that I have 3 and 4 star selected in my filter. How would I get newly loaded results to be returned within that filter.
I suppose I could send the filters back to the backend to specifically request what I'm looking for but I'm looking for an angular solution.
Here's the ng-repeat that iterates over each hotel I want any newly loaded results to be subjected to any filters already selected:
<div ng-repeat="hotel in (filteredHotels = (hotelResults | hotelRatingsFilter:ratings)) | startFrom:(currentPage - 1)*10 | limitTo:10 " class="hotel-results-container">
<ul>
<li>{{hotel.starRating}}</li>
</ul>
</div>
This is the code that communicates with the backend, as far as I can tell (I didn't write it)
.constant('HOTEL_SEARCH_CONSTANTS',{
httpSettings:
{
url:'/hotel/hotelsearch/search',
method:'POST'
}
})
.service('hotelSearchService', ['CachedObservable','$http','HOTEL_SEARCH_CONSTANTS','HOTEL_CONSTANTS','baseSearchSettings','loadingScreen',
function(CachedObservable,$http,HOTEL_SEARCH_CONSTANTS,HOTEL_CONSTANTS,baseSearchSettings,loadingScreen){
CachedObservable.call(this)
var _this = this,
p
this.search = function(searchParameters){
var config = HOTEL_SEARCH_CONSTANTS.httpSettings
var currentSearch = angular.extend(searchParameters,baseSearchSettings)
config.data = {
basicSearch: baseSearchSettings,
hotelSearch: searchParameters
}
_this.update(p=$http(config), angular.extend(angular.copy(searchParameters),baseSearchSettings) )
//Alert subscribers with both the promise and the search parameters
//used
loadingScreen.newSearch.update(p,HOTEL_CONSTANTS.loadingTemplates.loadingMessageTemplate)
//Trigger a loading screen and specify the template to use
}
}])
Thank you
I found this article that says that angular will iterate over the array if there is any change to the data. Looks like its safe to keep adding to the data array and any filters already being used will be applied by angular to the updated data array.
Step 6 says:
"A watch is added on the array, which triggers step 1 again if the array undergoes any change"
ng-repeat in angular

Angularjs : creating dynamic form

I'm developing a friend invitation feature for a website.
Only requirements are : by email and has a max number of invitations at a time.
My idea is the following :
At the start, user only sees one email field. When he enters an email adress in the only field, angularjs validates it (email format check) and creates an additional email field.
Now, I come from a jquery background and I think it's bad practice to manipulate DOM with angular.
How would one do it with angularjs ?
Is it a good idea to create a factory that "produces" (from a template file) fields ?
Can a library like bootstrap ui help me write simpler code for form validation and error management
This Plunker might fulfill your need at its closest: http://plnkr.co/edit/5qRXQ1XGzUnhYjLCiyYR?p=preview
The key point in this technique is letting the user directly edit a dynamic list of models. Indeed in the example, $scope.invites contains your values. The trick here is referring to them as models:
<input type="email" class="invite" name="invite{{ $index }}" ng-model="invites[$index].mail" ng-change="checkInvite($index)" />
$index being the index of the current ng-repeat iteration. checkInvite function will take care of watching changes in your invites fields.
Notes:
invites is an array of objects, this way we're sure not to mess with ng-repeat, iterating over the reference that we handle (vs models that would be handled by angular)
The field's name is useful to manually check the field's validity: in the controller we can check a field's validity accessing $scope.formName.fieldName.$valid
I also added an extra test that checks if the user clears a non-last filled-in field. In this case, we remove the field.
Have fun using angular!
Personally, I would find the design confusing, since I wouldn't know I could have more email addresses. At the minimum, I would want a + to indicate to the user that s/he can add more addresses. Think of how airlines do "multiple destinations" searches on their Websites.
However, if you are set at this, use an array in the scope. I am using a table for this, but anything will do.
<input ng-model="newemailaddress"></input><button ng-click="addEmail">Add</button>
<table>
<tr ng-repeat="addr in addresses"><td>{{addr}}</td></tr>
</table>
And your controller something like:
.controller('MyCtrl',function($scope) {
$scope.addresses = [];;
$scope.newemailaddress = "";
$scope.addEmail = function() {
// do validation
if (valid) {
$scope.addresses.push($scope.newemailaddress);
$scope.newemailaddress = "";
};
};
})

AngularJS: how put correct model to repeated radio buttons

I think I have some sort of special code here as all I could google was "too simple" for my problem and it also didn't helped to come to a solution by myself, sadly.
I got a radio button group of 2 radios. I am iterating over "type" data from the backend to create the radio buttons.
My problem is the data binding: When I want to edit an object its "type" is set correctly, but not registered by the view so it doesn't select the desired option.
Follwing my situation:
Backend providing me this as "typeList":
[
{"text":"cool option","enumm":"COOL"},
{"text":"option maximus","enumm":"MAX"}
]
HTML Code:
<span ng-repeat="type in typeList track by type.enumm">
<input
type="radio"
name="type" required
ng-model="myCtrl.object.type"
ng-value="type">
{{type.text}}
</span>
Some Explanation
I don't want to use "naked" texts, I want to use some sort of identifier - in this case it is an enum. The chosen value shall be the entire "type", not only "type.text" as the backend expects type, and not a simple String.
So all I do with this is always a package thingy, the type.text is for like formatted/internationlized text etc.
A Pre-Selection works by setting this in the controller: this.object.type = typeList[0];
The first radio button is already selected, wonderful.
But why isn't it selected when editing the object. I made a "log" within the HTML with {{myCtrl.object.type}} and the result is {"text":"cool option","enumm":"COOL"}. The very same like when pre selecting. I already work with the same "technique" using select inputs, and it works fine. I also found some google results saying "use $parent because of parent/child scope". But 1) I didn't get that straight and 2) think it is not the problem here, as I use a controllers scope and not the $scope, or is this thinking wrong?
It might be explained badly, sorry if so, but I hope someone 1) get's what I want and 2) knows a solution for it.
Thank you!
If you're trying to bind to elements from an array, I believe you need to assign the actual elements from the array to your model property.
So this creates a new obj and sets it to $scope.selectedType (not what you want):
$scope.selectedType = {"text":"cool option","enumm":"COOL"};
whereas this assigns the first element of the array (which is what you want)
$scope.selectedType = $scope.typeList[0];
So to change the model, you can lookup the entry from the array and assign it to your model with something like this
$scope.selectedType = $scope.typeList.filter(...)
Here's a quick example of this approach http://plnkr.co/edit/wvq8yH7WIj7rH2SBI8qF

Stellar.js in RWD

I've been messing around with stellar.js. In my design, the navigation needs to become a select in order to save horizontal space.
I imagine the html would be similar to this :
<select>
<option data-slide="1">Accueil</option>
<option data-slide="2">Services</option>
<option data-slide="3">RĂ©alisations</option>
<option data-slide="4">Contact</option>
</select>
However, I am not sure as to how the .js file should be changed in order to do this. I believe this is where the magic happens, but I am not 100% on this :
slide.waypoint(function (d, e) {
dataslide = c(this).attr("data-slide");
if (e === "down") {
c('.navigation li[data-slide="' + dataslide + '"]').addClass("active").prev().removeClass("active")
} else {
c('.navigation li[data-slide="' + dataslide + '"]').addClass("active").next().removeClass("active")
}
});
Has anyone ever done this ? Thanks !
I can tell by the code you posted that you're following the tutorial published a while a go in Tutsplus, right?
Well, that piece of Javascript code leverages the waypoint plugin to mark as selected the link corresponding to the slide in the viewport (and hence visually enhance it). But since you're going to be using a dropdown list the approach above is useless. To get things done you will need first to ensure that the user is properly scrolled upon changing the selectbox value. This will make the trick (you'd better change the jQuery selector below to ensure this handler does not affect any other input control in the page but the main dropdown nav widget):
$('select').on('change', function() {
var slide = $(this).find(':selected').data('slide');
goToByScroll(dataslide);
});
Ok, and now we need to ensure that the listbox changes its default value as the user performs the scrolling. We will leverage some bits of the code you posted. Something like this will be enough:
slide.waypoint(function (d, e) {
var dataslide = c(this).attr("data-slide");
$('select').prop('selectedIndex', parseInt(dataslide)-1);
});
AS a word of caution, the code above assumes that all aslides have a numeric data-slide value that goes from 1 to n and each listbox index refer to a matching slide with the same index in the page, plus 1, being its sorting order the same as the dataslide it represents.

Resources