Dynamically add to a list in angular - javascript

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();
});

Related

AngularJS: Determining how much time the directive takes to render

How can I measure how much a directive (element) takes to render?
If not, is it possible to determine what directive take the most time to be rendered?
PS. Yes, I've used Batarang but it only showed watch-expressions that take the most time. An yes, I've googled and found a question that is much alike, still there's no answer there.
I created directive to check rendering times of angular view. Directive uses simple but useful speeder lib - https://github.com/maciejsikora/Speeder. It count microseconds from ms-start renders to ms-stop renders.
<span ms-perf ms-start='symbol'></span>
...here some actions ng-ifs, repeats etc.
<span ms-perf ms-stop='symbol'></span>
Full example of using directive with ng-repeats:
https://jsfiddle.net/maciejsikora/4ud2rLgz/
In example directive is used in controller, but it can be used also in another directive. Minuses of this solution is that we need to append directive to DOM and after finding problem it should be removed from there. Of course good idea would be to create provider and configurate it for development and production enviroment, so in production no results and time counting should run.
Why not just use Chrome's Timeline inspector?
You could start recording the timeline before render, and then end it after the state change.
The timeline for rendering the directive alone would be the time in Purple, and the sum of Purple and Painting wedges would give you the total time from the time that the XHR fetch was completed, till the template is painted onto the screen. Is there a reason why this wouldn't be accurate?
For directives without any Promises we can use another directive which will $compile its element and then call $timeout without dirty check (3rd argument - false) in $compile's callback function:
app.directive('measure', function () {
return {
controller: function ($scope, $element, $compile, $timeout, $window) {
$window.start = new Date().getTime();
// make sure to compile only once
if (!$window.done) {
console.log('STARTING MEASUREMENT');
$window.done = true;
$compile($element)($scope, function(clonedElement, scope) {
var timer = $timeout(function () {
console.log('ENDING MEASUREMENT: ' + (new Date().getTime() - $window.start) + 'ms');
$timeout.cancel(timer);
}, 0, false);
});
}
}
};
})
for directives with Promises, we can measure it using again $timeout without dirty check but called in then block of last Promise:
app.directive('someOtherDir', function () {
return {
template: '<div ng-repeat="item in vm.data"><img ng-src="{{ item.thumbnailUrl }}" title="{{ item.title }}"></div>',
controller: function ($http, $timeout, $window) {
console.log('STARTING MEASUREMENT');
$window.start = new Date().getTime();
var vm = this;
$http.get('data.json').then(function (res) {
vm.data = res.data;
var timer = $timeout(function () {
console.log('ENDING MEASUREMENT: ' + (new Date().getTime() - $window.start) + 'ms');
$timeout.cancel(timer);
}, 0, false);
});
},
controllerAs: 'vm'
};
});
here is my playground plunker http://plnkr.co/edit/McWOeF7rZ7ZYKnDWafy6?p=preview, comment / uncomment 2 directives, try to increase i in someDir directive:
for (var i = 0; i < 20; i++) {
vm.dates.push({
timestamp: i * 1000 * 60 * 60 * 24,
label: new Date(i * 1000 * 60 * 60 * 24).toString()
});
}
try 200, 2000...
Honestly this question in an of itself does not have a good answer and I'll explain more below. This question, at least to me, seems more like a means to an end. So I think we need to get at the heart of the real question:
Are you having performance issues you are trying to identify or are you just trying to profile to prove something is fast enough?
Unfortunately, there is too many variable things to knowing how long a directive takes to render, such as:
child directive
async template loading
layout thrashing
Just to name a few of a big hitters. Also that all the directive does it adds elements or sets some classes, then hands control over the the browser itself to render the layout. Once control has been handed over your are basically out of luck.
Modifying the DOM is fast, very fast, take Velosity.js that proved that JS can produce faster and smother animation than CSS, but there are limits:
Limit the number of DOM elements, don't display 10 of thousands of table rows to the user.
Modifying an elements class list of styles, do to the fact that CSS cascades, means all child elements are rendered again. Added a class to the body? Your entire page just for rendered again.
Writing to the DOM then reading a property from the DOM forces the page to immediately ensure the layout is correct, forcing a render. Do thing multiple times very quickly and you causing layout thrashing (multiple sequential forced renders of the DOM).
I suggest the following variant
myApp.directive('log', function() {
return {
controller: function( $scope, $element, $attrs, $transclude ) {
console.log( Date.now() + ' (dirrective controller)' );
//some stuff here
},
link: function( scope, element, attributes, controller, transcludeFn ) {
//some stuff here
console.log( Date.now() + ' (dirrective post-link function)' );
}
};
});
Difference between second log and first log is something very simmilar to time spent to render the directive.

