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')
Related
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.
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 angular code that fetches 8 json files asynchronously each via $http.get. This is called using ng-init="someFunct()" in a template code that is attached. Everything works great including filtering when a user types into an input text box. Filtering is especially important to my application.
To make filtering even better, I extract keywords from the said json files which I then wrap with <span class="tag" ng-click="filterWith='kywd'">{{kywd}}</span> in the hope that a user can click on the tags instead of type. This ONLY works if I embed the tags statically - in the real application I cannot know the keywords in advance. If I insert dynamically via $("#someContainerID").append(TAG_HTML_CODE) or similar it NEVER works!
In a nutshell this is what I need to achieve:
1) Dynamically inject multiple (in hundreds) such tags into DOM;
2) Inject the tags ONLY after everything else has loaded and compiled - but especially after the json files have been read and keywords extracted;
3) The tags that I inject need to respond to something like ng-click="filterWith='some_keyword'"
If there was a way to tell when AngularJS has finished all other processing - how great this would be! I have read everywhere and it seems so cryptic and confusing - pls HELP!
I have even tried the following code to no avail:
$timeout(function () {
$scope.$apply(function () {
//code that works on the keywords - works perfect!
var filterRegex = /\s*([\w\d.%]+)\s*/i;
var dom_elem = angular.element(document.querySelector("#filter_tags"));
dom_elem.html("");
for (var m = 0; m < tags.length; m += 1) {
var match = filterRegex.exec(tags[m][0]);
if (match != null) {
dom_elem.append($compile("<span data-ng-model=\"filterWith\" data-ng-click=\"filterWith='" + match[1] + "'\" title=\"" + tags[m][1] + "\" class=\"sk3tag clk\">" + match[1] + "</span>")($scope));
}
}
});
}, 10000, false);
}
EDIT: Narrowed the scope of my challenge to mainly one!
The bigger challenge for me is how to enable ng-click in the dynamically injected code and how to do it right.
Use Promise.all() to trigger when everything is loaded.
Earlier I had asked the question above. Somebody suggested I read further on directives instead. I did, fairly well. I came up with the following solution, to use click events on html code injected dynamically to DOM. I thank truly God for helping me figure it out, eventually. I no longer need to wait for the asynch data, whenever it comes and hence updates the model, my html tags are updated automatically - MVC magic! It seems to work great!
ANGULAR
//excerpt
myNgApp.controller('ctlTodayLatest', ['$scope', '$timeout', '$compile', '$http', function () {
$http.get('/filtertags.json').then(function (response) {
$scope.filterTags = response;
},
function (response) {
// called asynchronously if an error occurs
// or server returns response with an error status.
console.log(response);
}
);
}]);
myNgApp.directive("myFilterTag", function () {
return {
template: "<span data-ng-repeat=\"tag in filterTags\" title=\"{{tag[0]}}\" class=\"mytag clk\" ng-click=\"filterWith(tag[0])\">{{tag[0]}}</span>",
link: function (scope, element, attrs) {
scope.filterWith = function (term) {
scope.fQ = term;
};
}
};
});
HTML
//excerpt
<div id="filter_tags" class="xip2 TAj" my-filter-tag></div>
I am using angular ui router to handle some routing on my frontend. This is what my routing code looks like.
// angular config
$stateProvider.state('app', {
templateUrl: '/static/partials/home.html',
controller: 'NavCtrl'
});
$stateProvider.state('app.reader', {
url : '/reader/*path?start&end&column&page',
templateUrl: '/static/partials/reader.html',
resolve : {
panelContent : [
'$state', '$stateParams', '$http',
function ($state, $stateParams, $http) {
alert('resolving panel Content');
return []; // simplest thing possible to illustrate my point
}
]
},
controller: 'ReaderCtrl'
});
/// etc etc
$urlRouterProvider.otherwise('/reader/');
My html makes use of multiple nested views, I'll try and illustrate as best I can
index.html
<html>
<div ui-view></div> <!-- /static/partials/home.html gets injected here -->
</html>
/static/home.html
<html>
<!-- some side bar stuff -->
<!-- reader -->
<div ui-view></div> <!-- /static/partials/reader.html gets injected here -->
</html>
So I've got multiple levels of nesting going on
-- index.html
-- home.html
-- reader.html
Now, when I load the page for the first time, my alert message
alert('resolving panel Content');
fires just once.. that makes sense. However, let's say I click "next page" inside my pagination..
<!-- inside /static/partials/reader.html -->
<uib
pagination total-items= "totalItems"
ng-model= "pageNumber"
ng-change= "pageUpdate"
max-size= "maxPageNumbersDisplayed"
></uib>
this eventually fires a function inside my "ReaderCtrl"
$scope.pageUpdate(page) {
$state.go( '.', {page: page});
}
This updates the url, from going to something like this
/#/reader/<my path>
to something like this
/#/reader/<my_path>?page=2
Now for the part that has me tearing my hair out.
I get back to the "resolve" code block in the reader section of my routing.
The alert message happens twice.
By doing a bit of debugging in the web console, I discovered that the order goes
1) alert message in resolve
2) travel through the entirety of ReaderCtrl
3) lots and lots of angular calls
4) alert message (2nd time)
5) travel through entirety of ReaderCtrl a second time.
You might be inclined to know what is going on in NavCtrl, but I am not making any calls there. All that is in NavCtrl are functions that ReaderCtrl can inherit, in order to update the scope for /static/partials/home.html
So really, it appears as though I am stuck on step 3 here.
Does anyone have any ideas as to why my resolve block appears to be firing twice?
edit:
after a bit more debugging, I have seemed to figure out that the order goes something like this, starting right after the "updatePage" function executes.
1) first "resolving message"
-- the url has not yet changed
2) second "resolving message"
-- the url appears to have changed very shortly before this message
So, I guess my question now is...
why does
$state.go('.', args);
NOT change the url before the first alert fires, but DOES change the url at/near the second alert?
edit 2: could not end up fixing my issue, so I sort of hacked around it for the time being... I essentially made a function that did what I assume $state.go() was doing behind the scenes, and constructed the url.
function _mk_url(args) {
var url = "/reader";
var pageNumber = args.pageNumber || 1;
url += "?page=" + pageNumber;
var columns = args.columns || [];
columns.forEach(function(d) {
url += "&column=" + d;
});
//etc..
return url;
}
var args = {"columns" : ["a", "b", "c"], "pageNumber" : 2};
var url = _mk_url(args);
$location.url(url);
I was having this problem and found out it was because I was calling my resolve function manually somewhere else in the code. Search your code for panelContent() and you may find where it's getting triggered again.
I'd got this problem. The reason was in my html template. I used ui-sref directive in both child and parent elements
<li ui-sref="{{r.name}}" ng-class="vm.isCurrent(r)" ng-repeat="r in vm.settingsRoutes">
<span ui-sref="{{r.name}}" ng-bind-html="r.title"></span>
</li>
so when I clicked on span, I fired stateChange twice.
I've had the same bug.
And I found that I was changed $stateParams in one of the resolve functions.
The solution is make a copy from this object and then do what you want with a copy.
resolve: {
/** #ngInject */
searchParams: function ($stateParams) {
let params = angular.copy($stateParams); // good
// good:
if (params.pending === undefined) {
params.pending = true;
}
// bad:
if ($stateParams.redirect === 'true') {
$stateParams.pending = false; // this line changing the URL
}
return params;
},
}
I seem to be having a problem using a directory integrating packery into angular. It seems to be working just fine when I load onto the first page, but when I leave the page and return it looks like its breaking packery in a weird sort of a way where it's working but everything seems to be collapsed to 1 column and I can't for the life of me figure out how.
I'm using this example I found http://codepen.io/amergin/pen/AEIvt , and it seems to be working great. for reference here is the directory
angular.module('demoApp')
.directive('packeryAngular', ['$rootScope', '$timeout',
function($rootScope, $timeout) {
return {
restrict: 'A',
scope: true,
link: function(scope, element, attrs) {
//console.log("link called on", element[0]);
scope.element = element;
if (!$rootScope.packery) {
$rootScope.packery = new Packery(element[0].parentElement, {
gutter: 10,
columnWidth: '.grid-sizer',
rowHeight: 300,
itemSelector: '.item'
});
var draggable1 = new Draggabilly(element[0]);
$rootScope.packery.bindDraggabillyEvents(draggable1);
var orderItems = function() {
var itemElems = $rootScope.packery.getItemElements();
};
$rootScope.packery.on('layoutComplete', orderItems);
$rootScope.packery.on('dragItemPositioned', orderItems);
} else {
// console.log("else", element[0]);
$timeout(function() {
$rootScope.packery.appended(element[0]);
});
var draggable2 = new Draggabilly(element[0], {handle: '.handle'} );
$rootScope.packery.bindDraggabillyEvents(draggable2);
}
$timeout(function() {
$rootScope.packery.layout();
});
// watch for destroying an item
scope.$on('$destroy', function() {
$rootScope.packery.remove(scope.element[0]);
scope.packery.layout();
});
}
};
}
]);
and in my template I just have
<div class="item gs-w" ng-class="widget.size" ng-repeat="widget in contentHere" packery-angular >
Pretty straight forward, and again this works great (thank you to the codepen author!), however it's going haywire when I swap views. What's weird is the directive is definitely running because i can drag and re arrange the packery items, but they are all stuck on 1 column on the left most side and I can't figuire out for the life of me why.
I don't know if this is relevant but it might be worth sharing - 1. I am using the ngroutes to swap my views around the typical way. 2. This directive call is nested inside a view. 3. As I mentioned, packery and dragabilly are running when I return the page, they are just stuck on 1 column to the left (can drag up and down).
I really have no idea where to start with this as I have no errors thrown or clues. Would appreciate any help. Thanks for taking the time to read!
Edit - I have some pictures to clarify
This is how it looks normally -
And this is how it looks when you leave and return to the page
you can see it's going over the buttons too which are in a bootstrap row so I'm not sure what's going on with it. You can still drag and move them in both instances.
I think your issue might be here:
if (!$rootScope.packery) { ... }
Within that if block you are defining $rootScope.packery and initializing the plugin. Your destroy method never nullifies $rootScope.packery, so the plugin is never re-initialized. When you return to the view.
Try modifying like so:
// watch for destroying an item
scope.$on('$destroy', function() {
$rootScope.packery.remove(scope.element[0]);
scope.packery.layout();
$rootScope.packery = null; // add this line
});