How to set AngularJS select value based from model value? - angularjs

I have an array that contains my supplier list:
$scope.supplierList = [{id=1, name='supplier 1'}, {id=2, name='supplier 2'}];
Then I have a model(lets call it product) that I got from a service with values like:
id=1
name=Product A
supplier=supplier 1(this is an object reference)
When I load the product to a form for editing, I need to have 'supplier 1' be the selected option, I have tried different steps but nothing seems to work, my code is as follows:
<select ng-model="product.supplier" ng-options="supplier.name for supplier in supplierList">
<option value="">Select a Supplier</option>
</select>
And in the controller:
$scope.update = function (id) {//user clicks edit button, form modal shows up with filled up values because of model binding
$scope.product = Product.get({id: id});//retrieving the product
$scope.product.supplier = $scope.supplierList[1];//trying out if i can change the selected to something else, never works! Always defaults to select a supplier
//supplier is always deemed undefined
};
Any ideas? thanks!

I'm guessing that Product is created with $resource(..). What isn't obvious is that the Product.get(..) call will return an (almost) empty object at first, which will be filled with the data from the response once it is received. At the same time, any previous values in the object will be removed.
To set the supplier you would need to do something like
$scope.product = Product.get({id: id}, function (product) {
product.supplier = $scope.supplierList[1];
})
Take a look at https://docs.angularjs.org/api/ngResource/service/$resource and do consider going with basic $http calls instead. ngResource is nice and comfy at first, but it's abstractions soon becomes problematic as illustrated by your problem.

I suppose that Product.get({id: id}); is a call to a resource, so it will be executed asynchronously. Data that will come from the server will override your "$scope.product.supplier" assignment.
Try to change this code to:
$scope.update = function (id) {
$scope.product = Product.get({id: id}, function(){
$scope.product.supplier = $scope.supplierList[1];
});
};

Related

Angular ng-options ONLY works when ng-repeat operates on same model