Canonical Way to use JQueryUI Autocomplete with Meteor

Using Meteor, I'd like to understand the most efficient way to use JQuery UI's Autocomplete with large volumes of server-side data.
I have two working proposals and would like to hear opinions on the differences and if there are any better ways to do the same thing.
Using pub/sub:
// Server
Meteor.publish("autocompleteData", function (theSearchTerm) {
var query = {
name: { $regex: theSearchTerm, $options: 'i'}
};
return MyData.find(query, options);
});
// Client
Template.myTemplate.rendered = function() {
initAutocomplete($(this.find('.my.autocomplete')));
};
var initAutocomplete = function(element){
element.customAutocomplete({
source: function(request, callback){
var sub = Meteor.subscribe('autocompleteData', request.term, function(){
var results = MyData.find({}, {limit: 50}).fetch();
sub.stop();
callback(results);
});
},
select: function(event, ui){
// Do stuff with selected value
}
});
};
Using remote functions (Meteor.Methods):
// Server
Meteor.methods({
getData: function(theSearchTerm) {
var query = {
name: { $regex: theSearchTerm, $options: 'i'}
};
return MyData.find(query, {limit: 50}).fetch();
});
});
// Client
Template.myTemplate.rendered = function() {
initAutocomplete($(this.find('.my.autocomplete')));
};
var initAutocomplete = function(element){
element.customAutocomplete({
source: function(request, callback){
Meteor.call('getData', request.term, function(err, results){
callback(results);
});
},
select: function(event, ui){
// Do stuff with selected value
}
});
};
Which, if either, is the the most efficient way to setup a server-side autocomplete using Meteor with a large dataset?
For what it's worth, I'll offer a few of my thoughts on the subject. As a disclaimer, I'm just a Meteor enthusiast and not an expert, so please correct me if I've said something faulty.
To me, it seems like a potential advantage of pub/sub in cases like these is that data is cached. So when subscribing to the same record set, lookup will be near instantaneous since the client can search the local cache instead of asking the server for data again (publication is smart enough not to push repeated data to the client).
However, the advantage is lost here since you're stopping the subscription, so every time the user types the same search term, data is again pushed to the client (at least, the cursor's added event fires again for every document). In this case I would expect the pub/sub to be on nearly equal footing with Meteor.call.
If you want to cache the data of pub/sub, one way is to take out the sub.stop(). But unless your users have the tendency to search similar terms, caching the data is actually worse since with every letter the user types more data will be stored on the client, perhaps never to be seen again (unless searching is such a prominent feature in your app that the user would benefit from this?).
Overall, I see no compelling advantage with using pub/sub over Meteor methods, though I'm not versed in Meteor well enough to offer more specific advantages/disadvantages between the two. I personally think Meteor methods looks cleaner though.
If you're trying to implement a search feature though, I personally like the easy-search package, which supports this type of server-side search with autocomplete. In any case, I hope you get your question resolved! I'm curious to know the answer too.

How to refresh an angular tag

