Programmatically Select AngularJS Typeahead Option - angularjs

I have a AngularJS Typeahead that retrieves matches asynchronously. When a barcode is scanned into the field, it returns the matching result, but the user still has to select it. I would like to automatically select the result if it's an exact match. I see that the typeahead has a select(idx) function, but am not sure how to get a reference to it from my controller.
I was envisioning something like this:
$scope.SearchItems = function (term) {
return $http.get('api/Items/Search', {
params: {
term: term
}
}).then(function (response) {
if (response.data.length == 1 && response.data[0].Code == term) {
// Somehow inform typeahead control to select response.data[0]
}
return response.data;
});
};

I had a similar issue and never figured how to access the typeahead's select(idx), but I managed to get this functionality working. Here's my hacky workaround....
$promise.then(function(res) {
angular.forEach(res, function(item, key) {
// if we have an exact match
if (item.title === term) {
// update model
$scope.src = item;
// find item in dropdown
var elm = '[id*=option-' + key + ']';
var opt = angular.element(document.querySelectorAll(elm));
//call click handler outside of digest loop
$timeout(function() {
opt.triggerHandler('click');
}, 0);
}
});
// return async results
return res;
});
Basically we just update our model manually, locate the element in our dropdown and then fire off the 'click' handler. Make sure you wrap the triggerHandler call in a $timeout() set to zero, otherwise you will get a $rootScope:inprog error since digest is already in progress.

Related

AngualrJS $watchCollection triggers even when arrays are equal

I am trying to implement an auto sync between my angularJS model and my database.
I am running the following function every ten seconds to get data from from database in which I update my variable products_copy:
$interval(function(){$scope.getProductsSync();},10000);
$scope.getProductsSync = function() {
var q = $q.defer();
$http({
url: '/getData/getProducts.php',
method: "POST",
data: {"user_id":$scope.user_id}
})
.then(function(response) {
if(response.data.valid)
{
console.log('Products sync complete: '+new Date().toLocaleTimeString());
console.log(response.data.products);
$scope.products_copy = response.data.products;
q.resolve('Products sync complete');
}
else if(response.data.msg=="offline")
{
console.log('Products sync offline');
q.resolve('Products sync offline');
}
else
{
console.log('Products sync error');
console.log(response);
q.reject('Products sync error');
}
},
function(response) {
console.log('Products sync error');
console.log(response);
q.reject('Products sync error');
});
return q.promise;
}
Whenever there is a change in this data I want to update my actual product list in my model which is defined by the array products. Hence, I am using the service $watchCollection to detect a change in the data and update my model when there is a change. However this change is triggered in each call even though the data from the database is unchanged.
$scope.$watchCollection('products_copy', function (newValue, oldValue, scope) {
if(newValue!==oldValue)
{
console.log('Watch on products was called');
console.log('Old value: '+oldValue);
console.log('New value: '+newValue);
$scope.products = newValue;
}
});
I want the event to be triggered only when there is change in the array such as:
add item
remove item
edit any property of any item
EDIT 1:
I changed it to angular.equals but even that resulted in trigger in every call. However, when I examined each item in the console I realised angular was adding a property $$hashkey which was varying between the items. Any idea how to solve this?
It is how it should be, because on each response you assign a new reference to $scope.products_copy and no matter that there're no changes of the data inside it, when you do newValue!==oldValue you compare the refereces, they are always different
if you try
var a = [];
var b = [];
console.log(a===b); // false
// the both are empty and seems to be equal
you have to check the data inside old and new data. Try with angular.equals
You can try for angular.equal(object1, object2)
$scope.$watchCollection('products_copy', function (newValue, oldValue, scope) {
if(!angular.equals(newValue, oldValue))
{
console.log('Watch on products was called');
console.log('Old value: '+oldValue);
console.log('New value: '+newValue);
$scope.products = newValue;
}
});
The problem was that angular was inserting a $$hashKey attribute which resulted in a difference even in same objects. Removing this field did the trick for me.

Is there a way to refresh virtual repeat container?

