Using a weaker equality test for angular select - javascript

Lot's of advice tells me to do this:
// in js
$scope.items = [
{ id: 1, name: 'Foo'},
{ id: 2, name: 'Bar'}];
// in html
<select ng-model="selectedItem"
ng-options="item as item.name for item in items"></select>
And that works fine. This works fine too:
$scope.selectedItem = $scope.items[1];
The select will be initialized to the Bar object.
But this doesn't work:
$scope.selectedItem = { id: 2, name: 'Bar'};
The select control is not initialized to the Bar object (understandably, I think). The selectedItem is equivalent to the Bar object, but not equal to it. I have this problem in an app where parse is the back-end. The selectedItem is a pointer from one object to another, and the items are all of the (handful) of objects in the target class. I get these in two different queries.
Is there a way to manipulate the angular so that I still select an object, but use a custom equality test, like the object id?

Yes, but it will require use of an external library or some scripting of your own. You just need a lookup function which will take your key/value pair (such as you present it in your code sample) and return an item from the array.
The example below uses findWhere in Underscore, which:
Looks through the list and returns the first value that matches all of
the key-value pairs listed in properties.
$scope.selectedItem = _.findWhere($scope.items, {id: 2, name: 'Bar'});
Using .findWhere, you can also search for a subset of the key/value pairs contained in an array item, like so:
$scope.selectedItem = _.findWhere($scope.items, {id: 2});
Demo

Related

AngularJS Material - ng-checked on md-radio-button with ng-value as an object

I've looked around to see if anyone has answered this and it appears they have not. I want to use an object rather than a string or integer as the value of the radio button. Is this possible? Doesn't seem so because I'm having trouble with the tags md-radio-button recognizing an object rather than string or integer value as having been selected. I can see that it works after the page loads and I select something but I don't know how to check the radio button if the value already exists. You can see a very simple demonstration here: https://codepen.io/anon/pen/ZmjrLN
I've tried class="md-checked" to see if that works, it does not. I've tried ng-checked="selectedStatus.Name == status.Name", it doesn't work either. In fact ng-checked="true" also does not work.
I would think md-radio-button could work with an object!
--- EDIT FOR CLARIFICATION ---
From an answer below, referencing the code from codepen, if I use the same object in $scope.statuses to populate $scope.selectedStatus, it indeed selects the correct radio button on load. HOWEVER, in the real world $scope.selectedStatus is populated with the actual status from the server and $scope.statuses is also populated from that same call. They are the same but 2 different objects.
In a nutshell, I still want to check off the correct one, even though the objects aren't exactly the same, they should be treated like they are, because they are the same.
When comparing objects, ng-checked looks to see if checked is the same object or a reference and does not check the contend of the object. So to set the selected / checked radio button, make the value refer to the object. Here is an example of the Javascript updates for your codepen:
angular
.module('BlankApp')
.controller('AppController', function($scope) {
$scope.statuses = [
{id: 1, Name: 'Active'},
{id: 2, Name: 'Dormant'}
];
$scope.selectedStatus = $scope.statuses[0]; // reference the object directly
});
Or if the value is set outside of the scope and we have to compare, then we can compare manually and then set the selectedStatus by finding the correct object and assigning it.
angular
.module('BlankApp')
.controller('AppController', function($scope) {
$scope.statuses = [
{id: 1, Name: 'Active'},
{id: 2, Name: 'Dormant'}
];
$scope.selectedStatus = {id: 1, Name: 'Active'};
// Find the index of the selected that matches on id
let i = $scope.statuses.findIndex((val) => {
return val.id = $scope.selectedStatus.id;
});
$scope.selectedStatus = $scope.statuses[i]
});

Knockout select bindings overwriting my predefined observable object

