I am trying to use a pair of Kendo dropdown lists to filter a dataset in AngularJS. One uses a static ObservableArray datasource to filter the dataset by status; the other datasource is a set of distinct values from the "regionID" column of the dataset, also stored as an ObservableArray. When the status dropdown changes, the region dropdown is supposed to reload the list of regions from the newly-filtered data. This works, but the selected value ends up blanked out, even though the model that's supposed to represent the selected value still has the correct value, and the data is still correctly filtered by that same value. Opening the dropdown and then clicking off of it causes it to then display that value as it should.
Here is what the HTML looks like:
<select name="filterByRegion" style="width: 180px;" class="form-control" ng-model="selectedRegion" ng-change="onRegionFilterChange(selectedRegion)" kendo-drop-down-list k-options="regionFilterOptions"></select>
<select name="accountStatus" style="width: 180px;" class="form-control" ng-model="status" ng-change="onAccountStatusChange(status)" kendo-drop-down-list k-options="accountStatusOptions"></select>
Here are the two "onChange" methods from the controller:
$scope.onAccountStatusChange = function(status) {
$scope.status = status;
updateRegionFilterList();
};
$scope.onRegionFilterChange = function(selectedRegion) {
$scope.selectedRegion = selectedRegion;
};
And lastly, here is the updateRegionFilterList method:
var updateRegionFilterList = function () {
$scope.regions.empty();
angular.forEach($scope.accounts, function(account) {
if (account.reviewStatus === $scope.status) {
if ($scope.regions.indexOf(account.regionID) <= -1) {
$scope.regions.push(account.regionID);
}
}
});
$scope.regions.sort();
$scope.regions.unshift("Filter By Region:");
if ($scope.regions.indexOf(tempRegion) <= -1) {
$scope.selectedRegion = "Filter By Region";
}
};
I have tried many ways of working around this, including trying to preserve and reload the selectedRegion value, splicing out unneeded regions instead of emptying the ObservableArray, etc., and I have checked everything in the debugger and all of the values are correct (no obvious race conditions either).
Any ideas?
I figured it out. I had mixed kendo and angular configurations with the dropdowns and was relying on an angular process to update the data, which wasn't updating the Kendo MVVM stuff properly. Switching it to pure Kendo did the trick.
Related
I have a checkbox that I'd like to set the indeterminate state to based on the states of other checkboxes. When I'm on the page that the checkboxes are all in, it updates as expected (i.e. the checkbox is found). But when I navigate to that from another page, my method does not find the checkbox (i.e. returns null).
When I debug in Chrome devtools, I notice
let checkBoxWithIndeterminateState;
let checkbox = false;
fireWhenCheckBoxChanged() {
// returns null when navigating from another page but not when on its own page
checkBoxWithIndeterminateState = document.getElementById('checkBoxWithIndeterminateState')
checkBoxWithIndeterminateState.indeterminate = true
}
Template:
<input type="checkbox" id="checkBoxWithIndeterminateState" data-ng-model="checkbox">
How do I wait until the new template has loaded before my method tries to find the checkbox? I've read some suggestions to use this._$scope.$on('$viewContentLoaded'... but this doesn't work.
Thanks!
What about adding an ng-init directive to your target checkbox and do your logic in it, this way you are sure the element is there, here is a suggestion:
<input type="checkbox" ng-init="initTragetCheckbox()">
In your controller
$scope.initTragetCheckbox = function () {
// your code to execute for other checkboxes
var checkbox1 = document.getElementById("checkbox1");
var checkbox2 = document.getElementById("checkbox2");
....
}
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.
I'm working on an app using AngularJS and Bootstrap UI. I've been fumbling my way through using the Typeahead control in Bootstrap UI.
Here's my Plunker
My challenge is I want the user to have the option of choosing an item, but not required to do so. For instance, right now, if you type Test in the text field and press "Enter", Test will be replaced with Alpha. However, I really want to use Test. The only time I want the text to be replaced is when someone chooses the item from the drop down list. My markup looks like the following:
<input type="text" class="form-control" placeholder="Search..."
ng-model="query"
typeahead="result as result.name for result in getResults($viewValue)"
typeahead-template-url="result.html" />
How do I give the user the option of choosing an item, but allow them to still enter their own text?
The issue is that both Enter and Tab confirm the selection of the currently highlighted item and Typeahead automatically selects an item as soon as you start to type.
If you want, you can click off the control to lose focus or hit Esc to exit out of typeahead, but those might be difficult to communicate to your users.
There's an open request in Bootstrap Ui to not auto select / highlight the first item
One solution is to populate the first item with the contents of the query thus far, so tabbing or entering will only confirm selection of the current query:
JavaScript:
angular.module('plunker', ['ui.bootstrap'])
.filter('concat', function() {
return function(input, viewValue) {
if (input && input.length) {
if (input.indexOf(viewValue) === -1) {
input.unshift(viewValue);
}
return input;
} else {
return [];
}};})
HTML:
<input type="text"
ng-model="selected"
typeahead="state for state in states | filter:$viewValue | limitTo:8 | concat:$viewValue"
class="form-control">
Demo in Plunker
I came across this same situation and found no good answers so I implemented it myself in ui-bootstrap Here is the relevant answer. This is probably not the best route to take, but it does get the job done. It makes the first result in the typeahead to be what you're currently typing, so if you tab or enter off of it, it's selected -- you must arrow-down or select another option to get it.
Here is the modified ui-bootstrap-tpls.js file
I added a mustMouseDownToMatch property/attribute to the directive, like:
<input type="text" ng-model="selected" typeahead="item for item in typeaheadOptions | filter:$viewValue" typeahead-arrow-down-to-match="true">
And the javascript:
var mustArrowDownToMatch = originalScope.$eval(attrs.typeaheadArrowDownToMatch) ? originalScope.$eval(attrs.typeaheadArrowDownToMatch) : false;
I also added this function which will put the current text into the first item of the typeahead list, and make it the selected item:
var setFirstResultToViewValue = function (inputValue) {
scope.matches.splice(0, 0, {
id: 0,
label: inputValue,
model: inputValue
});
}
And that is called in the getMatchesAsync call in the typeahead directive:
var getMatchesAsync = function(inputValue) {
// do stuff
$q.when(parserResult.source(originalScope, locals)).then(function(matches) {
// do stuff
if (matches.length > 0) {
// do stuff
}
if (mustArrowDownToMatch) {
setFirstResultToViewValue(inputValue);
scope.activeIdx = 0;
setTypeaheadPosition();
}
// do stuff
};
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.
EDIT: for those of you who don't want to go through the code, I'm basically passing the form a "node" object with node.selectedAnswer = "4,1,4" or some string like that. The form has radio buttons and one of the buttons has a value "4,1,4". the radio button also has ng-checked="node.selectedAnswer" expression. But that doesn't work. I know for sure that node.selectedAnswer has the appropriate value.
I have a series of radio button questions that I'm asking the user. I want them to be able to go previous and next. I'm using a stack to store the data retrieved from ajax call, as well as selectedAnswer when they select an option and click next. I've commented the code itself to explain the situation where I can. Everything seems to be working, except ng-checked is just not picking up node.selectedAnswer, even though I can output {{node.selectedAnswer}} properly to the page.
<div class="container-fluid" ng-app="AccountRequest" ng-controller="GameNode" ng-init="outside={}">
<div class="row-fluid">
<div class="span2"></div>
<div class="span10">
<form>
<!-- node.selectedAnswer displays the selectedAnswer correctly when clicking previous and going back.
However, ng-checked is somehow not selecting the appropriate radio button. -->
<span>{{node.Question.Text}} selected answer: {{node.selectedAnswer}}</span>
<div class="radio" ng-repeat="answer in node.Answers">
<input type="radio" id="answerGroup" name="answerGroup" ng-checked="node.selectedAnswer" ng-model="outside.selectedAnswer"
value="{{answer.BranchId}},{{node.LeafId}},{{answer.Id}}"/> {{answer.Text}}
</div>
<div>
<input type="button" ng-click="previous()" value="Previous"/>
<input type="button" ng-click="next(outside.selectedAnswer)" value="Next"/>
</div>
</form>
</div>
</div>
</div>
//below is the script
app.controller('GameNode', function ($scope, $http) {
var nodes = [];
function load(branchId, leafId, answerId) {
$http.get("/AccountRequest/GetNode?branchId=" + branchId +
"&leafId=" + leafId +
"&answerId=" + answerId)
.success(function (data) {
//get data and push it in the stack
nodes.push(data);
$scope.node = data;
});
}
function populateValues(selectedAnswer) {
var answer = null;
if (selectedAnswer === undefined || selectedAnswer == null)
selectedAnswer = "0,0,0";
//when next is clicked, retrieve the selectedAnswer from form and store it in current node as a property.
if (nodes.length > 0) {
var curNode = nodes.pop();
curNode.selectedAnswer = selectedAnswer;
nodes.push(curNode);
}
answer = selectedAnswer.split(',');
if (answer != null) {
load(answer[0], answer[1], answer[2]);
}
}
$scope.next = populateValues;
$scope.previous = function () {
//when previous is clicked, pop the current node out and throw it away.
//then pop the previous node out, read it, and push it back in as current node.
if (nodes.length > 1) {
nodes.pop();
var prevNode = nodes.pop();
nodes.push(prevNode);
$scope.node = prevNode;
}
};
populateValues();
});
Older Answer - This works, (was marked correct) but using $parent can get a bit messy in nested repeats.
In this instance, you don't need to use ng-checked at all. Since this is a radio group, the checked attribute will be bound to the model. If the model is bound to the value of an individual radio button, then your ability to change which button is "checked" becomes very simple.
Here is a plunk that demonstrates the concept.
So in your case a few changes need to be made.
1. Get rid of 'id' attribute - the ID must be unique for each element.
2. Each item created in an ng-repeat creates its own child scope. So to access the original model, "$parent" must be invoked.
<input type="radio" name="answerGroup" ng-model="$parent.someAnswerAttribute"
value="{{answer.BranchId}},{{node.LeafId}},{{answer.Id}}"/>
In your controller define the model as you already did, then modify it to be tied to a value of a button, which in your case will be a bit lengthy, since you have multiple attributes within your value.
$scope.someAnswerAttribute = // exactly what the value of a radio button would be.
Again, the plunker above reflects this concept. Hope this helps!
..
..
Edit - Better Answer:
Since the ng-repeat creates its own child scope, and two-way binding is necessary, the ng-model should be referencing an object instead of a primitive. In other words, if the model was $scope.myModel="Biff", the child scope can not access that without invoking $parent (in the answer below). However, if the model is referencing a property of an object, the child will receive prototype inheritance of that object. (I think I said that right).
So using the older answer example, we can change:
From this in the parent controller:
$scope.someAnswerAttribute = "Biff";
To this in the parent controller:
$scope.someAnswerAttribute = {value: "Biff"}
And in the radio group:
<input type="radio" name="answerGroup" ng-model="someAnswerAttribue.value"
value="{{answer.BranchId}},{{node.LeafId}},{{answer.Id}}"/>
This plunk is forked from the older answer and demonstrates model as an object property.