I've got a md-virtual-repeat-container that handles the data from an API call triggered by a search box. I'd like to be able to refresh this container when a user enters a second search query.
I'm using a setup similar to this plnkr which is from this question:. The only difference is its getting data from a server when the user enters a search term.
My Question
Is there a way to trigger a refresh an md-virtual-repeat-container?
I don't think the code is relevant but here's my (simplified) production code:
var self = this;
self.infiniteItems = {
numLoaded_: 0,
toLoad_: 0,
items: [],
getItemAtIndex: function (index) {
if (index > this.numLoaded_) {
this.fetchMoreItems_(index);
return null;
}
return this.items[index];
},
getLength: function() {
return this.numLoaded_ + 25;
},
fetchMoreItems_: function (index) {
if (this.toLoad_ < index) {
this.toLoad_ += 5;
var offset = 0;
$http({
method: 'GET',
datatype: 'json',
url: '{my-api-call}',
contentType: "application/json; charset=utf-8",
cache: false,
params: {
param: query,
page: offset
}
}).then(angular.bind(this, function (obj) {
this.items = this.items.concat(obj.data.SearchResults);
this.numLoaded_ = this.toLoad_;
this.offset++;
$scope.searchResults = obj.data.SearchResults;
}));
}
}
};
You can force a redraw of your md-virtual-repeat container by triggering a fake window resize event (this causes the directive to call its private handleScroll_() method which then calls containerUpdated()).
This will trigger a fake window resize event:
angular.element(window).triggerHandler('resize');
You can also use this to cause refresh of items in specific scope and not in the entire document:
scope.$broadcast('$md-resize')
Resetting the model should also work.
In your case you could a have a new function on infiniteItems:
refresh : function() {
this.numLoaded_= 0;
this.toLoad_ = 0;
this.items = [];
}
I'm going to post my solution since there isn't an answer yet. If an better answer gets posted I'll gladly accept it.
First, I migrated all of my search result code into it's own template, directive and controller.
Next, I added an empty container in place of my logic. I'll use this as a place to dump my directive dynamically every time I search.
`<div id="search-result-container"></div>`
Finally, I dynamically appended my new directive like this
$scope.handleHotlineSearchClick = function () {
var query = $('#legal-search-input').val();
if (query != "") {
$scope.searchLoaded = false;
$('#search-result-container').empty().append($compile("<search-result></search-result>")($scope));
}
};
This way every time the user enters a new search term the directive gets reloaded.

WebSQL data into AngularJs DropDown