Hi i have a application which data is passed from one page to another with predefined data objects/arrays assigned to it, my issue is i can see the observableArray having a value and then it turns the SelectedPeople observable to undefined.
I have eliminated down to the data bind markup as when i remove that my observable array does not set anything to undefined.
Here is how i am binding my observables/observableArray to the elements.
<select data-bind="options: ObservableArray.People, value: ObservableArray.SelectedPeople, optionsText: 'Name'"></select>
ObservableArray.People = Observable Array of objects - works fine and renders all the dropdown options
ObservableArray.SelectedPeople = Observable
Both have the 'Name' object defined to match the optionsText. It works perfectly when selecting data from scratch but when i try have predefined data in it the Observable.SelectedPeople object keeps getting sent as undefined when it tries to load.
Basically my Observable.SelectedPeople has a object on that which should predefined the value of that select and the object 100% matches one of the dropdown ObservableArray.People options. I need it not to set Observable.SelectedPeople to undefined and populate the select box.
Can anyone see why this is happening.
Thanks
...and the object 100% matches one of the dropdown ObservableArray.People options.
This line makes me suspicious of whether you're using an actual reference to the object, or just an object that is similar.
For example, this will not work:
var options = [{ id: 1 }, { id: 2 }, { id: 3}];
var selectedOption = ko.observable({ id: 1 });
Knockout does not perform some sort of deepEquals comparison; if it sees a non-primitive, it does a reference check. options[0] !== { id: 1 }, so this initial selection is not valid.
The code below will work, because you're using an actual object from the array you use in your select element:
var options = [{ id: 1 }, { id: 2 }, { id: 3}];
var selectedOption = ko.observable(options[0]);

ngOptions - Value Is Undefined

I'm working with AngularJS and I've generated an object with a size property that holds an array of objects.
var product = [
// ..
sizes: [
{choice: 'one', order: 1},
{choice: 'two', order: 2},
{choice: 'three', order: 3}
]
//..
];
On the view side I generate the select box like:
<select ng-init="size = product.sizes[0]" ng-options="size.choice for size in product.sizes" ng-model="size"></select>
This works but when I create a button to get the current selected value of the select it returns undefined: alert($scope.size);
I've duplicated the problem here. Also just for giggles I recreated the same thing (?) in another CodePen here.
The second one works the way I want it to but not the first. I made both but I cannot figure out why the second works and the first does.
this is a classic problem in angular. Its because you are using ng-if which is creating a child scope. you need to set your model to someObject.size and change all references to that.
And then in the controller put $scope.someObject = {};

Show an aggregated list in angularjs

In my model I have data similar to:
$scope.list = [{id:0,tags:['tag1','tag2']},{id:2,tags:['tag2']}};
I want to show a list of tags (contains unique values of 'tag1' and 'tag2') with checkboxes. Hopefully something like:
<div ng-repeat="tag in list.tags">
<label class="checkbox">
<input type="checkbox" ng-model="filter.tag" />
{{tag}}
</label>
</div>
I know how to filter the main list based on whats checked if I hard code the list, but not how to generate the list of unique tags automatically.
You are looking to perform three operations:
Get the array of tags from each item in $scope.list
Flatten these into a single array
Get the unique values from this array
You can do this with pure JavaScript, but to make things easier, I would recommend using Underscore, a library that gives you access to many functions for manipulating and inspecting arrays, objects, and so forth.
Let's start with this code:
$scope.list = [
{id: 0, tags: ['tag1', 'tag2']},
{id: 1, tags: ['tag2']},
{id: 2, tags: ['tag1', 'tag3', 'tag4']},
{id: 3, tags: ['tag3', 'tag4']}
];
Now, let's perform the first operation: get the array from the tags property for each object in $scope.list. Underscore provides the pluck method, which is just what we need.
pluck _.pluck(list, propertyName)
A convenient version of what is perhaps the most common use-case for map: extracting a list of property values.
Using pluck, we can get the following:
var tags = _.pluck($scope.list, 'tags');
// gives us [['tag1', 'tag2'], ['tag2'], ['tag1', 'tag3', 'tag4'], ['tag3', 'tag4']]
Now, we want to flatten that array.
flatten _.flatten(array, [shallow])
Flattens a nested array (the nesting can be to any depth). If you pass shallow, the array will only be flattened a single level.
tags = _.flatten(tags);
// gives us ['tag1', 'tag2', 'tag2', 'tag1', 'tag3', 'tag4', 'tag3', 'tag4']
Finally, you only want one instance of each tag.
uniq _.uniq(array, [isSorted], [iterator]) Alias: unique
Produces a duplicate-free version of the array, using === to test object equality. If you know in advance that the array is sorted, passing true for isSorted will run a much faster algorithm. If you want to compute unique items based on a transformation, pass an iterator function.
tags = _.unique(tags)
// gives us ['tag1', 'tag2', 'tag3', 'tag4']
We can combine these together with Underscore's useful chain method to chain these together. Let's create a function on the scope that returns the unique tags:
$scope.uniqueTags = function() {
return _.chain($scope.list)
.pluck('tags')
.flatten()
.unique()
.value();
};
Since this is a function, it will always return the unique tags, no matter if we add or remove items in $scope.list after the fact.
Now you can use ng-repeat on uniqueTags to show each tag:
<div ng-repeat="tag in uniqueTags()">
<label class="checkbox">
<input type="checkbox" ng-model="filter.tag" />
{{tag}}
</label>
</div>
Here is a working jsFiddle that demonstrates this technique: http://jsfiddle.net/BinaryMuse/cqTKG/
Use a custom filter to get a unique set/array of tags, suitable for use with ng-repeat:
.filter('uniqueTags', function() {
return function(list) {
var tags = {};
angular.forEach(list, function(obj, key) {
angular.forEach(obj.tags, function(value) {
tags[value] = 1;
})
});
var uniqueTags = []
for (var key in tags) {
uniqueTags.push(key);
}
return uniqueTags;
}
});
I first put the tags into an object, which automatically gives us uniqueness. Then I convert it to an array.
Use as follows:
<div ng-repeat="tag in list | uniqueTags">
Fiddle.
The following may not do what I think you probably want/expect it to do:
<input type="checkbox" ng-model="filter.tag">
This does not create $scope properties filter.tag1 and filter.tag2 on the controller scope (i.e., the scope where ng-repeat is used). Each iteration of ng-repeat creates its own child scope, so the ng-model above will create scope property filter.tag on each ng-repeat child scope, as shown in my fiddle.

