2 Dropdowns, 1 object ids, other with lists from lists in objects - angularjs

I've got two multiselect dropdowns on a page that uses angularJS. The first is populated from a service call which returns a lists of objects which are basically category objects. The second dropdown is a list of all the specifics from all those categories. Let's call them Groups and Items for simplicity. Originally, I just populated the second list with a second api call which returned a list of those Items. But since the Group objects already contain a list of the Items which are related, this seems unnecessary. (And I'd like to be able to tie the two together, so that if a group is selected, only the items which fall into that group are displayed in the second dropdown, but that's secondary...) My first dropdown get's populated with this code:
$scope.promiseGetGroupNames = providerContactInfoService.getGroupNames();
$scope.promiseGetGroupNames.then(function (pl) {
$scope.GroupDDLdata = pl.data;
}, function (errorPl) {
//error message...
});
So I would assume that in there I could loop through the data and generate a list there, but it seems like angular would have a simpler way of doing things... Is there a way that I can set the options of the second dropdown to be a complete list of items from all of the groups in the first, and then when a user selects one of those groups, remove all but the items which fall into that category?
Additionally, I have researched this, and found some answers that looked like there was a way to connect the ng-options of 2nd dropdown to the ng-model of the first, which seems like what I want to do, at least for the 2nd half, but I still need to get all options in there at page load...
The other idea I had was to simply make two service calls... the first pulls all of the possible items, the 2nd would pull a subset that only falls into the group select... since only one group is ever allowed to be selected, it's not terribly inefficient, but if the person selects one group, then a different one... seems like that would all be unnecessary if it's possible to link all the objects/options/models together...
What is the best practice for this type of scenario, and if it's the first, could someone explain a little about how to set up that connection? I understand the basics of pulling the possible options from the model, but not exactly sure on the syntax (I'm relatively new to angularJS.)

How about something like this ?
1) Populate your first dropdown list :
<select ng-options="groups" ng-model="selectedGroup">
$scope.init = function () {
service.getGroups.then(function (groups) {
$scope.groups = groups;
});
};
$scope.init();
2) Use ng-change directive on your first dropdown list :
<select ng-options="groups" ng-change="fillSecondList()" ng-model="selectedGroup">
<select ng-options="secondList" ng-model="selectedThing">
$scope.fillSecondList = function () {
// Whatever you want, you can use $scope.selectedGroup here
$scope.secondList = ...
};
$scope.fillSecondList will fire when your selection changes so you can update $scope.secondList according to what's selected in your first list ($scope.selectedGroup), for example using a service call using selectedGroupas a parameter.
EDIT :
If all your needed data is fetched from the first service call, you can use a filter, as you don't actually need to modify your secondList model but rather change what is displayed in your second list.
$scope.init = function () {
service.getGroups.then(function (groups) {
$scope.groups = groups;
// Extract the second list from the first one
$scope.secondList = groups.doSomething();
});
};
<select ng-options="secondList | filter: { group : selectedGroup.id }" ng-model="selectedThing">
For example, this will display only secondList items which have a group property equal to selectedGroup.id.
You can use a custom filter if your filtering is more complicated, or a filter function in your controller (triggered with ng-change) if you don't plan to re-use this filter anywhere else :
$scope.filterSecondList = function () {
$scope.secondList = $scope.secondList.filter(function (item) {
return item.group === $scope.selectedGroup.id;
});
};

Related

AngularJS Filter for ng-repeat, how to get access to the filtered data

