Referencing scope children in filters (AngularJS) - angularjs

I'm an AngularJS newbie, and am putting together a pretty basic proof-of-concept for my boss. It's listings for car hire, with a results list in the main area of the view populated via some external JSON, and a filters panel down the side. You can see the Plunker I've created here:
http://plnkr.co/lNJNYagMC2rszbSOF95k
I've been able to successfully reference child objects/values in my ngRepeat:
<article data-ng-repeat="result in results | filter:search" class="result">
<h3>{{result.carType.name}}, {{result.carDetails.doors}} door, £{{result.price.value}} - {{ result.company.name }}</h3>
<ul class="result-features">
<li>{{result.carDetails.hireDuration}} day hire</li>
<li data-ng-show="result.carDetails.airCon">Air conditioning</li>
<li data-ng-show="result.carDetails.unlimitedMileage">Unlimited Mileage</li>
<li data-ng-show="result.carDetails.theftProtection">Theft Protection</li>
</ul>
</article>
...however, I've so far been unable to access the 2nd level child objects in my search filter. So, for example, where I'm filtering by 'car type' (see below), I'd like to be able to use 'search.carType.name' as my ngModel, to be as specific as possible - but this doesn't work, although just using 'search.carType' works fine. Can anyone advise on what I'm doing wrong?
<h4>Car type:</h4>
Compact <input type="checkbox" data-ng-model="search.carType" ng-true-value="Compact" ng-false-value="" /><br>
Intermediate <input type="checkbox" data-ng-model="search.carType" ng-true-value="Intermediate" ng-false-value="" /><br>
Premium <input type="checkbox" data-ng-model="search.carType" ng-true-value="Premium" ng-false-value="" /><br>

Your search object is being populated correctly, but filter isn't consuming it in the way you expect. Looking at the implementation of filter (https://github.com/angular/angular.js/blob/master/src/ng/filter/filter.js), it appears to only go one layer of child-properties deep when it is given an object as a filter definition.
Ajay's suggestion will work, but you would then need to chain additional filters to accomodate your other parameters. You could change both car type and company to specify x.name in the ng-model and then alter the filter to filter:search.carType.name|filter:search.company.name. If you were only going to have a small number of parameter types, I'd handle it this way.
On the other hand, the nice thing about your current approach is that it's transparent. There's no need to the filter call to be changed if the number of parameters changes elsewhere. If you might have a relatively large number of those, or if they were dynamic, I would take a more scalable approach. Write a filter FUNCTION that consumes the search object, and goes more than one level deep in comparing the children to the filtered data.

Here is a nice post by Anton Kropp on deep object filtering: FILTER ON DEEP OBJECT PROPERTIES IN ANGULARJS
The relevant code:
function initFilters(app){
app.filter('property', property);
}
function property(){
function parseString(input){
return input.split(".");
}
function getValue(element, propertyArray){
var value = element;
_.forEach(propertyArray, function(property){
value = value[property];
});
return value;
}
return function (array, propertyString, target){
var properties = parseString(propertyString);
return _.filter(array, function(item){
return getValue(item, properties) == target;
});
}
}
And the HTML
<ul>
only failed: <input type="checkbox"
ng-model="onlyFailed"
ng-init="onlyFailed=false"/>
<li ng-repeat="entry in data.entries | property:'test.status.pass':!onlyFailed">
<test-entry test="entry.test"></test-entry>
</li>
</ul>
And a JSFiddle here

Related

Angular dynamic model naming?