Data Binding to a specific item of an array in Angular

Given a data structure that contains an array of JavaScript objects, how can I bind a certain entry from that array to an input field using Angular?
The data structure looks like this:
$scope.data = {
name: 'Foo Bar',
fields: [
{field: "F1", value: "1F"},
{field: "F2", value: "2F"},
{field: "F3", value: "3F"}
]
};
The fields array contains several instances of the given structure, with each entry having both a field attribute and a value attribute.
How can I bind an input control to the value field attribute of the array entry with the field F1?
<input ng-model="???"/>
I know that I could bind all fields using an ng-repeat, but that's not what I want. The above data is just an example from a much larger list of fields, where I only want to bind a pre-defined subset of fields to controls on the screen. The subset is not based on the attributes in the array entries, but is known at design time of the page.
So for the above example, I would try to bind F1 to one input on the page, and F2 to another one. F3 would not be bound to a control.
I've seen examples where a function was used in the ng-model, but it doesn't seem to work with Angular 1.1.0.
Is there another clever way to bind the input field to a specific array entry?
Here's a fiddle that has an example, but does not work since it's trying to use function in the ng-model attribute: http://jsfiddle.net/nwinkler/cbnAU/4/
Update
Based on the recommendation below, this is what it should look like: http://jsfiddle.net/nwinkler/cbnAU/7/
I personally would reorganize the array in a way that field property of an entry of the array become the identifier of the object. Mhhh that sentence may sound strange. What I mean is the following:
$scope.data = {
name: 'F1',
fields: {
F1: {
value: "1F"
},
F2: {
value: "2F"
}
}
};
If you want to bind a the value dynamically and it's an easy and quick way to achieve it.
Here is your fiddle modified so that it words. http://jsfiddle.net/RZFm6/
I hope that helps
You can use an array of objects, just not an array of strings.
HTML:
<div ng-repeat="field in data.fields">
<input ng-model="field.val"/>
</div>
JS:
$scope.data = {
name: 'F1',
fields: [
{ val: "v1" },
{ val: "v2" }
]
};
I've updated #Flek's fiddle here: http://jsfiddle.net/RZFm6/6/
Edit: Sorry just read your question properly, you can still use an array with:
<label>Bound to F1:</label>
<input ng-model="data.fields[0].value"/>
though maybe stop and think. Is there going to be variable number of fields ? or are you making a predetermined number of fields ? Use an array in the former and an object for the latter.
One way to do it is to simply add the necessary references to the scope, like this:
$scope.fieldF1 = fieldValue('F1');
$scope.fieldF2 = fieldValue('F2');
And then use those references:
<input ng-model="fieldF1.value"/>
<input ng-model="fieldF2.value"/>
Fiddle: http://jsfiddle.net/cbnAU/5/
Note: I'm assuming that $scope.data is static, but if it happens to be dynamic you can always watch for changes on it and recalculate the references...

Categories

Resources