I have tried to make the title as descriptive as possible.
I have a set of data which I use ng-repeat to display in a table. There is also a filter applied so the user can type in a search bar and it will filter the results of the ng-repeat, there is nothing special about the filter (it is the default filter.
This obviously filters the data and shows it correctly.
The issue is, I also have a dropdown allowing the user to specify what sort of report they want.
The tablature data is raw, but they can select bar or line and it will show a graph. The data for the graph is created when they load the page. It takes the raw data and transforms it into labels and series.
What I would like to happen, is that when a filter is applied, the function transforms the filtered data, but I have no idea how I can actually get access to that data.
Does anyone know how I might go about it?
Update
I tried adding the filter to the controller and then doing my conversion in that function like this:
// Apply our filter
self.applyFilter = function () {
var filteredList = $filter('filter')(self.list, self.filter);
self.list = filteredList;
self.chartData = provider.getOverdueData(filteredList);
};
The problem with that, is that it is invoked many times and causes a $rootScope:infdig Infinite $digest Loop error.
Ok, so the error was caused by the new array which was being created each time applyFilter was invoked. I am not sure why an actual filter works (i.e. in the view) rather than in the function because they are doing the same thing ?!
But I was able to get around this issue by using angular.equals. First when I get the data, I had to store the result into a separate array, then when I invoke my filter I can use the unmodified array.
// Get our report
provider.overdueCollections().then(function (response) {
self.models = response;
self.list = response;
self.predicate = 'center';
self.chartData = provider.getOverdueData(response);
});
Once that was done, the table used self.list as it's datasource, but the applyFilter used the self.models to apply the filter to. It now looks like this:
// Apply our filter
self.applyFilter = function () {
// Create our filtered list
var filteredList = $filter('filter')(self.models, self.filter);
// Check to see if we are the same as the current list
var isSame = angular.equals(self.list, filteredList)
// If we are not the same
if (!isSame) {
// Update our scope
self.list = filteredList;
self.chartData = provider.getOverdueData(filteredList);
}
};
This meant that the data was only applied to the scope if it changed.

Selecting and accessing items in ng-repeat

I have an array of objects in a service that I want to display in a view like this
<li ng-repeat="item in filteredItems = (resource.items | filter:multipleFilters...">
<span class="title">{{item.escaped_name}}</span>
</li>
I want these objects to be selectable which is the easy part. Then I'd need to get the count of the selected items and be able to iterate over all of them to process/change data.
What is the best method to save and access the selected items?
Note that the selected items could change, for example a selected item could fall away due to changed filters. Also, I don't want to set a selected property on the objects in the array directly - the array of objects is in a service and used throughout the app in many lists and I don't want to "clean up" the selected property for each view.
I usually include a ng-click tag:
<li ng-repeat="item in ..." ng-click="select_item(item)">
The item passed to select_item() will be the one the user selected.
You can take the object oriented approach and create models to do your work for you basically. I've been in the same scenario and it's worked for me, plus I think it's a good tool angular gives you to work with. You would have a listModel and an itemModel - the list model would have a list of your single item itemModels. The list model you would use 1 instance of between where ever you are using this list of items. Take this with a grain of salt because this is just an example.
So you'd have the listModel
.factory('listModel', [singleItemModelInject,
function(singleItemModel) {
function listModel(items) {
this.items = _.map(items, listModel.create);
}
listModel.create = function(value, name) {
return new listModel(value);
};
listModel.prototype = {
get whatever() {
},
set whatever() {
}
}
return listModel;
}
]);
Notice it injects singleItemModel - this would be the individual item model it would look the same except it would have all your information on creation with what you pass it like
.factory('singleItemModel', [whateverYouNeedInjected,
function() {
function singleItemModel(item) {
this.name = item.name;
//default to not selected
this.selected = item.selected || false;
this.whateverElseYouNeed = item.whateverElseYouNeed
}
singleItemModel.create = function(value) {
return new singleItemModel(value);
};
So then you would have a single instance of the listModel you would use across your app, you can toggle a hide or show with whatever property, as long as you have setters and getters for your properties you want to access (like the name and isSelected or whatever you want) and have it 2 way bound, if it is changed to selected anywhere, it is universal because of the single instance you are using.
You can also you individual instances if you dont want the selected values to persist across the app, and only in that one page (or where ever you are using it)

AngularJS - Filtering by multiple checkbox groups

I'm looking for a bit of advice on what's the best way to go about this:
I have about 5 groups of checkboxes. The list would be something like brands, model, colour, size and so on. As a user ticks off a brands, colours etc a list of cars get updated based on their selections.
The list is generated from a call to the database, I know it would be fairly straightforward if the list was generated once as a list that could be filtered entirely on the frontend but the DB is expected to grow to over 10,000 rows so that wouldn't be ideal.
My thinking at first was to post to the DB each time a checkbox is ticked and return the result to the view. This works fine for one set of checkboxes but I can't get my head around how to do it with multiple checkbox groups.
Here's the function in my controller that handles that:
$scope.getSelectedBrands = function() {
$scope.brandsselected = $filter('filter')($scope.brands, {checked: true});
var senddata = $filter('filter')($scope.brands, {checked: true});
$http.post('/brands', senddata).success(function(data, status, response) {
$scope.cars = data;
});
}
I don't know if using a different but very similar function for each checkbox group would make too much sense and doesn't seem very DRY. At the moment each checkbox group e.g brand would have it's own URL to post to to return the set of results. Also since a checkbox in any of the checkbox groups could be ticked first the initial data would be returned from a different function depending on what checkbox was checked.
Is there a better way to approach this? I also would need to allow users to uncheck a checkbox and repopulate the list again.
Hopefully that makes sense.
Thanks
OK I think I was approaching it the wrong way. All I needed to do was to create a function like the below and fire it whenever a checkbox is checked. It just gets call the form data.
$scope.getFormdata = function(){
var formdata = [{
brands : $filter('filter')($scope.brands, {checked: true}),
types : $filter('filter')($scope.types, {checked: true})
}];
console.log(formdata);
}
Very straightforward.

Linking MVC In AngularJS

I have a basic application in AngularJS. The model contains a number of items and associated tags of those items. What I'm trying to achieve is the ability to filter the items displayed so that only those with one or more active tags are displayed, however I'm not having a lot of luck with figuring out how to manipulate the model from the view.
The JS is available at http://jsfiddle.net/Qxbka/2 . This contains the state I have managed to reach so far, but I have two problems. First off, the directive attempts to call a method toggleTag() in the controller:
template: "<button class='btn' ng-repeat='datum in data' ng-click='toggleTag(datum.id)'>{{datum.name}}</button>"
but the method is not called. Second, I'm not sure how to alter the output section's ng-repeat so that it only shows items with one or more active tags.
Any pointers on what I'm doing wrong and how to get this working would be much appreciated.
Update
I updated the method in the directive to pass the data items directly, i.e.
template: "<button class='btn' ng-repeat='datum in data' ng-click='toggle(data, datum.id)'>{{datum.name}}</button>"
and also created a toggle() method in the directive. By doing this I can manipulate data and it is reflected in the state HTML, however I would appreciate any feedback as to if this is the correct way to do this (it doesn't feel quite right to me).
Still stuck on how to re-evaluate the output when a tag's value is updated.
You can use a filter (docs) on the ng-repeat:
<li ng-repeat="item in items | filter:tagfilter">...</li>
The argument to the filter expression can be many things, including a function on the scope that will get called once for each element in the array. If it returns true, the element will show up, if it returns false, it won't.
One way you could do this is to set up a selectedTags array on your scope, which you populate by watching the tags array:
$scope.$watch('tags', function() {
$scope.selectedTags = $scope.tags.reduce(function(selected, tag) {
if (tag._active) selected.push(tag.name);
return selected;
}, []);
}, true);
The extra true in there at the end makes angular compare the elements by equality vs reference (which we want, because we need it to watch the _active attribute on each tag.
Next you can set up a filter function:
$scope.tagfilter = function(item) {
// If no tags are selected, show all the items.
if ($scope.selectedTags.length === 0) return true;
return intersects($scope.selectedTags, item.tags);
}
With a quick and dirty helper function intersects that returns the intersection of two arrays:
function intersects(a, b) {
var i = 0, len = a.length, inboth = [];
for (i; i < len; i++) {
if (b.indexOf(a[i]) !== -1) inboth.push(a[i]);
}
return inboth.length > 0;
}
I forked your fiddle here to show this in action.
One small issue with the way you've gone about this is items have an array of tag "names" and not ids. So this example just works with arrays of tag names (I had to edit some of the initial data to make it consistent).

knockout checkbox binding selected value

I am learning knockout and was trying to build a page that will build a list of selectable users.
JSFiddle: http://jsfiddle.net/Just/XtzJk/3/ (I am unable to get the data assignment right).
The data assignment is working in my page as I make a call to Controller, like below and it binds to the controls as expected
$.getJSON("/Wizard/GetUsers",function(allData){
var mappedUsers = $.map(allData.AllUsers, function(item){return new User(item)});
self.AllUsers(mappedUsers);
if(allData.SelectedUsers != null){
var mappedSelectedUsers = $.map(allData.SelectedUsers, function(item){return new User(item)});
self.SelectedUsers(mappedSelectedUsers);}
});
Problems:
a.) What's wrong with the JSFiddle I wrote? Got it working.
b.) In my code I am able to get the function for selected checkbox invoked but I am unable to get the value stored in the "User" parameter that I receive in the function. In Chrome JS console I can see the user object has the right value stored, I just am unable to retrieve it. Got this by doing ko.toJS().
Thanks.
EDIT:
Ok, I got my JSFiddle working, I had to select Knockout.js in the framework. The updated fiddle: http://jsfiddle.net/Just/XtzJk/5/
Also, for getting the selected checkboxe's value I did
ko.toJS(user).userName
But I think I'll take the approach of selecting values from a list and then on click move them to another "Selected" list and remove the values from the previous ones. Got this idea from this post: KnockoutJS: How to add one observableArray to another?
OK, I think I've got the solution you need...
I started by setting up an observable array of selectedUserNames, and I applied this to the <li> elements like this:
<input type="checkbox"
name="checkedUser"
data-bind="value: userName, checked:$root.selectedUserNames" />
[Note: it's important to declare the value before declaring the checked binding, which threw me for a bit… ya learn something new every day!]
Why bind an array of userName values to the checked binding? Well, when an array is passed to the checked binding, KO will compare the value of each checkbox to the values in the checked array and check any checkbox where its value is in that array. (Probably explained better in the KO documentation)
Then, while I left the observableArray for SelectedUsers, I set up a manual subscription to populate it, like so:
self.selectedUserNames.subscribe(function(newValue) {
var newSelectedUserNames = newValue;
var newSelectedUsers = [];
ko.utils.arrayForEach(newSelectedUserNames, function(userName) {
var selectedUser = ko.utils.arrayFirst(self.AllUsers(), function(user) {
return (user.userName() === userName);
});
newSelectedUsers.push(selectedUser);
});
self.SelectedUsers(newSelectedUsers);
});
[I had originally tried to set up a dependent observable (ko.computed) for selectedUserNames with functions for both read and write, but the checkbox wasn't having it.]
This subscription function examines the new selectedUserNames array, looks up the user from AllUsers whose userName matches a value in that selectedUserNames array, and pushes matching User objects to the SelectedUsers array… well, actually it pushes each matching User to a temp array and then that temp array is assigned to SelectedUsers, but the goal is met. The SelectedUsers array will now always contain what we want it to contain.
Oh, I almost forgot… here's the fiddle I created, so you've got the full solution: http://jsfiddle.net/jimmym715/G2hxP/
Hope this helps, but let me know if you have any questions

Resources