i'm learning angular, so please be gentle ;)
I have a table that is populated with some data. And i would like to add filtering (not angular filter, but data filter) on each column.
The idea for me is to add a text box at the top of each column in the header. Then use those textbox values to create the Filters array in my service when it called the api to get the data.
I currently have:
<tr>
<th ng-repeat="column in report.Columns" ng-show="column.IsVisible">
<span class="sort" ng-click="updateSortOrder($index)" ng-class="{'sort-asc': predicate == $index && !reverse, 'sort-desc':predicate == $index && reverse}">{{ column.DisplayName }}</span>
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-filter fa-fw"></i></span>
<input class="form-control" type="text" placeholder="Search" ng-model="">
</div>
</th>
</tr>
what i'm stuck on, is how to assign the text boxes to the ng-model, naming them dynamically based on the column that they stand in.
I thought about using ng-model="column.ColumnName" - but wont that bind the text boxes to the report.Columns.ColumnName ? i dont want to edit that, i want to create a new var on the scope for each one.
Something like $scope.Filters.-ColumnName-.
And then i would like to loop through each ColumnName on $scope.Filters and use it's value in my filters array on my service call.
I hope this is making sense. If I'm heading down the wrong route to achieve this, please feel free to point me in the right direction, as i said, i've just started learning Angular.
Thanks in advance
This is not angular related specifically it is more of a javascript issue.
For variable property names you use [] object notation
<input ng-model="filters[column]"
In controller :
$scope.filters ={}
If properties aren't already created in the scope object, ng-model will create them automatically when there is any user input

Why don't AngularJS Filters work when inside an ng-if?