I have very simple question about getting data from WebSql
I have DropDown i.e
<select id="selectCatagoryFood" data-role="listview" data-native-menu="true"
ng-init="foodCatagory = foodCatagories.cast[0]"
ng-options="foodCatagory as foodCatagory.text for foodCatagory in foodCatagories.cast"
ng-model="foodCatagory"
ng-change="changeFoodCatagory()">
</select>
now i want to add data init from webSQL. I already get Data from webSql but i am confuse that how to add that data into DropDown
An example or hints maybe very helpful for me.
Update 1 :: Add Controller Code
myApp.controller('foodSelection',function($scope,foodCatagories){
$scope.foodCatagories = foodCatagories;
$scope.changeFoodCatagory = function(){
alert($scope.foodCatagory.value);
}
});
Update 2 webSQL and JayData
_context.onReady({
success: showData,
error: function (error){
console.log(error);
}
});
function showData(){
var option = '';
_context.FoodGroup.forEach(function(FG)
{
option += '<option value="'+FG.FoodGroupID+'">'+FG.Description+'</option>';
}).then(function(){
console.log(option);
});
}
Update 3
var myApp = angular.module('myApp',[]);
myApp.factory('foodCatagories',function(){
var foodCatagories = {};
foodCatagories.cast = [
{
value: "000",
text: "Select Any"
}
];
return foodCatagories;
});
Update 4
One thing that i didn't mention is that I am using JayData for getting data from webSQL to my App
I will try to explain how it works:
EDIT: Live demo
html
Here is your stripped down select.
<select ng-options="item as item.text for item in foodCategories"
ng-model="foodCategory"
ng-required="true"
ng-change="changeFoodCategory()">
</select>
The directive ng-options will fill automatically the option elements in your select. It will take the foodCategories variable from the $scope of your controller and foreach item in the collection, it will use the text property as the label shown (<option>{{item.text}}</option>') and it will select the whole objectitemas the value of the selectedoption. You could also refer to a property as the value like ({{item.text}}). Then yourng-modelwould be set to theid` value of the selected option.
The directive ng-model corresponds to the variable in the $scope of your controller that will hold the value of the selected option.
The directive ng-required allows you to check if a value has been selected. If you are using a form, you can check if the field is valid formName.ngModelName.$valid. See the docs for more details on form validation.
The directive ng-change allows you to execute a function whenever the selected option changes. You may want to pass the ng-model variable to this function as a parameter or call the variable through the $scope inside the controller.
If no default value is set, angular will add an empty option which will be removed when an option is selected.
You did use the ng-init directive to select the first option, but know that you could set the ng-model variable in your controller to the default value you would like or none.
js
Here I tried to simulate your database service by returning a promise in the case that you are doing an async request. I used the $q service to create a promise and $timeout to fake a call to the database.
myApp.factory('DbFoodCategories', function($q, $timeout) {
var foodCategories = [
{ id: 1, text: "Veggies", value: 100 },
{ id: 2, text: "Fruits", value: 50 },
{ id: 3, text: "Pasta", value: 200 },
{ id: 4, text: "Cereals", value: 250 },
{ id: 5, text: "Milk", value: 150 }
];
return {
get: function() {
var deferred = $q.defer();
// Your call to the database in place of the $timeout
$timeout(function() {
var chance = Math.random() > 0.25;
if (chance) {
// if the call is successfull, return data to controller
deferred.resolve(foodCategories);
}
else {
// if the call failed, return an error message
deferred.reject("Error");
}
}, 500);
/* // your code
_context.onReady({
success: function() {
deferred.resolve(_contect.FoodGroup);
},
error: function (error){
deferred.reject("Error");
}
});
*/
// return a promise that we will send a result soon back to the controller, but not now
return deferred.promise;
},
insert: function(item) {
/* ... */
},
update: function(item) {
/* ... */
},
remove: function(item) {
/* ... */
}
};
});
In your controller you set the variables that will be used in your view. So you can call your DbFoodCategories service to load the data into $scope.foodCategories, and set a default value in $scope.foodCategory that will be used to set the selected option.
myApp.controller('FoodSelection',function($scope, DbFoodCategories){
DbFoodCategories.get().then(
// the callback if the request was successfull
function (response) {
$scope.foodCategories = response; //response is the data we sent from the service
},
// the callback if an error occured
function (response) {
// response is the error message we set in the service
// do something like display the message
}
);
// $scope.foodCategory = defaultValue;
$scope.changeFoodCategory = function() {
alert($scope.foodCatagory.value);
}
});
I hope that this helped you understand more in detail what is happening!
See this example and how use $apply to update the data in scope.
in the new version we released a new module to support AngularJS. We've started to document how to use it, you can find the first blogpost here
With this you should be able to create your dropdown easily, no need to create the options manually. Something like this should do the trick:
myApp.controller('foodSelection',function($scope, $data) {
$scope.foodCatagories = [];
...
_context.onReady()
.then(function() {
$scope.foodCatagories = _context.FoodGroup.toLiveArray();
});
});
provided that FoodGroup has the right fields, of course

Angularjs E2E Testing with Angular-UI Select2 Element

I have a partial with a select2 element utilizing Angular UI http://angular-ui.github.io/
The issue I am running into is that the element is required and although i have successfully set the field through the following code, the required attribute is not removed as Angular's model must not be updating due to the outside change and I am not sure how to either provide a $scope.apply() or utilize another function of Angular to continue the test.
First to allow for direct jQuery functions to run: (taken from How to execute jQuery from Angular e2e test scope?)
angular.scenario.dsl('jQueryFunction', function() {
return function(selector, functionName /*, args */) {
var args = Array.prototype.slice.call(arguments, 2);
return this.addFutureAction(functionName, function($window, $document, done) {
var $ = $window.$; // jQuery inside the iframe
var elem = $(selector);
if (!elem.length) {
return done('Selector ' + selector + ' did not match any elements.');
}
done(null, elem[functionName].apply(elem, args));
});
};
});
Then to change the field value:
jQueryFunction('#s2id_autogen1', 'select2', 'open');
jQueryFunction('#s2id_autogen1', 'select2', "val", "US");
jQueryFunction('#s2id_autogen1', 'select2', 'data', {id: "US", text: "United States"});
jQueryFunction('.select2-results li:eq(3)', 'click');
jQueryFunction('#s2id_autogen1', 'trigger', 'change');
jQueryFunction('#s2id_autogen1', 'select2', 'close');
input('request._countrySelection').enter('US');
Note that not all of those functions are needed to reflect the changes to the ui, merely all that I have utilized to try and get this working...
To get this to work I consulted both Brian's answer and sinelaw, but it still failed in my case for two reasons:
clicking on 'div#s2id_autogen1' does not open the select2 input for me, the selector I used was 'div#s2id_autogen1 a'
getting the select2 element I would get the ElementNotVisibleError, probably because my select2 is inside a bootstrap modal, so I explicitly wait for the element to be visible before clicking it (you can read the original hint I read to use this here).
The resulting code is:
function select2ClickFirstItem(select2Id) {
var select2Input;
// Wait for select2 element to be visible
browser.driver.wait(function() {
select2Input = element(by.css('#s2id_' + select2Id + ' a'));
return select2Input;
}).then(function() {
select2Input.click();
var items = element.all(by.css('.select2-results-dept-0'));
browser.driver.wait(function () {
return items.count().then(function (count) {
return 0 < count;
});
});
items.get(0).click();
});
}
Hope it helps.
I was unable to get this to work within the Karma test runner, however this became significantly easier within the protractor test suite.
To accomplish this within the protractor test suite I used the following to select the first select2 box on the page and select the first option within that box:
var select2 = element(by.css('div#s2id_autogen1'));
select2.click();
var lis = element.all(by.css('li.select2-results-dept-0'));
lis.then(function(li) {
li[0].click();
});
The next select2 on the page has an id of s2id_autogen3
I'll second what #Brian said if you use protractor and the new karma this has worked for me:
function uiSelect(model, hasText) {
var selector = element(by.model('query.cohort')),
toggle = selector.element(by.css('.ui-select-toggle'));
toggle.click();
browser.driver.wait(function(){
return selector.all(by.css('.ui-select-choices-row')).count().then(function(count){
return count > 0;
});
}, 2000);
var choice = selector.element(by.cssContainingText('.ui-select-choices-row',hasText));
choice.click();
};
use it like:
if the value of the item you want to select is "Q3 2013" you can provide it the model of the selector and an exact or partial text match of the item you want to select.
uiSelect('query.cohort','Q3 2013');
or
uiSelect('query.cohort','Q3');
will both work
I made it work under Karma with following changes.
Add following DSL to the top of your e2e test file:
angular.scenario.dsl('jQueryFunction', function() {
return function(selector, functionName /*, args */) {
var args = Array.prototype.slice.call(arguments, 2);
return this.addFutureAction(functionName, function($window, $document, done) {
var $ = $window.$; // jQuery inside the iframe
var elem = $(selector);
if (!elem.length) {
return done('Selector ' + selector + ' did not match any elements.');
}
done(null, elem[functionName].apply(elem, args));
});
};
});
Then to change select2 value in your scenario use
it('should narrow down organizations by likeness of name entered', function() {
jQueryFunction('#s2id_physicianOrganization', 'select2', 'open');
jQueryFunction('#s2id_physicianOrganization', 'select2', 'search', 'usa');
expect(element('div.select2-result-label').count()).toBe(2);
});
Sometimes the select2 may take time to load, especially when working with ajax-loaded data. So when using protractor, and expanding on Brian's answer, here's a method I've found to be reliable:
function select2ClickFirstItem(select2Id) {
var select2 = element(by.css('div#s2id_' + select2Id));
select2.click();
var items = element.all(by.css('.select2-results-dept-0'));
browser.driver.wait(function () {
return items.count().then(function (count) {
return 0 < count;
})
});
items.get(0).click();
}
This uses the fact that driver.wait can take a promise as a result.

Capture "updater" action from AngularUI's Bootstrap Typeahead

How can I capture the "updater" event from AngularUI's UI Bootstrap directive?
I've defined my HTML:
<input type="text" pattern="[0-9]*" class="span6" placeholder="8675309" ng-model="empid" typeahead="entry for entry in httpEmpIdSearch($viewValue, 8)">
... and my associated function:
$scope.httpEmpIdSearch = function(query, limit)
{
return $http.get(
'/visitorLog/api/v1/employee/?limit=' + limit + '&empid__startswith=' + query
).then(function(response)
{
output = [];
angular.forEach(response.data.objects, function(value, key) {
this.push(value.bems.toString());
}, output);
return output;
});
}
I would like to take additional action (autocomplete a form) when the user clicks on an ID in the popup. If raw Bootstrap I would use the "updater":
$('#sampleElement').typeahead({
minLength: 3,
source: function (query, process)
{
// get the data
},
updater: function (item)
{
// user clicked on an item, do something more!
},
});
I've tried various listeners like:
$scope.$on('typeahead-updated', function(event)
{ ... }
But I've not way to let me capture such an event. Is there some way I can take additional action once a typeahead option is selected?
Also, in version 0.4.0 of the ui-bootstrap library the typeahead-on-select directive is now supported. This should provide your desired event.
See:
https://github.com/angular-ui/bootstrap/commit/91ac17c9ed691a99647b66b3f464e3585398be19
It does not appear that there is a way to hook into that event in the directive. You could however put a watch on the model value and simulate this behavior.
Check out this plunker example
Since you are pulling the list of data from the server on the fly this solution may not work for you. But I would definitely have a look at watch it seems like the best way to achieve the desired outcome.
Something along these lines could work.
$scope.output = [];
$scope.httpEmpIdSearch = function(query, limit)
{
return $http.get(
'/visitorLog/api/v1/employee/?limit=' + limit + '&empid__startswith=' + query
).then(function(response)
{
$scope.output.length = 0;
angular.forEach(response.data.objects, function(value, key) {
this.push(value.bems.toString());
}, $scope.output);
return $scope.output;
});
}
$scope.$watch('empid', function(newValue, oldValue){
if(newValue !== oldValue && $scope.output.indexOf(newValue) !== -1){
//do work here
}
});
The full list of typeahead related directives is here:
https://github.com/angular-ui/bootstrap/tree/master/src/typeahead/docs
typeahead-editable, typeahead-input-formatter, typeahead-loading, typeahead-min-length, typeahead-on-select, typeahead-template-url, typeahead-wait-ms

Resources