Add Content To DOM AFTER AngularJS Has Completed Asynchronous Loading - javascript

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>

Related

Changing URL strings via JS on MediaWiki site

So...I've been setting up a brand-new wiki for the past four days. While everything's going almost smoothly, I'd like to deal with a special touchup.
I created a preload template à la Wikipedia, "Template starter", some hours ago. While I could go the other way and create an input form, I want to make it so that red links in the Template namespace always lead to the code from this page, whenever a new one is ready for creation. So that:
http://rfm.referata.com/w/index.php?title=Template:Foo&action=edit&redlink=1 (default)
becomes:
http://rfm.referata.com/w/index.php?title=Template:Foo&action=edit&redlink=1&preload=Template:Template+starter (target)
by way of a JavaScript snippet added to the site's Common.js and Mobile.js.
Haven't come across similar MW-based examples, so I'd like to make sure everything's A-O.K.
Working from this answer, you could do something like this in Mediawiki:Common.js:
$(function() {
var preload = 'Template:Template_starter';
$("body.ns-10 #mw-content-text a.new").attr('href', function(i, h) {
// Maybe some links already have a preload parameter.
if (h.indexOf('preload=') !== -1) {
return h;
}
// All others get it appended.
return h + "&preload=" + preload;
});
});
Note that the check for a pre-existing '&preload=' isn't all that robust.
This extension should work well in your case https://www.mediawiki.org/wiki/Extension:NewArticleTemplate
Improving on what you provided last time, Mr. Wilson:
$(function() {
var preload = 'Template:Template_starter';
$("a.new[href*='Template:']").attr('href', function(i, h) {
// Maybe some links already have a preload parameter.
if (h.indexOf('preload=') !== -1) {
return h;
}
// All others get it appended.
return h + "&preload=" + preload;
});
});
Note that the previous version only worked within pages in the Template namespace; this fix applies to all pages with a red link to a nonexistent template. Thanks a dozen!

AngularJS UI router's "resolve" fires twice

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;
},
}

AngularJS infinite-scroll issue

I'm trying to use infinite-scroll to lazy load images. I'm getting the following error when it's called though:
TypeError: undefined is not a function
at handler (http://onfilm.us/ng-infinite-scroll.js:31:34)
Here's a very watered down look of what I have thus far.
function tagsController($scope) {
$scope.handleClick = function(tags) {
// Parse Tags
$scope.finished_tags = parsed_data;
};
$scope.$emit( 'handleEmit', { tags = $scope.finished_tags; });
};
function imagesController($scope,$http) {
var rows_per = 5;
$scope.$on('handleBroadcast', function(event, args) {
// Sort the images here, put them in matrix
// Example: matrix[row_number] = { picture1, picture2, picture3 }
$scope.data = matrix;
$scope.loadMore();
};
$scope.loadMore() = function() {
var last = $scope.images.length;
for ( var i = 0; i < rows_per; i++ ) {
$scope.images[last + i] = new Array();
$scope.images[last + i] = $scope.data[last + i].slice( 0 );
}
}
}
The rough idea is that the page loads the first time (w/ no tags) and get images from a PHP script. All of them. They are stored, and loadMore() is called which will populate $scope.images with 5 rows of images. It does, and they are loaded.
The line in that script is accessing $window.height and $window.scrollup. I'm still pretty green w/ Javascript, so feel free to lambast me if I'm doing something horribly wrong.
This is the broken version I'm testing with:
http://onfilm.us/test.html
Here is a version before the lazy loading was implemented, if seeing how the tags work will help. I don't think that's the issue here though.
http://onfilm.us/image_index.html
EDIT: I do think this is a problem w/ the ng-infinite-scroll.js script. The error is on line 31 (of version 1.0.0). It's telling me:
TypeError: undefined is not a function
It doesn't like $window apparently.
My JS Kung Fu is not really equipped to say why. YOu can see a literal copy/paste job from the simple demo here (with the error) onfilm.us/scroll2.html
By refering your site, It appears at first instance that your HTML-markup is not appropriate. You should move infinite-scroll to the parent of ng-repeat directive so that it will not make overlapping calls for each row generated. Please visit http://binarymuse.github.io/ngInfiniteScroll/demo_basic.html

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')

$location does not navigate on first ng-click callback

Something weird is going on in my AngularJS application. I have a view which has a list of anchor tags bound to an array of objects (I'm using ng-repeat to render the list). Each one of the anchor tag has an ng-click bound to a method on the controller. The method is as follows:
$scope.onSiteSelected = function ($event, site) {
$event.preventDefault();
$.ajax({
url: '/Site/GetSiteMap/' + site.Id,
success: function (response) {
if (response && response.length > 0) {
menuManager.setSiteMap($scope.$root.menu, response[0]);
var url = response[0].Children[0].Settings.Url;
$location.path(url);
}
}
});
}
The url var is being initialized to the correct value every time. But, when I click on the anchor tag the first time, $location.path(url) does nothing, and when I click it for a second time, it does navigate to the target url.
UPDATE:
Ok I got it working by using the $http service as follows:
$scope.onSiteSelected = function ($event, site) {
$event.preventDefault();
var address = '/Site/GetSiteMap/' + site.Id;
$http.get(address)
.success(function (response) {
if (response && response.length > 0) {
menuManager.setSiteMap($scope.$root.menu, response[0]);
var url = response[0].Children[2].Settings.Url;
$location.path(url);
}
});
}
Does it really make a difference if I used $,ajax or $http? I thought the two can be used interchangeably...?
No you cannot use them interchangibly, $.ajax is jQuery whereas $http is angular's http service.
Resist the urge to use jQuery as there is almost always a way to do it in the angular way.
That being said, if you do something outside of angular world, (mostly callbacks didn't occur from angular), you need to apply the changes to force a digest cycle.
$.doSth(function callback() {
$scope.$apply(function () {
// your actual code
});
});
Please read the most voted answer in "Thinking in AngularJS" if I have a jQuery background? . This would guide you to avoid using jQuery where there is an alternative.

Categories