I was brought in to fix a website that was on fire a couple months back. I've got most things under control and I'm down to fixing various wish-list items. One of them involved some angular code that I just can't seem to get to do what I want. On some pages there are videos followed by a short quiz. I need to update the user's scores after each event. So far, this proved to be easy enough for the total score which looked like this:
<a id="updateafterscore" href="~/user/leaderboard/" class="fill-div">
{{ profile.currentScore }}
</a>
And that got updated with this:
document.getElementById('updateafterscore').innerHTML = data.Data.CurrentScore;
So far, so good. However other elements on the page have, thus far, proved impossible to update. Here's what's on the page:
I added the "id="refreshvideo" myself so I could try to alter the tag. Finally, here's the angular module for simple-circle (I've left out the actual drawing code since it's not really relevant):
angular.module('thrive.shared').directive('simpleCircle', function() {
return{
replace: true,
template: '<canvas width="60" height="60" style="margin: -10px 0 0 -15px;"></canvas>',
restrict: 'E',
scope: {
value: '#',
color: '#',
bgColor: '#',
forecolor: '#',
radius: '#'
},
link: function (scope, elem, attrs) {
var multiplyLength = 1;
var canvasElem = elem[0];
var inMotion = false;
if (scope.value <= 2) {
multiplyLength = 5;
}
scope.$watch('value', function() {
drawCircle(canvasElem, scope.color, scope.value * multiplyLength, scope.value, scope.name);
});
function drawCircle(canvas, color, calculatedPoints, displayPoints, name) {
So, to the question: how the heck do I update the number that's displayed? I tried various things:
document.getElementById('refreshvideo').setAttribute('value', data.Data.VideoWatchedCount);
document.getElementById('refreshvideo').setAttribute('data-value', data.Data.VideoWatchedCount);
$scope.profile.videosWatched = data.Data.VideoWatchedCount;
None of these things worked. I inspected the canvas element in the source in the browser and I could see the value and data-value tags change to whatever I set them, but the image remained unchanged. Am I setting the wrong thing? (Perhaps whatever $watch is watching) Do I have to force some kind of re-paint of a canvas element?
#charlietfl means your solution is not actually using AngularJS - you're completely bypassing it. Angular provides two-way data binding between Javascript data and the HTML DOM. All you do is tell it where to draw data, and it will do that for you automatically, keeping it up to date from then on as the data changes.
In Angular, you never call getElementById and certain never set innerHTML because then you block Angular from doing its thing - in many cases you actually break it. Every one of those instances introduces a new bug while "patching" another.
Go back to your example template line:
<a ..attributes...>{{ profile.currentScore }}</a>
When it sees this, Angular will create what it calls a "watcher" on profile.currentScore. If its value right now is '1', it will render this as <a ...>1</a>.
Every digest cycle, that watcher will tell it to look at profile.currentScore to see if it changed. This line of code is pretty typical in JS:
profile.currentScore = 42;
Angular will "see" this happen through that watcher, and will automatically update the rendered template. You do nothing else - and if you ever feel that you need to, it almost always means something else is wrong.
If you're running into this a lot, try the "standard quick-fix". We see this a lot with people who didn't architect an application properly, and they're doing data model updates outside Angular's digest cycle where it can't "see" them. Try wrapping your update code in an $apply() call:
$scope.$apply(function() {
profile.currentScore = 42;
});
If you have a LOT of updates to make and you don't want to nest the call, you can also cheat, like this:
// Lots of stuff...
profile.currentScore = 42;
// Lots more stuff...
$scope.$apply();
You will know right away if you need to do this. If it works, you need to do it. :) If you get an error message in your console saying you're already in a digest cycle, you do NOT need to do it (it's something else).
I mentioned that I thought perhaps I was modifying the wrong profile variable and so it wasn't refreshing. So I looked back a little bit in the code that is supplying the numbers:
angular.module('episodes').controller('episodeCtrl', ['$scope', '$rootScope', '$window', 'episode', 'relatedCourses', 'Video', 'episodeItems', 'profile', 'Profile',
function ($scope, $rootScope, $window, episode, relatedCourses, Video, episodeItems, profile, Profile) {
// stuff skipped....
onComplete: function () {
Video.complete({ videoId: item.item.id }).$promise.then(function () {
item.progress = "Completed";
$scope.loadNextItem();
$scope.profile = Profile.get(); // <<-- gotten from somewhere
$.ajaxSetup({ cache: false });
$.get('/user/getCurrentUserPointsModel', function (data) {
if (data == "")
return;
$scope.profile.currentScore = data.Data.CurrentScore;
$scope.profile.videosWatched = data.Data.VideoWatchedCount;
$scope.profile.testTakenAndCorrectAnswerCount = data.Data.TestTakenAndCorrectAnswerCount;
Profile.save(); // <-- added
The value in $scope.profile is pulled from Profile, but I don't fully get how that gets where it is. I suppose I will need to figure that out because there's another place where these updates have to happen that lack that Profile information. Anyways I added the last 4 lines in place of this:
document.getElementById('updateafterscore').innerHTML = data.Data.CurrentScore;
... and all worked according to plan. I guess I tackle the other part later when I figure out how the data gets to the controller.
You can't do this that way. It's not Angular way of dealing with data.
Read the documentation before https://docs.angularjs.org/tutorial/step_04
If you need to modify your DOM using document.. probably sth wrong is with your code.
BTW. Stop using globals like:
document.getElementById('updateafterscore')

Accessing a collection from outside of the $scope

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.

How do you debug Timing Issues in Javascript?

I recently ran into a familiar javascript/jQuery timing bug and spent too long debugging it. What I need is a smarter debugging path for this problem.
In specific, my issue was that user inputs were supposed to be causing a Mongo database call and the results were sent, after a little math, to displayed outputs. But the displayed outputs were crazily wrong. However, once I added a FireBug break point the problem went away. At that point I knew I had a timing issue, but not how to solve it.
Here are the relavant pieces of code before the error:
handleDataCallBack : function(transport) {
var response = $.parseJSON(transport);
if(!hasErrors) { this.updatePage(response); }
},
accessDatabase : function(){
var params = { ... };
DAL.lookupDatabaseInfo(this.handleCallBackOutputPanel, this, params);
},
calculateValues: function() {
// some numerical values were updated on the page
}
onDomReady : function() {
// ...
//bind drop-down select change events
$('#someDropDown').change(function() {
me.accessDatabase();
me.calculateValues();
});
}
To fix the problem, all I had to do was move the "calculateValues" method from the onDomReady inside the call back:
handleDataCallBack : function(transport) {
var response = $.parseJSON(transport);
this.calculateValues();
if(!hasErrors) { this.updatePage(response); }
},
The problem was that the database hadn't responded before the calculations were started. Sure, that's easy to spot in retrospect. But what methods can I use to debug asynchronous timing issues in javascript/jQuery in the future? This seems well outside the context of IDE tools. And FireBug didn't help. Are there any tools for tracking down asynchronous web development issues? Or maybe some time-tested methods?
i assume your problem is caused here:
$('#someDropDown').change(function() {
me.accessDatabase();
me.calculateValues();
});
this issue is that your calculations are done just right after the call. seeing that the DB call is async, calculate does not wait for it. however, you can do it using "callbacks". i see you do try to implement it and yes, it is correct. however, i find this more elegant:
calculateValues: function() {
// some numerical values were updated on the page
},
//since this is your general callback hander
//you hand over the return data AND the callbackAfter
handleDataCallBack: function(transport, callbackAfter) {
var response = $.parseJSON(transport);
//you may need to use apply, im lost in scoping here
callbackAfter();
//or
callbackAfter.apply(scope);
if (!hasErrors) {
this.updatePage(response);
}
},
accessDatabase: function(callbackAfter) {
var params = {};
//pass callbackAfter to the function,
//after this is done, pass it to the handler
DAL.lookupDatabaseInfo(this.handleCallBackOutputPanel, this, params, callbackAfter);
},
onDomReady: function() {
$('#someDropDown').change(function() {
me.accessDatabase(function() {
//send over what you want done after.
//we'll call it "callbackAfter" for easy tracing
me.calculateValues();
});
});
}​

Categories