I am working on an ASP.Net MVC page that uses a dropdown which currently uses the ng-repeat tag. I'm working to solve the problem where the dropdown does not correctly select the current model value when the page loads so I switched the dropdown to use ng-options.
My new dropdown looks like this:
<select id="one" ng-model="data.CommandProvider"
ng-options="item.ident as item.ProviderName for item in providers">
</select>
When the page loads my new select displays as a large empty rectangle. It's approximately the width and height to match the three items it should contain but it's not a dropdown. No options and no dropdown button.
However, when I follow the new dropdown with the old dropdown like so:
<select id="one" ng-model="data.CommandProvider"
ng-options="item.ident as item.ProviderName for item in providers">
</select>
<select id="two" ng-model="data.CommandProvider">
<option ng-repeat="opt in providers track by opt.ident"
value="{{opt.ident}}">
{{opt.ProviderName}}
</option>
</select>
BOTH dropdowns load their options correctly but NEITHER dropdown correctly displays the current value of the model.
If the page only contains the old dropdown based on ng-repeat that dropdown displays correctly.
I don't understand what could cause such behavior in ng-options and what would cause the dropdowns to never correctly represent the model on page load?
ADDED: So the previous author had mismatched HTML tags and that was causing the error with the new dropdown - why it didn't break the original I don't know. That being said the new dropdown STILL does not display the value of the model when the page is loaded.
So after working this problem for too long this is the solution that worked for me:
There are three http requests in play: one for each select input and one for the model data and whenever the model data returned before the select data one or both of the select would be out of sync with the model. My solution was to synchronize the data requests.
The select inputs:
<select ng-model="data.Connection">
<option ng-repeat="opt in connections track by opt.ident" value="{{opt.ident}}">{{opt.ConnectionName}}</option>
</select>
<select id="two" ng-model="data.CommandProvider">
<option ng-repeat="opt in providers track by opt.ident" value="{{opt.ident}}">{{opt.ProviderName}}</option>
</select>
The javascript:
// connection and provider data variables
$scope.providers;
$scope.connections;
// function to retrieve connection dropdown data
$scope.getConnections = function () {
$scope.getApiData('GetConnections',
{}, function (data) {
$scope.connections = data;
});
}
// function to retrieve the provider dropdown data
$scope.getProviders = function () {
$scope.getApiData('GetProviders',
{}, function (data) {
$scope.providers = data;
});
}
// retrieve the primary page data
$scope.getCommandData = function () {
$scope.getApiCommandDataV1('GetCommandById',
{Id: #ViewBag.ID},
function (data) {
$scope.data = data;
});
}
// retrieves data from the core api
$scope.getApiData = function (alias, params, successFn, errorFn = null) {
var qdata = { SqlAlias: alias, SqlParameters: params };
if (errorFn == null) {
$http.post('/api/request', qdata).success(successFn);
} else {
$http.post('/api/request', qdata).success(successFn).error(errorFn);
}
}
// function to request the data for the page
$scope.init = function () {
$scope.getConnections();
}
// set a watch on the connections variable to fire when the data
// returns from the server - this requests the Providers information.
$scope.$watch('connections', function (newValue, oldValue, scope) {
if (newValue == undefined || newValue == null)
return;
$scope.getProviders();
}, true);
// set a watch function on the providers variable to fire when the data
// returns from the server - this requests the primary data for the Command.
$scope.$watch('providers', function (newValue, oldValue, scope) {
if (newValue == undefined || newValue == null)
return;
$scope.getCommandData();
}, true);
// initialize the page logic and data
$scope.init();
As you can see my use of $scope.$watch forces the data requests to be synchronous rather than asynchronous and using this method insures the two select inputs are correct every time the web page loads.
Feel free to comment on my coding here as there may be better ways to address this problem - just keep in mind that I have only been working with JavaScript and Angular for about a month.

Angular model and select not working

I've been having an issue for a day or two trying to get a select element working with my angular model.
I have a driving log, and one of the fields is truck. The value should be the truck id, which is received and sent from/to an API.
I've tried a couple methods, using ng-repeat to generate options, as well as using ng-options. The problem I ran into with the ng-repeat method was that I wasn't able to set the selected item, even with a lot of tinkering and doing things that shouldn't have to be done, and bad practice.
The second method I believe is the correct one, and it's using ng-options.
<select ng-model="timeLog.truck" convert-to-number
ng-options="truck.description for truck in trucks track by truck.id">
<option value="">Choose Truck</option>
</select>
.controller('EditTimeLogCtrl', function($scope, $stateParams, $location, timeLog, LogEntry, localStorageService) {
// edit an individual time log
$scope.timeLog = timeLog;
$scope.trucks = localStorageService.get('trucks');
$scope.saveTimeLog = function() {
LogEntry.update($scope.timeLog, function(data) {
$location.path('/tab/logs/edit');
});
}
})
Everything else in my timeLog model works, and the value in the model is an integer.
For some reason, I can't get the initial value to set correctly even though the docs specify to use this to set a default value.
The other issue I have when using ng-options is that when I submit the form, it uses the truck object {"description": "big red", "id": 7, ... } instead of the value of the option, which would just be 7. The API is expecting the id, so that does not work.
I've found 3 stackoverflow articles about that, and they all give various answers which don't really solve the problem.
This seems like a very common use case, maybe I'm thinking about it the wrong way? I just have a model which has a dropdown/select field and I need that to populate to what the selected value is if the model already exists (i.e. edit form), and pass the id value in the model save.
Your ngOptions syntax is a bit off - it's value as text for obj in arr - so change yours to:
ng-options="truck.id as truck.description for truck in trucks track by truck.id"
And then set your model to the id of the object you want selected:
$scope.timeLog.truck = 7; //truck id 7 selected.
If you want the whole object as the value:
ng-options="truck as truck.description for truck in trucks track by truck.id"
And set the whole object:
$scope.timeLog.truck = $scope.trucks[0];
Make sure your timeLog.truck IS (===) the actual object in the trucks array (same referenced object)
angular
.module('app', [])
.controller('myController', myController);
function myController($scope) {
$scope.trucks = [{
"description": "big red",
"id": 7
}, {
"description": "big yellow",
"id": 6
}];
$scope.timeLog = {truck: $scope.trucks[0]};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular.js"></script>
<div ng-app="app">
<div ng-controller="myController">
<select ng-model="timeLog.truck" ng-options="truck.description for truck in trucks">
<option value="">Choose Truck</option>
</select>
</div>
</div>

One way binding angularjs

I have a list of employees in a select, where the user can pick an employee and edit its details. Then he triggers an ajax call through a button, so the server can update the record in the DB.
I am binding with ngModel the fields and the data from my list of employees, but it is problematic if the update fails on the database side, because my list of employees is updated through the two way binding.
Is there a way to initialize my fields when the user picks an element in the select and update my employee list only when I get response?
Here is my explicit code from my directive (view):
select(ng-model='selectedEmployee' ng-options="employee.name for employee in employees")
form(role='form')
input(type='input' ng-model='selectedEmployee.userId')
input(type='input' ng-model='selectedEmployee.name')
button (type='button' ng-click='updateEmployee()') update
and the directive
app.directive('employeeList', ['employeeServices',
function(employeeServices) {
var employeeListController = function($scope) {
employeeServices.getEmployees()
.success(function(result) {
$scope.employees = result.data
})
.error(function(err) {
})
$scope.selectedEmployee = null
$scope.updateEmployee = function() {
employeeServices.updateEmployee({
userId: $scope.selectedEmployee.userId,
name: $scope.selectedEmployee.name
})
.success(function(data) {
//I want to update my $scope.employees here
})
.error(function(data) {
//Otherwise I show some error message
})
.then(function() {
$scope.selectedEmployee = {}
})
}
}
return {
...
controller: employeeListController
}
}
])
Solution
So in order to solve the problem I used angular.copy along with ng-change. I've added ng-change to the select, where I copied the selectedEmployee to selectedEmployeeDirty that I supplied as model for my form. Then in the service's callback I updated the selectedEmployee.
Very simple. Object, in javascript, are shared throught a "reference".
In fact, this is a C pointer - or something like that -, how share the memory location of your object.
If you do this:
var a = {},
b = a;
a.toto = true;
console.log(b);
You will see
b = { toto: true }
Keep that in mind.
Now, how can we isolate your edited object, without updating the original one? Make a copy! angular.copy is a friend, and would duplicate every properties of src to the dst.
Use the ng-model as you did, save change, and, only on callback, update the original one :-)

Issue with Angularjs Dropdown and a custom filter

I'm having an issue using a dropdown that is populated with ng-repeat option values or even when using ng-options.
Basically I'm pulling a list of subsidiaries from the database. I then have a dropdown to choose a company, which in turn should populate the subsidiary dropdown with subsidiaries of the chosen company. Since many of the subsidiaries are of the same company, if I try and pull the the company name in ng-repeat, I get the same company several times. So I have created a custom filter that filters out the companyName and companyID of each company listed only once.
Everything works in the theory that when I change the value of the company dropdown, the correct subsidiaries are listed. However the value shown in the company box is stuck on the first option listed and will not change. If I remove the custom filter and allow it to list all the repeat names, the box displays correctly.
My first thought is to make a separate HTTP call that would just get companies from my companies table, but I would think I want to limit HTTP calls to as few as possible. Plus it would seem that I should be able to accomplish this.
What concept am I not grasping that prevents this from displaying correctly when I use my filter and what should I do to fix this?
thanks
HTML:
<div class="col-sm-5">
<select ng-model ="parentCompany" name="company">
<option ng-repeat="company in companies | uniqueCompanies:'companyName'" value="{{company.id}}" >{{company.name}}</option>
</select>
</div>
<div class="col-sm-5">
<select name="subsidiary">
<option ng-repeat="subsidary in companies" value="{{subsidary.subID}}" ng-hide="$parent.parentCompany !== subsidary.companyID">{{subsidary.subName}}</option>
</select>
</div>
Controller:
getCompanies();
function getCompanies(){
$http.get("get.php?table=getcompanies").success(function(data) {
$scope.companies = data;
});
}
Filter:
.filter("uniqueCompanies", function() {
return function(data, propertyName) {
if (angular.isArray(data) && angular.isString(propertyName)) {
var results = [];
var keys = {};
for (var i = 0; i < data.length; i++) {
var val = data[i][propertyName];
var val2 = data[i]['companyID'];
if (angular.isUndefined(keys[val])) {
keys[val] = true;
results.push({'name':val, 'id':val2});
}
}
return results;
} else {
return data;
}
};
});
Sample Data :
[{"subID":null,"subName":null,"companyID":"1","companyName":"DWG"},
{"subID":null,"subName":null,"companyID":"2","companyName":"Vista"},
{"subID":"1008","subName":"Data Services","companyID":"3","companyName":"Medcare"},
{"subID":"1009","subName":"Companion","companyID":"3","companyName":"Medcare"},
{"subID":"1010","subName":"GBA","companyID":"3","companyName":"Medcare"},
{"subID":"1011","subName":"PGBA","companyID":"3","companyName":"Medcare"},
{"subID":"1013","subName":"Health Plan","companyID":"3","companyName":"Medcare"},
{"subID":"1014","subName":"PAISC","companyID":"3","companyName":"Medcare"},
{"subID":"1015","subName":"CGS","companyID":"3","companyName":"Medcare"}]
You are creating new objects in your filter with different properties so they will be different every time. You can you track by as mentioned by others. Since filters are executed every digest cycle you may want to set up a $watch and only create a new list of unique companies when your companies change. I actually get the 10 $digest() iterations reached error without doing this.
$scope.$watchCollection('companies', function(newValue) {
$scope.filteredCompanies = $filter('uniqueCompanies')($scope.companies,
'companyName');
});
You could also set a watch on parentCompany and create the list of subsidiaries only when it changes, as well as clear out the value you have for subsidiaryCompany:
$scope.$watch('parentCompany', function(newValue) {
$scope.subsidiaries = [];
for (var i = 0; i < $scope.companies.length; i++) {
var c = $scope.companies[i];
if (c.companyID === newValue) {
$scope.subsidiaries.push(c);
}
}
$scope.subsidiaryCompany = undefined;
});
I may not be fully understanding you're issue here, but it looks like you could filter the data when you get it. Such as ...
function getCompanies(){
$http.get("get.php?table=getcompanies").success(function(data) {
$scope.companies = data.reduce(function (prev, cur) {
// some code for skipping duplicates goes here
}, []);
});
}
Array.reduce may not be the best way to get a new array without duplicates, but that's the general idea, anyway.

angular-ui-select2 and breezejs: load ajax list after typing in 2 characters

I have a project where I'm using BreezeJS to fetch data from my webserver. I'm using AngularJS with the ui-select2 module. Currently, I have it where when I load my page, breezejs makes a call to fetch the data that I dump into a scope variable. From there, select2 can easily make the reference to it and build accordingly.
If I want to ajaxify things, it gets really tricky. I want to have the ability to use select2's ajax or query support, but instead of using it to fetch data, I want to use breezejs to do it. So during a page load, nothing is loaded up until I start typing in X minimum characters before it makes an ajax fetch.
Constraints:
I do not want fetch data using select2's "ajax". I want BreezeJS to handle the service calls. When I use ajax, it makes an ajax call everytime I press a character in order to filter the results (and resemble autocomplete). I just want the list to load up once and use the native filtering after that.
Here is what I have so far:
breezejs - StateContext.JS
m.app.factory('StateContext', ['$http', function ($http) {
configureBreeze();
var dataService = new breeze.DataService({
serviceName: "/Map/api",
hasServerMetadata: false
});
var manager = new breeze.EntityManager({ dataService: dataService});
var datacontext = {
getAllStates: getAllStates
};
return datacontext;
function getAllStates() {
var query = breeze.EntityQuery
.from("States");
return manager.executeQuery(query);
}
function configureBreeze() {
breeze.config.initializeAdapterInstances({ dataService: "webApi" });
}
}]);
This works and returns my json object correctly.
Here is how I call the service:
m.app.controller('LocationCtrl', ['$scope', 'StateContext', function ($scope, StateContext) {
$scope.getAllStates = function () {
StateContext.getAllStates().then(stateQuerySucceeded).fail(queryFailed);
}
$scope.getAllStates();
$scope.states = [];
function stateQuerySucceeded(data) {
data.results.forEach(function (item) {
$scope.states.push(item);
});
$scope.$apply();
console.log("Fetched States");
}
function queryFailed(error) {
console.log("Query failed");
}
$scope.select2StateOptions = {
placeholder: "Choose a State",
allowClear: true,
minimumInputLength: 2
};
}
and here is my html:
<div ng-app="m" id="ng-app">
...
...
<select ui-select2="select2StateOptions" ng-model="LocationModel.State">
<option value=""></option>
<option ng-repeat="state in states" value="{{state.id}}">{{state.name}}</option>
</select>
</div>
Currently the html select2 control loads up when the page loads. But I want to have it so when I type in more than 2 characters, I'll be able to make the call to $scope.getAllStates(); as an ajax call. BreezeJS already uses ajax natively when configuring the BreezeAdapter for webapi.
I was thinking about using select2's ajax, or query calls.. but I'd rather use breeze to fetch the data, since it makes querying extendable, and I don't want to violate my design pattern, or make the code harder to maintain, and I don't want the ajax calls to be made everytime I enter a new character into the textbox, I just want it to occur once.
Close attempt:
changed my html to:
<!-- Select2 needs to make this type="hidden" to use query or ajax, then it applies the UI skin afterwards -->
<input type="hidden" ui-select2="select2StateOptions" ng-model="LocationModel.State" /><br />
in my controller, changing select2StateOptions:
$scope.select2StateOptions = {
placeholder: "Choose a State",
allowClear: true,
minimumInputLength: 2,
query: function (query) {
debugger;
var data = StateContext.getAllStates().then(stateQuerySucceeded).fail(queryFailed);
}
};
Here's the problem. BreezeJS uses a Q library, which makes use of a thing called a "promise"; which is a promise that data will be returned after making the ajax call. The problem with this, the query function is expecting data to be populated, but the promise to call the "stateQuerySucceeded" function is made after returning from the query function.
So it hits the query function first. Then hits getAllStates(). Returns from the query (nothing is populated), then "stateQuerySucceeded" is called after that.
In otherwords, even though I have been able to fetch data, this is done too late.. select2's query function did not receive the data at the right time, and my html select is hanging on "Searching ... " with a search spinner.gif.
I don't really know this angular-ui-select2 control. I think the relevant part of the documentation is this example:
$("#e5").select2({
minimumInputLength: 2,
query: function (query) {
var data = {results: []}, i, j, s;
// simulate getting data from the server
for (i = 1; i < 5; i++) {
s = "";
for (j = 0; j < i; j++) {s = s + query.term;}
data.results.push({id: query.term + i, text: s});
}
query.callback(data);
}
});
I will leave aside the fact that you don't seem to be interested in using the two-or-more characters that the user enters in your query (maybe you just left that out). I'll proceed with what seems to me to be nonsense, namely, to fetch all states after the user types any two letters.
What I think you're missing is the role of the query.callback which is to tell "angular-ui-select2" when the data have arrived. I'm guessing you want to call query.callback in your success function.
$scope.select2StateOptions = {
placeholder: "Choose a State",
allowClear: true,
minimumInputLength: 2,
query: function (query) {
StateContext.getAllStates()
.then(querySucceeded).catch(queryFailed);
function querySucceeded(response) {
// give the {results:data-array} to the query callback
query.callback(response);
}
function queryFailed(error) {
// I don't know what you're supposed to do.
// maybe return nothing in the query callback?
// Tell the user SOMETHING and then
query.callback({results:[]});
}
}
};
As I said, I'm just guessing based on a quick reading of the documentation. Consider this answer a "hint" and please don't expect me to follow through and make this actually work.

Resources