I have a simple AngularJS page with different sections that I show & hide when links are clicked. One of these areas has a repeated list that can be filtered.
When the section containing the list is shows/hidden with ng-show or ng-hide it behaves normally. When ng-if is used, the list cannot be filtered.
Demos
This version does not work due to the use of ng-if
This version does work due to the use of ng-show
Sample HTML
<nav>
Area 1
Area 2
</nav>
<div ng-if="area==='one'">
<h3>Area 1!</h3>
<input type="text" placeholder="filter list..." ng-model="filterText" />
<ul>
<li ng-repeat="item in list | filter: listFilter">
{{item.id}} - {{item.name}}
</li>
</ul>
</div>
<div ng-if="area==='two'">
<h3>Area 2!</h3>
<p>Stuff here...</p>
</div>
Sample Angular
$scope.area="one";
$scope.filterText="";
$scope.list = [
{id:1, name:"banana"},
{id:2, name:"apple"},
{id:3, name:"orange"},
{id:4, name:"pear"},
{id:5, name:"apricot"}
];
$scope.listFilter = function(item){
var term = $scope.filterText.trim().toLowerCase();
return item.id.toString().indexOf(term) > -1 || item.name.indexOf(term) > -1;
};
I don't hold a Masters degree on the subject of Prototypal Inheritance myself but I'll try my best to explain it shortly (there's a ton of resources on the subject);
Number
String
Boolean
null
undefined
Symbol (as of ES6)
are considered primitives (MDN).
Now - when you 'inherit' a primitive from your parent scope, what is actually happening is that the child scope 'mirrors' or 'shadows' the given primitive value. As such, you can think of it as a copy of the above.
That is roughly the nature of primitives in the context of Prototypal Inheritance.
This can clearly be observed in a modified version of your broken fiddle.
Try playing with the two inputs and you can see that there is a connection of the two values when you only touch the outside input (i.e. the child value 'shadows' the parent value). But once you touch the inside input, the values disconnect from one another.
The recommended way to get around this is to use a reference to a property on your model (I say model, but really it's just a JS object) that is defined further up the prototype chain;
$parentScope.obj = { filterText: '' };
ng-model="obj.filterText"
Now you should be good to go with ngIf, ngSwitch, ngRepeat to name a few of the angular supplied directives that create a new scope.
Resources on the subject
understanding scopes # angular
presentation by misko on the subject
stackoverflow answer by Mark Rajcok
the dot #egghead.io
google search 'dot ng model'

How to set a boolean flag to collapse/expand a row with ng-repeat

I have this plunker code.
What I'm trying to do, is to display the gray box one time per row.
To achieve this, I thought to modify the partition filter in order to return a JSON to add it a new property by row to know if the gray box is expanded or not.
But, I could Not successfully return a JSON.
Do you know how to modify the filter to return a JSON or a better way to show the gray box by row?
Related questions:
Push down a series of divs when another div is shown
Update 1
The issue could be easily resolved by using the correct scope for the ng-repeat for the row without modifying the filter, thanks to #m59.
http://plnkr.co/edit/eEMfI1lv6z1MlG7sND6g?p=preview
Update 2
Live Demo
If I try to modify the item, it seems the ng-repeat would be called again losing the props values.
<div ng-repeat="friendRow in friends | partition:2"
ng-init="props = {}">
<div ng-repeat="item in friendRow"
ng-click="collapse(item)"
ng-class="{myArrow: showArrow}">
{{item.name}} {{item.age}} years old.
<div>{{item.name}}</div>
</div>
<div collapse="!props.isExpanded">
some content
<br/>
<input type="text" ng-model="currentItem.name">
</div>
</div>
js
$scope.collapse = function(item){
this.props.isExpanded = !this.props.isExpanded;
this.showArrow = !this.showArrow;
$scope.currentItem = item;
};
This causes the gray box to collapse each time the item is modified. Any clue?
I've updated my code/answer regarding partitioning data. It's important to fully understand all of that before deciding on an approach to your project.
The problem you have in your plnkr demo is that you're modifying the parent $scope and not the scope of the ng-repeat for that row.
Just set a flag on the row and toggle it when clicked:
Live Demo
<div
class="row"
ng-repeat="friendRow in friends | partition:2"
ng-init="isExpanded = false"
ng-click="isExpanded = !isExpanded"
>
<div ng-repeat="item in friendRow">
{{item.name}} {{item.age}} years old.
</div>
<div collapse="!isExpanded">
some content
</div>
</div>
To access the correct scope within a function in the controller, you can use the this keyword instead of $scope. this will refer to the scope the function is called from, whereas $scope refers to the scope attached to the element with ng-controller (a parent of the ng-repeat scopes you want to target).
<div
class="row"
ng-repeat="friendRow in friends | partition:2"
ng-click="collapse()"
>
JS:
$scope.collapse = function() {
this.isExpanded = !this.isExpanded;
};
If you want to keep the ng-click directive on the item element instead of putting it on the row element as I have done, then you're dealing with another child scope because of that inner ng-repeat. Therefore, you will need to follow the "dot" rule so that the child scope can update the parent scope where the collapse directive is. This means you need to nest isExpanded in an object. In this example, I use ng-init="props = {}", and then use props.isExpanded. The dot rule works because the children share the same object reference to props, so the properties are shared rather than just copied, just like in normal JavaScript object references.
Live Demo
<div
class="row"
ng-repeat="friendRow in friends | partition:2"
ng-init="props = {}"
>
<div ng-repeat="item in friendRow" ng-click="collapse()">
{{item.name}} {{item.age}} years old.
</div>
<div collapse="!props.isExpanded">
some content
</div>
</div>
JS:
$scope.collapse = function(){
this.props.isExpanded = !this.props.isExpanded;
};
Update
We keep going through more and more issues with your project. You really just need to experiment/research and understand everything that's going on on a deeper level, or it will just be one question after another. I'll give it one last effort to get you on the right track, but you need to try in the basic concepts and go from there.
You could get past the issue of props reinitializing by putting $scope.expandedStates and then passing the $index of the current ng-repeat to your function (or just using it in the view) and setting a property of expandedStates like $scope.expandedStates[$index] = !$scope.expandedStates[$index]. With the nested ng-repeat as it is, you'll need to do $parent.$index so that you're associating the state with the row rather than the item.
However, you'll then have another problem with the filter: Using my old partition code, the inputs inside the partitions are going to lose focus every time you type a character. Using the new code, the view updates, but the underlying model will not. You could use the partition filter from this answer to solve this, but from my understanding of that code, it could have some unexpected behavior down the road and it also requires passing in this as an argument to the filter. I don't recommend you do this.
Filters are meant to be idempotent, so stabilizing them via some kind of memoization is technically a hack. Some argue you should never do this at all, but I think it's fine. However, you definitely should ONLY do this when it is for display purposes and not for user input! Because you are accepting user input within the partitioned view, I suggest partitioning the data in the controller, then joining it back together either with a watch (continuous) or when you need to submit it.
$scope.partitionedFriends = partitionFilter($scope.friends, 2);
$scope.$watch('partitionedFriends', function(val) {
$scope.friends = [].concat.apply([], val);
}, true); // deep watch

What magic does ngModel use that modifying the scope directly does not?

Learning angularjs at the moment, and I am confused as to how I can accomplish a task because I don't fully understand what ngModel is doing.
If I have a directive with two scope variables:
// An array of all my objects
$scope.allMyObjects
// The currently selected object from the array
$scope.selectedObject
and in the html
<span>{{ selectedObject.name }}</span>
<select id="select"
ng-model="selectedObject"
ng-options="object in allMyObjects">
</select>
This all works perfectly, when I select an object from the select, it updates the selectedObject on the scope and so the name of the currently selected object is displayed.
However, I don't want a select box, instead I want a list of all my objects with an editable name field, with a select button that I can use to select the specified object, so I came up with the following:
<div ng-repeat="object in allMyObjects">
<input class="object-name"
ng-model="object.name">
<a ng-click="loadObject(object)">Load</a>
</div>
and the loadObject() function on the scope:
function loadObject(object) {
$scope.selectedObject = object;
}
However, this doesn't work. I had assumed this was basically what ngModel was doing behind the scenes but am obviously confused. Is anyone able to shed some light or offer a better solution to what I wish to achieve?
Please see here :http://jsbin.com/jocane/1/edit?html,js,output
use ng-model="object.name" instead "sc.name"
<div ng-repeat="object in allMyObjects">
<input class="object-name"
ng-model="object.name">
<a ng-click="loadObject(object)">Load</a>
</div>
After an hour of debugging it came down to an issue with the scope being isolated by the ng-repeat, the problem didn't show up in any of the simplified jsfiddle examples because they used pure JS and it was the way I was accessing the scope via typescript that caused the issue.
Thanks for the answers that helped me narrow it down to my difficulty understanding typescript and not my difficulty understanding directives.

Ng-model's attribute in a ng-repeat input checkbox gets always literal or give error

So i need to know the extras of a car than a user wants to include in his preferences.
I'm trying to create input checkboxes from an array obtained by an ajax request and generate the inputs by ng-repeat. The major objective is to know the checkboxes selected by the user. I'd like that my approach to be create an auxiliar array which contains the selected ones, but i don't know how to set a unique ng-model to every item in the ng-repeat iteration so i can know the list of selected items. I guess there is something left in my knowlege of angular. Here is what i have for now..
In the controller...
$http.get('/ajax/ajax_get_extras/'+$scope.car.version+'/false').success(function(data) {
$scope.extras = data;
});
$scope.addExtra = function(){ // ... manage the auxiliar array }
In the html ...
<div ng-controller="Controller">
<form novalidate class="simple-form">
<span ng-repeat="extra in extras">
<input type="checkbox" ng-model="extra.id" ng-change="addExtra()" name="extra_{{extra.id}}" >{{extra.name}} - <strong>{{extra.real_price | onlynumber | currency}}</strong>
</span>
</form>
</div>
And i'm stuck since the extra.id doesnt transform to the real extra.id and stays as a string "extra.id" >_<
I tried extra_{{extra.id}}, extra.id, {{extra.id}}, $index as posibles ng-model and none works.
In AngularJS 1.1.5 there is "track by" that you can use in ngRepeat.
So you can:
<input type="checkbox" ng-repeat="e in extra track by $index" ng-model="extra[$index]">
Here is a example: http://plnkr.co/edit/6lNo6R5EPsNGHUU6ufTE?p=preview

Resources