I have some code that I need to run only once, but I'm not sure where do that code belongs to (service? factory?)
This is the code:
socket.on('recv chat', function (data){
$("#chat").append(
"<b><" + data.nick + "></b>: " +
data.texto +
"<br>"
);
});
As you can see from the code, it's just a basic chat-app. My whole webpage has a few tabs and one of those tabs is the chat-tab. If I put this code inside my chat's controller, it gets executed on each tab-switch, so when somebody sends a message, it gets appended a few times.
Where should I place it for it to be executed only once?
You say it should execute only once, but presumably what you actually want is just that it display the values. If so the obvious thing would be for the code to update the model and then use angular's data binding for the display. (A good rule of thumb in angular would be that anywhere except a directive that tries to manipulate the DOM is probably doing it wrong).
So, some untested code to put inside your controller might be:
socket.on('recv chat', function (data){
$scope.apply(function() {
$scope.nick = data.nick;
$scope.texto = data.texto;
});
});
And your html just has:
<div ng-show="nick"><b><{{nick}}></b>: {{texto}}</div>
I think you need to wrap the model updates in $scope.apply() otherwise the event won't be happening in the correct angular context.
Answer to your comment:
Is there a new socket within each new instance of the controller? If so there's no problem as the old event handler at worst updates the old model and should go away when the old socket goes away. If you're re-using the socket between controllers then I think you want to define a service to handle the socket and you can register the callback with the service.
var stopWatch = $scope.$on('someEvent', function(){
//some code here
stopWatch();
});
This is not the best solution but this is by far the best working solution i have came across. You need to remove the already registered even. Just add this line before you attach the events.
socket.removeAllListeners();
socket.on('recv chat', function (data){
$("#chat").append(
"<b><" + data.nick + "></b>: " +
data.texto +
"<br>"
);
});
Related
Goal: a dynamically generated list from external source.
I've set up a simple angular app that gets a list of events from an external JSON source. I want the list to update when events are added from the external source. It's currently working, but I have one problem and three questions:
1) I'm currently rewriting the list every 15 seconds. How do I just add to the end of the list without rewriting the list? (problem and question)
2) Is there another, better way to keep up to date with the external list? I'm trying to follow "RESTful" techniques, does that mean I should rely on the client side code to poll every so many seconds the way I'm doing? (best practice question)
3) Is setting the timeout in the controller best practice? Because it's controlling the action on the page?(best practice/comprehension question)
var eventModule = angular.module('eventModule', []);
eventModule.controller('eventControlller',
function($scope, $timeout, eventList) {
$scope.events = eventList.getAllEvents().success(
function(events) {$scope.events = events});
var poll = function() {
$timeout(function() {
$scope.events = eventList.getAllEvents().success(
function(events) {$scope.events = events});
poll();
}, 15000);
};
poll();
});
eventModule.factory('eventList', function($http) {
var url = "http://localhost/d8/events/request";
return {
getAllEvents: function() {
return $http.get(url);
}
};
});
If the list is an array, and you want to add new members to it, there are a few different ways. One way is to use the prototype.concat() function, like so:
function(events) {
$scope.events = $scope.events.concat(events)
});
If you cannot use that then you can go for loops solution:
function concatenateEvents(events) {
events.forEach(function(element) {
events.push(element);
}
}
Regarding the best ways to update the list, it depends on your requirements. If 15 seconds is not too long for you, then you can keep this logic, but if you need to speed up the response time, or even make it real time, then you need to emulate server-push architecture, which is different than the default web architecture, which is request-response architecture. Basically you may want to explore web sockets, and/or long polling, or reverse ajax, or comet... has many names. Web sockets is the recommended solution, others are only in case you have to use some non-compatible browsers.
Regarding the third question, I honestly don't know. Truly it doesn't feel good to control the UI from within your controller, but as I don't really know what your app is supposed to be doing, I don't know whether this is actually a bad way to do it.
Hope this helps!
EDIT - forgot to add another important point: You don't need to assign the eventList.getAllEvents() to $scope.events, as you are doing that in the callback handler function.
Perhaps you can modify your controller to something like this:
eventModule.controller('eventControlller', function($scope, $timeout, eventList) {
eventList.getAllEvents().success(
function(events) {
$scope.events = events
});
var poll = function() {
$timeout(function() {
eventList.getAllEvents().success(
function(events) {$scope.events = events});
poll();
}, 15000);
};
poll();
});
I have an Angular app used to track hours worked by the user. When the user adds a new job, they are taken through a wizard, with each page/controller adding a new property to the job object. Once the wizard is complete, the user can start tracking by navigating to the jobs home page from the app main page.
It is, however, possible to exit the wizard before it is completed (via the back button) and then navigate to the home page of the job. What I need is for the controller for that home page to redirect to the appropriate wizard page for whichever job property is missing.
The job variable is retrieved from local storage at the start of the controller code.
var job = DatastoreService.objectJob();
job.initFromHash($routeParams.jobHash);
function checkJobProps(prop, route){
if(!job.data.hasOwnProperty(prop))
$location.path('/wizard/add-position/' + $routeParams.jobHash + '/' + route);
}
checkJobProps('taxSettings', 'tax');
checkJobProps('payrollSettings','payroll-opt');
checkJobProps('breaks', 'breaks');
checkJobProps('allowances', 'allowances');
checkJobProps('deductions', 'deductions');
checkJobProps('generalSettings', 'general-settings');
There is code below this on the controller that breaks if certain properties are not available. None of these function calls execute fast enough to prevent errors. They will redirect, but not elegantly and it will also always be the last one in the list that takes effect.
Do I do this with a promise? The navigation that is used from the home page of the app to the home page of each job is a directive so, I guess it may be possible to init the job from the $routeParams.jobhash and check these properties within the directive, but I would have to learn more about directives first.
Any help would be much appreciated.
$location.path() is asynchronous and will not prevent the code that follows it from executing. You will have to manually stop the execution with a return statement.
Note that the return statement must belong to the controller function block. You cannot put it inside another function since that will only stop the execution of that specific function.
Something along these lines should work:
var job = DatastoreService.objectJob();
job.initFromHash($routeParams.jobHash);
var redirectPath;
function checkJobProps(prop, route) {
if (redirectPath || job.data.hasOwnProperty(prop)) return;
redirectPath = '/wizard/add-position/' + $routeParams.jobHash + '/' + route;
}
checkJobProps('taxSettings', 'tax');
checkJobProps('payrollSettings', 'payroll-opt');
checkJobProps('breaks', 'breaks');
checkJobProps('allowances', 'allowances');
checkJobProps('deductions', 'deductions');
checkJobProps('generalSettings', 'general-settings');
if (redirectPath) return $location.path(redirectPath);
... rest of the code ...
If there is a way in ui-grid that I can know a grid is finish updating the rows?(like a filter is being applied etc)? I want to run some function after the grid view changes.
I tried the following method:
$scope.filteredRows = $scope.gridApi.core.getVisibleRows($scope.gridApi.grid);
$scope.$watch('filteredRows', function(){console.log('view updated');});
The above approach works when the grid just finish initiating, after that, it won't work anymore.
I also tried using the filterChanged api:
$scope.gridApi.core.on.filterChanged($scope, function() {
console.log('filter changed');
foo();
});
The problem with this method is that although I can get notified when the filter is changed, but if the grid is very large, it needs some time to finish updating the view, and before that, the function foo() is being called before the grid update is finished.
Any idea will be appreciated.
I've seen use of $scope.grid.api.core.on.rowsRendered( $scope, $scope.col.updateAggregationValue ); in ui-grid-footer-cell.js. I'm not sure exactly when rowsRendered fires, but given it's being used to calculate aggregations and aggregations require knowledge whenever the rows are changed, and must run after the rowsProcessors finish running, there's a good chance that it's what you want.
EDIT: the framework to use it would be:
Define a function that you want to call when the visible rows have changed
var myFunction = function() {
do some stuff
};
Set this function to be called whenever rows are rendered
$scope.gridApi.core.on.rowsRendered( $scope, myFunction );
Well, I found a workaround, in order to call the function after the grid is updated, which takes some time, I added a delay in filterChanged event:
$scope.gridApi.core.on.filterChanged($scope, function() {
console.log('filter changed');
$timeout(foo(),800);
});
To use the $timeout, you need to add that to your controller first.
Ok.. I've tried angular.js. It is awesome. I'm impressed. I can get bindings and stuff.. Cool.
Now what if I need to access to my data from outside of the $scope? Let's say I have a signalR hub that sends some data and function that intercepts that and should add a new item or modify existing. How do I do that? Can you show me on this example how can I access $scope.twitterResult from click handle?
<script>
angular.module('Twitter', ['ngResource'])
function TwitterCtrl($scope, $resource){
$scope.twitter = $resource('http://search.twitter.com/:action',
{action: 'search.json', q: 'obama', callback:'JSON_CALLBACK'},
{get:{method:'JSONP'}});
$scope.doSearch = function(){
$scope.twitterResult = $scope.twitter.get();
}
}
$(function(){
$('#addItem').click(function(){
// add a row to $scope.twitterResult
});
});
</script>
<body>
<div data-loading></div>
<div ng-controller='TwitterCtrl' ng-init="doSearch()">
<ul>
<li ng-repeat='tweet in twitterResult.results'><p> {{tweet.text}}</p></li>
</ul>
</div>
</body>
A better way would be to wrap your "signal hub" in an AngularJS service. Take a look on my blog post about using web sockets with AngularJS, specifically "Interacting with Socket.IO."
Why did you write:
$(function(){
$('#addItem').click(function(){
// add a row to $scope.twitterResult
});
});
And not just use ng-click? Is this some 3rd party code or widget? Pending on these this, I'll try to better advise you and write up some example code.
If you have to register an event handler, you should do so through a directive. Otherwise things will get complicated when you start managing the lifecycles of these outside-of-angular event bindings.
General answer is: you don't simply mess with the scopes from the outside.
But the requirement you have is a genuine one.
So in order to do what you want you need to establish a communication between outside of the scope and the scope itself.
The easiest way is to export the $scope to window and just mess with it, breaching into the scope from outside. You should NEVER do this. There be dragons.
The scope should maintain it's internal state.
I'm not exactly familiar with angular but you can do something to the effect of:
function TwitterCtrl($scope, $resource) {
// ...
$('body').bind('newTweetsArrived', data) {
// data contains the new tweets
// the decision to accept or not new tweets is made within the control
if (in_the_mood_to_accept_new_tweets) {
// add new tweets to the $scope.twitterResult
}
// optionally notify other components that new tweets are accepted
// so that they can adjust or whatever
$('body').trigger('afterNewTweetsArrived');
}
}
// you add new tweets by triggering global custom event
$(function(){
$('#addItem').click(function(){
$('body').trigger('newTweetsArrived', { ...here_are_the_tweets... });
});
});
You could probably do something like this, but I'm not sure if it's the best idea:
var myTwitterScope;
angular.module('Twitter', ['ngResource'])
function TwitterCtrl($scope, $resource){
$scope.twitter = $resource('http://search.twitter.com/:action',
{action: 'search.json', q: 'obama', callback:'JSON_CALLBACK'},
{get:{method:'JSONP'}});
$scope.doSearch = function(){
myTwitterScope = $scope;
$scope.twitterResult = $scope.twitter.get();
}
}
$(function(){
$('#addItem').click(function(){
// add a row to $scope.twitterResult
myTwitterScope.twitterResult.push(...); // or however you would do this.
});
});
As others have mentioned, this is not the cleanest solution.
Very confused here.
I have a search box which reads a list of school names from my database. When I select a school, the id (from the db) gets put in a hidden textbox.
I also have a search box which reads a list of courses from my database. However, I made the query so that it only reads the courses from the selected school.
It does that, in theory.
I was planning to pass the school id, which I grab from the hidden box, to the search script which in turn passes it to my database query. However, the variable I put my school id in doesn't seem to be updating.. yet it does. Let me explain.
I come on the page. The school for my test account has id 1. The id number in my hidden box is indeed 1. I search for a school which I know has some courses assigned to it: the id number in the box changes to 3.
I have a JS variable called school_id which I declared outside of my $(document).ready. I assume that means it's global (that's what I got taught even though SO told me once it isn't really the correct way to do this. Still have to look into that). I wrote a function which updates this variable when the school search box loses focus:
$("#school").blur(function() {
school_id = $("#school_id").val();
});
A quick javascript:alert(school_id); in my browser bar also shows the updated variable: it is now 3 instead of 1.
Onto the search script part of my page (excerpt of the script):
script:"/profiel/search_richting?json=true&limit=6&id=" + school_id + "&"
As you can see, I pass the school_id variable to the script here. However, what seems to be happening is that it always passes '1', the default variable when the page loads. It simply ignores the updated variable. Does this string get parsed when the page loads? In other words, as soon as the page loads, does it actually say &id=1? That's the only idea I can come up with why it would always pass '1'.
Is there a way to make this variable update in my script string? Or what would be the best way to solve this? I'm probably missing out on something very simple here again, as usual. Thanks a lot.
EDIT
Updated per request. I added a function getTheString as was suggest and I use the value of this function to get the URL. Still doesn't work though, it still seems to be concatenating before I get a chance to update the var. HOWEVER, with this code, my ajax log says id:[object HTMLInputElement], instead of id:1. Not sure what that means.
<script type="text/javascript">
var school_id;
$(document).ready(function() {
$("#school").blur(function() {
school_id = $("#school_id").val();
});
// zoekfunctie
var scholen = {
script:"/profiel/search_school?json=true&limit=6&",
varname:"input",
json:true,
shownoresults:false,
maxresults:6,
callback: function (obj) { document.getElementById('school_id').value = obj.id; }
};
var as_json = new bsn.AutoSuggest('school', scholen);
var richtingen = {
script: getTheString(),
varname:"input",
json:true,
shownoresults:true,
maxresults:6
};
var as_json2 = new bsn.AutoSuggest('studierichting', richtingen);
});
function getTheString() {
return "/profiel/search_richting?json=true&limit=6&id=" + school_id + "&";
}
</script>
This is because the URL is static, it is not updated as the ID changes.
You should update the URL as part of the code you wrote to get the ID:
$("#school").blur(function() {
school_id = $("#school_id").val();
// update URL here ...
});
Aren't you concatenating script:"/profiel/search_richting?json=true&limit=6&id=" + school_id + "&" before the event is fired and the var updated?
Okay. So the problem was my third party plug-in instead of the code I wrote. I fixed this by editing the code of the autoSuggest plugin so it now includes my id field in the AJAX request.
var url = this.oP.script+this.oP.varname+"="+encodeURIComponent(this.sInp)+"&id="+ $("#school_id").val();
Thanks to everyone who tried to help me out!