OK switching my code to angularjs and the angular 'way', not sure what I am doing wrong.
A select list is not getting updated when the model changes unless I call $apply, and I find myself calling apply a lot.
index.html has this:
<div id='rightcol' data-ng-include="'partials/rightSidebar.html'"
data-ng-controller="rightSidebarController">
</div>
and rightSidebar.html has this:
<select id='srcList' size='10'
data-ng-model="data.source"
data-ng-click='srcOnclick()'
data-ng-options="s.title for s in data.srcList | filter:{title:data.srcFilter} | orderBy:'title'"></select>
rightSidebarController.js has this:
$scope.data = {};
$scope.data.srcList = dataProvider.getSourceList();
$scope.data.source = dataProvider.getSource();
dataProvider is a service that makes an asynchronous database call (IndexedDB) to populate srcList, which is what gets returned in dataProvider.getSource().
Is it the asynchronous database call that forces me to call $apply, or should the controller be ignorant of that?
Is there a 'better' way to do this?
Edited to add service code.
Another controller calls dataProvider.refreshSourceList:
myDB.refreshSourceList = function() {
myDB.getRecords("source", function(recs) {
myDB.srcList = recs;
$rootScope.$broadcast('SrcListRefresh');
});
};
myDB.srcList is the field being bound by $scope.data.srcList = dataProvider.getSourceList();
myDB.getRecords:
myDB.getRecords = function(storeName, callback) {
var db = myDB.db;
var recList = [];
var trans = db.transaction([storeName], 'readonly');
var store = trans.objectStore(storeName);
var cursorRequest = store.openCursor();
cursorRequest.onerror = myDB.onerror;
cursorRequest.onsuccess = function(e) {
var cursor = cursorRequest.result || e.result;
if (cursor === false || cursor === undefined) {
if (callback !== undefined) {
$rootScope.$apply(function() {
callback(recList);
});
}
} else if (cursor.value !== null) {
recList.push(cursor.value);
cursor.continue();
}
};
cursorRequest.onerror = myDB.onerror;
};
Anything you do async needs to be wrapped in $scope.$apply(). This is because angular works in a similar fashion to a game loop, however instead of constantly running, it knows to end the loop when an action is taken, and $scope.$digest() is called.
If you are using IndexedDB, I would recommend creating an angular wrapper for it, like so:
(forgive my IndexedDB code, I'm not experience with it)
angular.module('app',[])
.factory('appdb', function($rootScope){
var db = indexedDB.open('appdb', 3);
return {
get : function(table, query, callback) {
var req = db.transaction([table])
.objectStore(table)
.get(query);
req.onsuccess(function(){
$rootScope.$apply(function(){
callback(req.result);
});
});
}
};
});
This way you can be sure that any data retrieve and set on a controller scope inside of callback will have $scope.$digest() called afterward.
Related
I have an AngularJS 1.5 directive:
var assetSearchService = function(proService) {
var assets = [];
var searchAssets = function(searchTerm){
proService.searchAssets(searchTerm).then(function(data){
assets = data.data;
});
};
return {
searchAssets, searchAssets,
assets: assets
};
};
When I try to use assetSearchService.assets in my controller after calling search, the data is not set in assetService.assets.
If I log the data after the searchAssets promise returns, I am getting data.
this.assets does not work so how do I do get the variable back from the callback?
Found an answer. Neither assets = newArray nor conact does not work because both return a new array and break the reference, rather than modifying the current one.
This works:
Array.prototype.push.apply(assets, data.data);
Here's a working example: https://jsfiddle.net/mbaranski/5k4bqo0z/
You can achieve more complex solution
var assetSearchService = function(proService) {
service = this;
service.assets = [];
service.searchAssets = function(searchTerm){
proService.searchAssets(searchTerm).then(function(data){
assets.push(data.data);
});
};
return service;
};
This function fires once at page load, but then never again. I've tried $watch, $apply, and firebase's ref.on('value', ... but no dice. The model changes for the value of scope.job.getApplicants(), but scope.applicants_ is not refreshing when this happens.
function go() {
var p = [];
// scope.job.getApplicants() returns a $firebaseArray of user IDs
scope.job.getApplicants().$loaded().then(function(list) {
list.forEach(function(app) {
// User_(app.$id) returns a $firebaseObject
var user = User_(app.$id);
p.push(user);
})
});
return p;
}
scope.applicants_ = go();
This is how I resolved the problem:
ref.on('value', function(apps) {
var applicantIDs = Object.keys(apps.val());
scope.applicants_ = applicantIDs.map(function(app) {
return User_(app);
});
});
I'm an atmosphere & Angular newbie and I'm really struggling to find an answer to this! Maybe I'm asking the wrong question.
I am setting up notifications using Atmosphere. I can open the websocket and watch the updates happen if I post the API URL directly into my browser.
In Angular I have an ng-repeat loop, which I would like to run as each new update adds a new object to the websocket.
<li ng-repeat="notification in notifications track by $index">
I am using angular watch to check for updates, but it doesn't pick up the new objects being added to the array. Here is my code:
// notification alerts
$scope.notifications = [];
notificationsService.notificationAlerts().then(function success(response) {
var jsonStringArray = response.data.split('|');
$scope.notifications = $.map(jsonStringArray, function(n, i){
if (n !== ""){
return JSON.parse(n);
}
});
console.log('Connect', response);
});
$scope.$watch('notifications', function(newVal, oldVal){
console.log('Watch', $scope.notifications);
}, true);
Hopefully I've made myself clear, let me know if I need to elaborate, or if I'm asking the wrong question. Thanks!
OK, I managed to solve this, for anyone stumbling across it later. Here is the final JS:
// add number of notifications to ".notifications-number"
function updateNumberOfNotifications(){
var numberOfNotifications = $("ul.notifications-list li").not(".nocount").length;
if (numberOfNotifications < 1) {
$(".notifications-number, .notifications-list").addClass("hidden");
} else {
$(".notifications-number").html(numberOfNotifications);
$(".notifications-number, .notifications-list").removeClass("hidden");
}
}
// notification alert variables
$scope.notifications = [];
var socket = atmosphere;
var subSocket;
// subscribe
function subscribe() {
var request = {
url : "/service/notifier",
transport: 'long-polling'
};
request.onMessage = function (response) {
//console.log('response', response);
var jsonStringArray = response.responseBody.split('|');
// console.log('json string array', jsonStringArray);
$.each(jsonStringArray, function(index, elem){
if (elem != ""){
$scope.notifications.push(JSON.parse(elem));
console.log("object", JSON.parse(elem));
}
});
//$scope.notifications.push($scope.newNotification);
$scope.$apply();
updateNumberOfNotifications();
// console.log('$scope.notifications', $scope.notifications);
};
subSocket = socket.subscribe(request);
}
function unsubscribe(){
socket.unsubscribe();
}
// subscribe on load and update notifications
updateNumberOfNotifications();
subscribe();
I want to use the $q service of angular in my e2e tests. (I want to get the texts of a bunch of elements via getText() which returns a promise. After all promises are resolved, I want to test the list. So I want to use $q.all() etc.)
angular.injector(['myApp']).get('$q'); results in "ReferenceError: angular is not defined"
Installing angular via node and then var angular = require("angularjs"); results in "Error: Cannot find module 'angular'"
Also, inserting a browser.waitForAngular() does not help there.
Using the inject(function($q) {}) syntax has the same problem.
How can I use such angular functions in protractor?
edit:
Here's the very naive version of what I want to achieve
var collectEntries = function(containers) {
var entries = {};
containers.each(function (container) {
var title = container.element(by.tagName('h2'));
title.getText().then(function (text) {
var key = getSomeKey();
var entry = processEntry(text);
entries[key] = entry;
});
});
return entries;
};
That works in principle, at some point in time entries contains all data. However, I need to wait for that moment. What I would do is create and return a promise that gets resolved as soon as all getText promises are resolved.
e.g.
var deferred = $q.defer();
$q.all(getTextPromises).then(function () {
deferred.resolve(entries);
});
return deferred.promise;
From the looks of your code containers is a list of elementFinders? (i.e. var containers = [element(by.x), element(by.y), element(by.z)]):
Using q: (you need to add q as dependency in package.json first)
var q = require('q');
var collectEntries = function(containers) {
var entries = {};
containers.each(function (container) {
var deferred = q.defer();
var title = container.element(by.tagName('h2'));
title.getText().then(function (text) {
deferred.resolve(processEntry(text));
});
entries[getSomeKey()] = deferred.promise();
});
return q.all(entries);
};
expect(collectEntries).toBe({key1: 'title1', key2: 'title2'})
But protractor knows promise itself (and it's preferably that you don't mix protractor's promise with q promise unless you know what you're doing):
var collectEntries = function(containers) {
var entries = {};
containers.each(function (container) {
entries[getSomeKey()] = container.element(by.tagName('h2')).
getText().then(function (text) {
return processEntry(text);
});
});
return protractor.promise.fullyResolved(entries);
};
expect(collectEntries).toBe({key1: 'title1', key2: 'title2'})
If your containers are found using a single selector (i.e. var containers = element.all(by.xyz)), it's even easier:
var collectEntries = function(containers) {
return containers.reduce(function(entries, elem) {
return elem.getText().then(function(text) {
entries[getSomeKey()] = processEntry(text);
return entries;
});
}, {});
};
expect(collectEntries).toBe({key1: 'title1', key2: 'title2'})
I'm trying to get the following findTimelineEntries function inside an Angular controller executing after saveInterview finishes:
$scope.saveInterview = function() {
$scope.interviewForm.$save({employeeId: $scope.employeeId}, function() {
$scope.findTimelineEntries();
});
};
The save action adds or edits data that also is part of the timeline entries and therefore I want the updated timeline entries to be shown.
First I tried changing it to this:
$scope.saveInterview = function() {
var functionReturned = $scope.interviewForm.$save({employeeId: $scope.employeeId});
if (functionReturned) {
$scope.findTimelineEntries();
}
};
Later to this:
$scope.saveInterview = function() {
$scope.interviewForm.$save({employeeId: $scope.employeeId});
};
$scope.saveInterview.done(function(result) {
$scope.findTimelineEntries();
});
And finaly I found some info about promises so I tried this:
$scope.saveInterview = function() {
$scope.interviewForm.$save({employeeId: $scope.employeeId});
};
var promise = $scope.saveInterview();
promise.done(function() {
$scope.findTimelineEntries();
});
But somehow the fact that it does work this way according to http://nurkiewicz.blogspot.nl/2013/03/promises-and-deferred-objects-in-jquery.html, doesn't mean that I can use the same method on those $scope.someFuntcion = function() functions :-S
Here is a sample using promises. First you'll need to include $q to your controller.
$scope.saveInterview = function() {
var d = $q.defer();
// do something that probably has a callback.
$scope.interviewForm.$save({employeeId: $scope.employeeId}).then(function(data) {
d.resolve(data); // assuming data is something you want to return. It could be true or anything you want.
});
return d.promise;
}