I have quite a few scenarios where I need clicks, etc. to trigger behavior in another place on the page (a one-way communication scenario). I now have a need for bi-directional communication, where stuff that happens in element A can modify specific properties in the scope behind element B and vice-versa. Thus far, I've been using $rootScope.$broadcast to facilitate this but it feels like overkill, and winds up creating boilerplate in both places:
$scope.$on('event-name', function(event, someArg) {
if(someArg === $scope.someProperty) return;
$scope.someProperty = someArg;
});
$scope.$watch('someProperty', function(newValue) {
$rootScope.$broadcast('event-name', newValue);
});
Is there a better way? I'd like to tie the two (or three, or N) scopes together via a service, but I don't see a way to do that without magic event names and boilerplate.
I haven't used this myself, but this post explains basically how I would do it. Here's the code which illustrates the idea:
(function() {
var mod = angular.module("App.services", []);
//register other services here...
/* pubsub - based on https://github.com/phiggins42/bloody-jquery-plugins/blob/master/pubsub.js*/
mod.factory('pubsub', function() {
var cache = {};
return {
publish: function(topic, args) {
cache[topic] && $.each(cache[topic], function() {
this.apply(null, args || []);
});
},
subscribe: function(topic, callback) {
if(!cache[topic]) {
cache[topic] = [];
}
cache[topic].push(callback);
return [topic, callback];
},
unsubscribe: function(handle) {
var t = handle[0];
cache[t] && d.each(cache[t], function(idx){
if(this == handle[1]){
cache[t].splice(idx, 1);
}
});
}
}
});
return mod;
})();
Note the memory leak though if controllers are "deleted" without unsubscribing.
I think you can try the following service,
'use strict';
angular.module('test')
.service('messageBus', function($q) {
var subscriptions = {};
var pendingQuestions = [];
this.subscribe = function(name) {
subscriptions[name].requestDefer = $q.defer();
return subscriptions[name].requestDefer.promise; //for outgoing notifications
}
this.unsubscribe = function(name) {
subscriptions[name].requestDefer.resolve();
subscriptions[name].requestDefer = null;
}
function publish(name, data) {
subscriptions[name].requestDefer.notify(data);
}
//name = whom shd answer ?
//code = what is the question ?
//details = details abt question.
this.request = function(name, code, details) {
var defered = null;
if (subscriptions[name].requestDefer) {
if (pendingQuestions[code]) {
//means this question is already been waiting for answer.
//hence return the same promise. A promise with multiple handler will get
//same data.
defered = pendingQuestions[code];
} else {
defered = $q.defer();
//this will be resolved by response method.
pendingQuestions[code] = defered;
//asking question to relevant controller
publish(name, {
code: code,
details: details
});
}
} else {
//means that one is not currently in hand shaked with service.
defered = $q.defer();
defered.resolve({
code: "not subscribed"
});
}
return defered.promise;
}
//data = code + details
//responder does not know the destination. This will be handled by the service using
//pendingQuestions[] array. or it is preemptive, so decide by code.
this.response = function(data) {
var defered = pendingQuestions[data.code];
if (defered) {
defered.resolve(data);
} else {
//means nobody requested for this.
handlePreemptiveNotifications(data);
}
}
function handlePreemptiveNotifications() {
switch (data.code) {
//handle them case by case
}
}
});
This can be used as a message bus in multi controller communication. It is making use of the angular notify() callback of promise API.All the participating controllers should subscribe the service as follows,
angular.module('test')
.controller('Controller1', function($scope, messageBus) {
var name = "controller1";
function load() {
var subscriber = messageBus.subscribe(name);
subscriber.then(null, null, function(data) {
handleRequestFromService(data);
});
}
function handleRequestFromService(data) {
//process according to data content
if (data.code == 1) {
data.count = 10;
messageBus.respond(data);
}
}
$scope.$on("$destroy", function(event) {
//before do any pending updates
messageBus.unsubscribe(name);
});
load();
});
angular.module('test')
.controller('Controller2', function($scope, messageBus) {
var name = "controller2";
function load() {
var subscriber = messageBus.subscribe(name);
subscriber.then(null, null, function(data) {
handleRequestFromService(data);
});
}
function handleRequestFromService(data) {
//process according to data content
}
$scope.getHorseCount = function() {
var promise = messageBus.request("controller1", 1, {});
promise.then(function(data) {
console.log(data.count);
});
}
$scope.$on("$destroy", function(event) {
//before do any pending updates
messageBus.unsubscribe(name);
});
load();
});
Related
I've been trying to code up a search engine using angular js, but I can't copy one array to another. When I initiate the the code (in the service.FoundItems in the q.all then function) new array(foundArray) shows up as an empty array. I searched up how to copy one array to another and tried that method as you can see, but it isn't working. Please help, here is the code, and thank you.
P.S. if you need the html please tell me.
(function () {
'use strict';
angular.module('narrowDownMenuApp', [])
.controller('narrowItDownController', narrowItDownController)
.service('MenuSearchService', MenuSearchService)
.directive('searchResult', searchResultDirective);
function searchResultDirective() {
var ddo = {
templateUrl: 'searchResult.html',
scope: {
items: '<'
},
};
return ddo
}
narrowItDownController.$inject = ['MenuSearchService'];
function narrowItDownController(MenuSearchService) {
var menu = this;
menu.input = "";
menu.displayResult = [];
menu.searchX = function(name) {
menu.displayResult = MenuSearchService.FoundItems(menu.input, name);
console.log(menu.displayResult);
};
}
MenuSearchService.$inject = ['$http', '$q'];
function MenuSearchService($http, $q) {
var service = this;
service.getMatchedMenuItems = function(name, searchTerm) {
var deferred = $q.defer();
var foundItems = [];
var result = $http({
method: "GET",
url: ('https://davids-restaurant.herokuapp.com/menu_items.json'),
params: {
category: name
}
}).then(function (result) {
var items = result.data;
for (var i = 0; i < items.menu_items.length; i++) {
if (searchTerm === ""){
deferred.reject("Please enter search term");
i = items.menu_items.length;
}
else if (items.menu_items[i].name.toLowerCase().indexOf(searchTerm.toLowerCase()) ==! -1){
foundItems.push(items.menu_items[i].name)
deferred.resolve(foundItems);
}else {
console.log("doesn't match search");
}
}
});
return deferred.promise;
};
service.FoundItems = function (searchTerm, name) {
var searchResult = service.getMatchedMenuItems(name, searchTerm);
var foundArray = [];
$q.all([searchResult])
.then(function (foundItems) {
foundArray = foundItems[0].slice(0);
foundArray.reverse();
})
.catch(function (errorResponse) {
foundArray.push(errorResponse);
});
console.log(foundArray);
return foundArray;
};
};
})();
If the goal of the service.FoundItems function is to return a reference to an array that is later populated with results from the server, use angular.copy to copy the new array from the server to the existing array:
service.FoundItems = function (searchTerm, name) {
var foundArray = [];
var searchPromise = service.getMatchedMenuItems(name, searchTerm);
foundArray.$promise = searchPromise
.then(function (foundItems) {
angular.copy(foundItems, foundArray);
foundArray.reverse();
return foundArray;
})
.catch(function (errorResponse) {
return $q.reject(errorResponse);
})
.finally(function() {
console.log(foundArray);
});
return foundArray;
};
I recommend that the promise be attached to the array reference as a property named $promise so that it can be used to chain functions that depend on results from the server.
Frankly I don't recommend designing services that return array references that are later populated with results. If you insist on designing it that way, this is how it is done.
I tried the $promise thing that you recommended. I was wondering how you would get the value from it ie the array.
In the controller, use the .then method of the $promise to see the final value of the array:
narrowItDownController.$inject = ['MenuSearchService'];
function narrowItDownController(MenuSearchService) {
var menu = this;
menu.input = "";
menu.displayResult = [];
menu.searchX = function(name) {
menu.displayResult = MenuSearchService.FoundItems(menu.input, name);
̶c̶o̶n̶s̶o̶l̶e̶.̶l̶o̶g̶(̶m̶e̶n̶u̶.̶d̶i̶s̶p̶l̶a̶y̶R̶e̶s̶u̶l̶t̶)̶;̶
menu.displayResult.$promise
.then(function(foundArray) {
console.log(foundArray);
console.log(menu.displayResult);
}).catch(function(errorResponse) {
console.log("ERROR");
console.log(errorResponse);
});
};
}
To see the final result, the console.log needs to be moved inside the .then block of the promise.
Titus is right. The function always immediately returns the initial value of foundArray which is an empty array. The promise is executed asynchronously so by the time you are trying to change foundArray it is too late. You need to return the promise itself and then using .then() to retrieve the value just like you are currently doing inside the method.
From just quickly looking at your code I think you made have a simple error in there. Are you sure you want
foundArray = foundItems[0].slice(0);
instead of
foundArray = foundItems.slice(0);
Factory that gets the rates using REST
.factory('rateResource', ['$resource', function ($resource) {
return $resource('/rates/:currency/:effectiveDate', {
currency: '#currency',
effectiveDate: '#effectiveDate'
});
}])
Service that calls the factory to get resource and retrieve the rates and also errors if there are any.
.service('RateService', ['rateResource', '$rootScope',
function (rateResource, $rootScope) {
var self = this;
self.rates = [];
self.search = function (baseCurrency, effectiveDate) {
self.rates = [];
var error = null;
self.baseCurrency = baseCurrency;
self.effectiveDate = effectiveDate;
if (baseCurrency) {
rateResource.query({currency: baseCurrency, effectiveDate: effectiveDate})
.$promise.then(function (rates) {
if (rates) {
angular.forEach(rates, function (rate) {
rate.maintTs = $rootScope.formatTimestampToHHMMSS(rate.maintTs);
rate.editable = false;
self.rates.push(rate);
});
self.processing = false;
}
}, function (response) {
self.processing = false;
error = 'Processing failed due to '+response.status;
console.log(error);
})
return {
rates: self.rates,
errors: error
}
}
}
}]);
Controller calls the service for rates.
.controller('RateController', ['RateService',
function (rateService) {
var self = this;
self.baseCurrency = rateService.getBaseCurrency();
self.effectiveDate = rateService.getEffectiveDate();
self.rates = rateService.getRates();
//make the call from the controller
self.search = function () {
var response = rateService.search(self.baseCurrency, self.effectiveDate.yyyyMMdd());
self.rateRecords = response.rates;
self.errors = response.errors;
}
}])
rates are showing up fine in the controller after the promise is fulfilled. However, upon receiving errors, they are not getting transferred from service to controller. ( I changed the REST URL to make service return a 404 response ). What am I doing wrong?
Currently you are returning asynchronous call response from outside of async function, you shouldn't do that technically. Asynchronous code always get there responses inside their promise/callback function.
But in your case it is working because you are returning object with it reference. If you look at below code, return statement has carried self.rates object reference, so while returning even if it is blank, it is going to get updated value once self.rates gets filled up. So then you don't need to worry about the rates updation. But same thing for error would not work because it is of primitive datatype like var error = null, so when you are returning value it would be null(as async response haven't completed).
return {
rates: self.rates, //passed by reference
errors: error //passed by primitive type
}
So for solving this issue you could also make the error to an object type, so that it reference will get passed with reference object, like var errors = [] & when error occur push the error message into that array using .push
But I'd not recommend above way to go for, I'd rather take use of promise pattern and will maintain proper code call stack. Basically for that you need to return promise from search method & then put .then function to wait till resolve that function.
Service
.service('RateService', ['rateResource', '$rootScope',
function(rateResource, $rootScope) {
var self = this;
self.rates = [];
self.search = function(baseCurrency, effectiveDate) {
self.rates = [];
var error = null;
self.baseCurrency = baseCurrency;
self.effectiveDate = effectiveDate;
if (baseCurrency) {
return rateResource.query({
currency: baseCurrency,
effectiveDate: effectiveDate
})
.$promise.then(function(rates) {
if (rates) {
angular.forEach(rates, function(rate) {
rate.maintTs = $rootScope.formatTimestampToHHMMSS(rate.maintTs);
rate.editable = false;
self.rates.push(rate);
});
self.processing = false;
}
//returning object from promise
return {
rates: self.rates,
errors: error
}
}, function(response) {
self.processing = false;
error = 'Processing failed due to ' + response.status;
console.log(error);
//returning object from promise
return {
rates: self.rates,
errors: error
}
})
}
}
}
])
Controller
//make the call from the controller
self.search = function() {
var response = rateService.search(self.baseCurrency, self.effectiveDate.yyyyMMdd()).then(function() {
self.rateRecords = response.rates;
self.errors = response.errors;
});
}
I have an API that send updates via Server Sent Events (SSE) every seconds for my items.
Basically I have a collection $scope.items that contain a lot of information within and every second one item of this list is updated.
What I'm doing is:
var source;
if (!!window.EventSource) {
source = new EventSource('/updates');
} else {
alertify.error('SSE not supported');
}
// Emit SSE for items
source.addEventListener('items', function (e) {
var data = JSON.parse(e.data);
$timeout(function () {
var item_index = _.findIndex($scope.items, function (item) {
return item.id === data.id;
});
var status = data.status;
if (item_index > -1) {
if (status === 'cancelled') {
$scope.items.splice(item_index, 1);
}
$scope.items[item_index] = data;
$scope.$apply();
} else {
$scope.items.push(data);
}
});
}, false);
I was wondering if I'm doing it right or if I can improve this code because the app is quite slow when I start to have many and many items to cycle every second...
Looking at your code:
var item_index = _.findIndex($scope.items, function (item) {
return item.id === data.id;
});
I fear that a full search is done every time you access item_index
I would define a function:
function getIndex(data){
_.findIndex($scope.items, function (item) {
return item.id === data.id;
});
};
And the call it from within your
$timeout(function () {
var item_index = getIndex(data);
...
var app = angular.module('app',['ui.bootstrap']);
app.controller("ListCtrl", function ($scope, $http) {
$scope.submit = function () {
$scope.loading = true;
$scope.error = false;
$http.get('http://www.omdbapi.com/?s=' + $scope.search + '&r=json')
.then(function (res) {
var titles = [];
angular.forEach(res.data.Search, function(item){
$http.get('http://www.omdbapi.com/?t=' + item.Title + '&y=&plot=full&r=json').then(function(res){
if (res.data.Poster === "N/A") {
res.data.Poster = "http://placehold.it/350x450/FF6F59/FFFFFF&text=Image+not+Available!!";
}
titles.push(res.data);
});
});
$scope.movie = titles;
$scope.results = true;
$scope.error = false;
$scope.loading = false;
if (titles.length==0) { // not working
$scope.results = false;
$scope.error = true;
}
})
I have been tried several things like :
Object.getOwnPropertyNames(titles).length === 0)
obj == null
None of them seems to work...
This is happening because of incorrect scope:
var titles = []; is defined inside the .then
and you are checking the length outside of .then
since titles is not available outside .then it would not work. (undefined.length==0)
Solution:
.then(function (res) {
var titles = [];
angular.forEach(res.data.Search, function(item){
$http.get('http://www.omdbapi.com/?t=' + item.Title + '&y=&plot=full&r=json').then(function(res){
if (res.data.Poster === "N/A") {
res.data.Poster = "http://placehold.it/350x450/FF6F59/FFFFFF&text=Image+not+Available!!";
}
titles.push(res.data);
});
$scope.movie = titles;
$scope.results = true;
$scope.error = false;
$scope.loading = false;
if (titles.length==0) { // now this will work
$scope.results = false;
$scope.error = true;
}
});//titles will not be available after this.
$http.get() is async so the statement if (titles.length==0) { gets executed right away.
Have a counter to determine when all the Promises get resolved and then perform the check. Move the if statement inside the callback.
var count = res.data.Search.length;
angular.forEach(res.data.Search, function(item){
$http.get('http://www.o....rest of code').then(function(res) {
// rest of code
titles.push(res.data);
if (!count-- && !titles.length) {
$scope.results = false;
$scope.error = true;
}
}
});
});
In your case, the check
titles.length
will be executed before the
titles.push
because you use an asynchronous request which will return later.
You need to wrap your statements into the answer-block of the request.
Just as an aside, but beneficial for my practice and your future help:
Part of the problem you were having was scope-management (JS-scope, not Angular $scope), part of it was concurrency-management, and part of it appeared to be plain old scope-formatting making it hard to see where all control blocks start and end (that gets miserable when it's not just if/else, but with callbacks/promises, too).
This is a small example of how you might consider tackling these problems, through a quick refactor of your issues:
function pluck (key) {
return function pluckFrom(obj) { return obj[key]; };
}
angular.module("app", ["ui.bootstrap"]);
angular.moule("app").service("omdbService", ["$http", function ($http) {
function getSearch (search) {
var searching = $http.get("http://www.omdbapi.com/?s=" + search + "&r=json")
.then(pluck("data"));
return searching;
}
function getMovie (title) {
var searching = $http.get("http://www.omdbapi.com/?t=" + title + "&y=&plot=full&r=json")
.then(pluck("data"));
return searching;
}
return {
getSearch: getSearch,
getMovie: getMovie,
getPlaceholderPoster: function () { return "http://placehold.it/350x450/FF6F59/FFFFFF&text=Image+not+Available!!"; }
};
}]);
angular.moule("app").controller("ListCtrl", ["$scope", "$q", "omdbService", function ($scope, $q, omdb) {
function loadMovie (movie) {
return omdb.getMovie(movie.Title)["catch"](function () { return undefined; });
}
function movieExists (movie) { return !!movie; }
function updatePoster (movie) {
movie.Poster = movie.Poster || omdb.getPlaceholderPoster();
return movie;
}
function setResults (movies) {
$scope.movie = movies; // $scope.movies, instead?
$scope.results = true;
$scope.error = false;
$scope.loading = false;
}
function handleError () {
$scope.results = false;
$scope.error = true;
}
$scope.submit = function () {
$scope.loading = true;
$scope.error = false;
omdb.getSearch($scope.search)
.then(pluck("Search"))
.then(function (movies) { return $q.all(movies.map(loadMovie)); })
.then(function (movies) { return movies.filter(movieExists).map(updatePoster); })
.then(setResults, handleError);
};
}]);
There are 8000 valid ways of handling this, and everyone will see it a little differently.
This is also not really how I'd tackle it in production, but not too far off...
Moving all endpoint-calls out to a service which is responsible for those means that any controller in your system (with that module as a dependency) can access them.
Doing small things per function and letting Array.prototype methods do the iterating (IE8 can be shimmed if needed) means that each function is super-specific.
By wrapping the controller/service functions in arrays, and naming their dependencies, they're now minification friendly.
The body of submit() is less than 10 lines, and deals with all kinds of crazy async stuff, but I know that I've handled errors like one of the movies returning a 404 (my code should still fire, with the remaining movies, the code of others might not -- most code would either never trigger success or would fail all the way through the program, if the server threw an error for a movie).
Now, I'm not checking that the server is sending the right kind of data for a "movie", but that's different.
I have been adapting the IBM angularjs tutorial here into a Yeoman angular-fullstack tutorial and it has been relatively easy except I have one Issue. When I vote on a Poll the data does not refresh and show the results on my version.
I have tried Debugging through it as best I can and I cannot see any difference between my version and the IBM version that would cause this issue. I have also looked here on SO and on google but I'm actually completely lost.
my entire code base is located here on github and I have embeded what I think is the relevant code below, Any help would be greatly appreciated
This is the client side controller
.controller('PollViewCtrl', function ($scope, $routeParams, Poll, socket){
$scope.poll = Poll.get({pollId: $routeParams.id});
socket.on('myvote', function(data) {
console.dir(data);
if(data._id === $routeParams.pollId) {
$scope.poll = data;
}
});
socket.on('vote', function(data) {
console.dir(data);
if(data._id === $routeParams.pollId) {
$scope.poll.choices = data.choices;
$scope.poll.totalVotes = data.totalVotes;
}
});
$scope.vote = function() {
var pollId = $scope.poll._id,
choiceId = $scope.poll.userVote;
if(choiceId) {
var voteObj = { poll_id: pollId, choice: choiceId };
socket.emit('send:vote', voteObj);
} else {
alert('You must select an option to vote for');
}
};
})
and this is the relavent server side code
//app.js
var io = require('socket.io').listen(app.listen(config.port));
var poll = require('./lib/controllers/polls');
io.sockets.on('connection', poll.vote);
//poll.js
exports.vote = function(socket) {
socket.on('send:vote', function(data) {
var ip = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address.address;
Poll.findById(data.poll_id, function(err, poll) {
var choice = poll.choices.id(data.choice);
choice.votes.push({ ip: ip });
poll.save(function(err, doc) {
var theDoc = {
question: doc.question, _id: doc._id, choices: doc.choices,
userVoted: false, totalVotes: 0
};
for(var i = 0, ln = doc.choices.length; i < ln; i++) {
var choice = doc.choices[i];
for(var j = 0, jLn = choice.votes.length; j < jLn; j++) {
var vote = choice.votes[j];
theDoc.totalVotes++;
theDoc.ip = ip;
if(vote.ip === ip) {
theDoc.userVoted = true;
theDoc.userChoice = { _id: choice._id, text: choice.text };
}
}
}
socket.emit('myvote', theDoc);
socket.broadcast.emit('vote', theDoc);
});
});
});
};
Update
Here is the factory for socket
.factory('socket', function($rootScope) {
var socket = io.connect();
return {
on: function (eventName, callback) {
socket.on(eventName, function () {
var args = arguments;
$rootScope.$apply(function () {
callback.apply(socket, args);
});
});
},
emit: function (eventName, data, callback) {
socket.emit(eventName, data, function () {
var args = arguments;
$rootScope.$apply(function () {
if (callback) {
callback.apply(socket, args);
}
});
})
}
};
});;
You have to make an apply when you're receiving the sockent on AngularJS because socket.io is not "in the AngularJS world".
You have a rerally great tutorial here to do what you want :
http://www.html5rocks.com/en/tutorials/frameworks/angular-websockets/?redirect_from_locale=fr
If you have any questions just ask it !
Hope it helps
This was simply a mix up of variable name, while IBM was using pollId in their route for getting a poll I was using id but had managed to use pollId in my controller, once I changed this all behaved as expected.