ng-repeat and ng-href - when should it update? - angularjs

I am having an issue where a value in an object in a list is changing, but the change is not being reflected in ng-href.
I have the following HTML:
<div class='tabrow docRow'>
<span data-ng-repeat='f in dp.source.files'>
<span class='linkedDoc' >
<a class='attachedCaption' target='_blank'
data-ng-href='{{f.vpath}}'>{{f.caption}}
<img class='attachedSrc' data-ng-src='{{ f.vimg }}'/>
</a>
</span>
</span>
</div>
I have a watch on dp.source, and when it changes a call a function that loops through each f in dp.source.files and adds v.img and v.path.
vimg is updated before the callback in the watch exits. vpath is updated asynchronously, maybe a second or two after the callback completes. Since it is updated in an asynch call, I use $apply.
$scope.$watch('dp.source', function() {
for (var idx = 0; idx < $scope.dp.source.files.length; idx++) {
var f = $scope.dp.source.files[idx];
f.vimg = _ev.imgMapper(dataProvider.imgMap, f.path); <--- immediate
f.vpath = '#';
attachService.getURL(f); <-- asynch call that updates f.vpath
}
}
attachService:
myObj.getURL = function(obj) {
syncService.getURL(obj, function(url) { <-- asynch return
$rootScope.$apply(function() {
obj.vPath = url;
});
});
};
vimg is present on the rendered page immediately, while the change in vpath is NEVER reflected on the page.
What am I missing?
To be clear, I have verified that the variable f.vpath IS getting updated.

OMG is was a spelling error!
the HTML expects f.vpath and attachService updated obj.vPath
It was that simple

Related

Object changes inside array but stays unchanged on page

I have a table which is made by ng-repeat. That part which doesnt get refreshed is below;
<tbody class="defTbody" ng-repeat="activity in activityList track by activity.ActivityID">
<tr class="{{activity.DetailExpanded == true ? 'shown' : ''}}">
<td>
<a ng-if="!activity.DetailExpanded" ng-click="activity.DetailExpanded = true"><i class="fa fa-fw fa-plus-circle txt-color-green fa-lg"></i></a>
<a ng-if="activity.DetailExpanded" ng-click="activity.DetailExpanded = false"><i class="fa fa-fw fa-minus-circle txt-color-red fa-lg"></i></a>
</td>
<td style="font-size:smaller">
<a ng-click="getActivityStatements(activity.ActivityID)" style="cursor: pointer;">
{{activity.PlanDateTime}} / {{activity.ActivityType_DisplayName}}
<br />
<small class="text-muted"><i>{{activity.ActivityID}}<i></i></i></small>
</a>
</td>
<td style="font-size:smaller">
{{activity.ParentActivityDisplayName}}
<br />
<small class="text-muted"><i>{{activity.ParentActivityID}}<i></i></i></small>
</td>
<td>{{activity.Customer_DisplayName}}</td>
<td>{{activity.ActivityStatus_DisplayName}}</td>
<td>
{{activity.StatusReason}}
<br />
{{activity.StatusReasonNote}}
</td>
</tr>
</tbody>
$scope.activityList is fine, it gets populated correctly. This is how I change an item in activiyList
$scope.UpdateActivityListItem = function (activityID, prevResponseDetail) {
var url = "/api/activity/GetActivityResponse?activityID=" + activityID;
$http({
method: 'GET',
url: url
}).then(function successCallback(result) {
var response = result.data;
if (response.Status == "SUCCESS") {
var activity = $scope.activityList.filter(activity => activity.ActivityID == activityID)[0];
var renewedAct = response.Detail[0];
activity = renewedAct;
$scope.$apply();
console.log("yenilenmiş aktivite:" +$scope.activityList)
$('#modalActivityDetail').modal('hide');
console.log(JSON.stringify($scope.activityList));
$scope.addSpinnerClass(spinner);
}
else {
console.log("fail");
}
},
function errorCallback(result) {
console.log("error");
}
);
};
I confirmed in debug that item gets "updated" nicely but in screen it stays same.
I thought this was kind of a problem whicj $scope.$apply() can solve but adding it (as shown in code piece above) caused this problem
Error: error:inprog
Action Already In Progress
$digest already in progress
What else can I do?
PS: This is written in Angular JS 1.0
check this, You are filtering activityList and updating a filtered array (result) first element not activityList array.
var renewedAct = response.Detail[0];
for (let index = 0; index < $scope.activityList.length; index++) {
const activity = $scope.activityList[index];
if (activity.ActivityID == activityID) {
$scope.activityList[index] = renewedAct;
break;
}
}
You can do that even without looping again using splice with a single line
Here is the code,
var activity = $scope.activityList.filter(activity => activity.ActivityID == activityID)[0];
var index = $scope.activityList.indexOf(activity);
var renewedAct = response.Detail[0];
activity = renewedAct;
$scope.activityList.splice(index, 1, activity)
Note: The above line removes that element at that place and adds your new element.
Before calling $apply please check weather the digest cycle is busy or not.
if(!$scope.$$phase) {
//$digest or $apply
}
---------EDIT------
A new, powerful method has been added to any $scope: $evalAsync. Basically, it will execute its callback within the current digest cycle if one is occurring, otherwise a new digest cycle will start executing the callback.
Try this:
In these cases and all the others where you had a !$scope.$$phase, be sure to use $scope.$evalAsync( callback ) do all related changes in callback.

Angular Material - infinite scroll not working

I'm using Angular Material's infinite scroll, which is defined here: https://material.angularjs.org/latest/demo/virtualRepeat
My first question is: does this virtualRepeat demand to be inside a div with a scrollbar, or can it be applied to the whole page? I actually don't want to have my content inside another div with additional scrollbar (besides the browser's one).
So, I'm using $http and my service returns 30 items if I provide a value of 0, and 60 if I provide value of 1, etc.
var _page = 0;
$scope.infiniteItems = {
numLoaded_: 0,
toLoad_: 0,
// Required.
getItemAtIndex: function(index) {
if (index > this.numLoaded_) {
this.fetchMoreItems_(index);
return null;
}
return index;
},
// Required.
// For infinite scroll behavior, we always return a slightly higher
// number than the previously loaded items.
getLength: function() {
return this.numLoaded_ + 5;
},
fetchMoreItems_: function(index) {
// For demo purposes, we simulate loading more items with a timed
// promise. In real code, this function would likely contain an
// $http request.
if (this.toLoad_ < index) {
this.toLoad_ += 30;
postFactory.getPastPosts(_page).then(angular.bind(this, function(data) {
this.numLoaded_ = this.toLoad_;
_page++;
}));
}
}
};
Here's my HTML
<md-content flex layout-padding>
<div class="virtualRepeatdemoInfiniteScroll">
<md-list>
<md-virtual-repeat-container id="vertical-container">
<div md-virtual-repeat="post in infiniteItems" md-on-demand="" class="repeated-item" flex="">
<md-divider></md-divider>
{{post}}
<past-post model="post" action="reaction(currentPost, candidate, type)"></past-post>
</div>
</md-virtual-repeat-container>
</span>
</md-list>
</div>
</md-content>
The problem is that nothing happens. I get no values. The problem is probably in the postFactory.getPastPosts(_page).then(angular.bind(this, function(data) { as well, since the data is actually in data.data, but there is nothing in the documentation that would show where and how to set the data.
UPDATE
The code for getPastPosts is pretty straightforward: a basic $http request:
function getPastPosts(page) {
return $http.get(baseUrl + '/api/content/past-posts/' + page)
}
I'm using this in various part of the application so there's no doubt that it's working.
You could got an object in getPastPosts() if data returns a value.
The infinite scroll would not be work because it is not an array.
infiniteItems must be type of array.
try like this,
if(data.size == 1){ // I don't know your data structure.
this.push(data.data);
}else{
this = data.data
}

App Fails First Load When Listing Array's Item from a DataSnapshot from Firebase

I am storing a DataSnapshot into a $scope array, so the ng-repeat into HTML div updates my "result list"
The problem is, when I run the code, input a value and click to run the DataSnapshot function, the first result doesn't appear on the app screen, only on the app's log on browser's console. If I run the function again, the result appears. If I change the input and click the button (run the function) again, this input appears in the first try.
So, here's what you will probably need:
Print from first attempt(data appears on console but not on app):
Print from second attempt(data appears twice on console and once on app):
Print from third attempt with another input(data appears once on console and once on app):
Codes:
index.html
<div class="list card">
<div class="item item-body" ng-repeat="locker in lockers">
<a href="#" class="item item-icon-right text-center">
<img ng-src="{{imageSrc}}" style="width: 35px; height: auto;">
<p style="text-align: center">
Locker - {{locker.number}}
<br>
{{key}}
</p>
<i class="icon ion-chevron-right"></i>
</a>
</div>
app.js
angular.module('starter', ['ionic', 'firebase'])
.controller('LockerCtrl', ["$scope", "$firebaseArray", function($scope,$firebaseArray, snapshot){
var lockersRef = new Firebase('https://ifrjarmariosdb.firebaseio.com/armarios');
$scope.getButtonClicked = function() {
var lockernumber = document.getElementById('lockerNumberInput').value;
if(lockernumber.length > 0){
lockerInputedNumber = parseInt(lockernumber);
lockersRef.on('value', function(snapshot){
snapshot.forEach(function(data){
var number = data.val().number;
if(number == lockerInputedNumber){
$scope.key = data.key();
$scope.lockers = [data.val()];
console.log(data.val());
if(number == 101){ -
$scope.imageSrc = "img/locker_test2.png";
}
else{
$scope.imageSrc = "img/locker_test.jpg";
}
}
});
});
}
As you could see by the prints, I'm also facing some trouble to change the image source according to the number value from Firebase. If you could help me to solve that, it would be a great help.
Please, I'm not asking for the solution for this method, if you know a different method to do this, I ask you to post it too.
Thanks!
This code starts loading data from Firebase:
lockersRef.on('value', function(snapshot){
snapshot.forEach(function(data){
var number = data.val().number;
if(number == lockerInputedNumber){
$scope.key = data.key();
$scope.lockers = [data.val()];
console.log(data.val());
if(number == 101){ -
$scope.imageSrc = "img/locker_test2.png";
}
else{
$scope.imageSrc = "img/locker_test.jpg";
}
}
});
The loading happens asynchronously. Since it takes time before the data is available, the browser continues executing the JavaScript after this code.
When the data comes back from the server, it executes your callback function. But at that point, AngularJS is no longer expecting any changes to the $scope.
The solution is to make AngularJS aware of the fact that you've changed the scope. The easiest way to do that, is to wrap the callback into a $timeout() call:
lockersRef.on('value', function(snapshot){
$timeout(function() {
snapshot.forEach(function(data){
var number = data.val().number;
if(number == lockerInputedNumber){
$scope.key = data.key();
$scope.lockers = [data.val()];
console.log(data.val());
if(number == 101){ -
$scope.imageSrc = "img/locker_test2.png";
}
else{
$scope.imageSrc = "img/locker_test.jpg";
}
}
});
});
Some people that ran into the same problem:
Angular JS firebase.(child_added) not rendering on page
ng-show and ng-change unwanted delay
A few other things I note about your code:
you're downloading all lockers and then filtering for the one the user entered client-side. This is wasting bandwidth that your user might be paying for. A better way would be to leave the filtering to Firebase with a Query:
var query = lockersRef.orderByChild('number').equalTo(lockerInputedNumber);
query.on('value', function(snapshot){
there is a binding library for AngularJS+Firebase called AngularFire, which handles the $timeout() thing automatically. It's built on top of Firebase's JavaScript SDK that you're now using, so they interoperate perfectly.

setTimeout with loop issue with field update

I'm trying to loop through a list on a timer (currently 1 second but I want it faster). The problem is that the current value isn't being updated visually & I can't see why.
When I loop through with Firebug it works as intended but without firebug it's not showing the text change...is it skipping the text update somehow?
I set the timer to 1 second; surely a .html() call wouldn't take longer? Thanks for any info.
My HTML is simply:
<div id="winner2"><h1></h1></div>
<div id="button"">
<img id="start" src="start.png" />
<img id="stop" src="stop.png" />
</div>
My JS is:
var people = ["a", "b", "c"];
var counter = 1;
var run;
$(document).ready(function(){
$("#winner2 h1").html("a");
$("#stop").hide();
$( "#start" ).click(function() {
$("#start").hide();
$("#stop").show();
run = setTimeout (nameIncrement(),1000);
});
$( "#stop" ).click(function() {
$("#stop").hide();
$("#start").show();
clearTimeout(run);
});
function nameIncrement() {
if(counter == people.length) {
counter=0;
}
$("#winner2 h1").html(people[counter]);
counter++;
run = setTimeout (nameIncrement(),1000);
}
});
You are calling nameIncrement() and passing its return value to setTimeout(), rather than passing nameIncrement.
Remove the parentheses:
run = setTimeout(nameIncrement, 1000);
jsfiddle
It looks like you are running a recursive function without defining exit condition. this probably causes overload to the browser which decided not to run the function.
Try:
function nameIncrement() {
if(counter == people.length) {
counter=0;
return;
}
$("#winner2 h1").html(people[counter]);
counter++;
run = setTimeout (nameIncrement(),1000);
}
});
on debug mode, however, the browser is less defensive, so you can see your erors yourself.

How do I change AngularJS ng-src when API returns null value?

In working with the API from themoviedb.com, I'm having the user type into an input field, sending the API request on every keyup. In testing this, sometimes the movie poster would be "null" instead of the intended poster_path. I prefer to default to a placeholder image to indicate that a poster was not found with the API request.
So because the entire poster_path url is not offered by the API, and since I'm using an AngularJS ng-repeat, I have to structure the image tag like so (using dummy data to save on space):
<img ng-src="{{'http://example.com/'+movie.poster_path}}" alt="">
But then the console gives me an error due to a bad request since a full image path is not returned. I tried using the OR prompt:
{{'http://example.com/'+movie.poster_path || 'http://example.com/missing.jpg'}}
But that doesn't work in this case. So now with the javascript. I can't seem to get the image source by using getElementsByTagName or getElementByClass, and using getElementById seems to only grab the first repeat and nothing else, which I figured would be the case. But even then I can't seem to replace the image source. Here is the code structure I attempted:
<input type="text" id="search">
<section ng-controller="movieSearch">
<article ng-repeat="movie in movies">
<img id="myImage" src="{{'http://example.com/'+movie.poster_path}}" alt="">
</article>
</section>
<script>
function movieSearch($scope, $http){
var string,
replaced,
imgSrc,
ext,
missing;
$(document).on('keyup', function(){
string = document.getElementById('search').value.toLowerCase();
replaced = string.replace(/\s+/g, '+');
$http.jsonp('http://example.com/query='+replaced+'&callback=JSON_CALLBACK').success(function(data) {
console.dir(data.results);
$scope.movies = data.results;
});
imgSrc = document.getElementById('myImage').src;
ext = imgSrc.split('.').pop();
missing='http://example.com/missing.jpg';
if(ext !== 'jpg'){
imgSrc = missing;
}
});
}
</script>
Any ideas with what I'm doing wrong, or if what I'm attempting can even be done at all?
The first problem I can see is that while you are setting the movies in a async callback, you are looking for the image source synchronously here:
$http.jsonp('http://domain.com/query='+replaced+'&callback=JSON_CALLBACK').success(function(data) {
console.dir(data.results);
$scope.movies = data.results;
});
// This code will be executed before `movies` is populated
imgSrc = document.getElementById('myImage').src;
ext = img.split('.').pop();
However, moving the code merely into the callback will not solve the issue:
// THIS WILL NOT FIX THE PROBLEM
$http.jsonp('http://domain.com/query='+replaced+'&callback=JSON_CALLBACK').success(function(data) {
console.dir(data.results);
$scope.movies = data.results;
// This will not solve the issue
imgSrc = document.getElementById('myImage').src;
ext = img.split('.').pop();
// ...
});
This is because the src fields will only be populated in the next digest loop.
In your case, you should prune the results as soon as you receive them from the JSONP callback:
function movieSearch($scope, $http, $timeout){
var string,
replaced,
imgSrc,
ext,
missing;
$(document).on('keyup', function(){
string = document.getElementById('search').value.toLowerCase();
replaced = string.replace(/\s+/g, '+');
$http.jsonp('http://domain.com/query='+replaced+'&callback=JSON_CALLBACK').success(function(data) {
console.dir(data.results);
$scope.movies = data.results;
$scope.movies.forEach(function (movie) {
var ext = movie.poster_path && movie.poster_path.split('.').pop();
// Assuming that the extension cannot be
// anything other than a jpg
if (ext !== 'jpg') {
movie.poster_path = 'missing.jpg';
}
});
});
});
}
Here, you modify only the model behind you view and do not do any post-hoc DOM analysis to figure out failures.
Sidenote: You could have used the ternary operator to solve the problem in the view, but this is not recommended:
<!-- NOT RECOMMENDED -->
{{movie.poster_path && ('http://domain.com/'+movie.poster_path) || 'http://domain.com/missing.jpg'}}
First, I defined a filter like this:
In CoffeeScript:
app.filter 'cond', () ->
(default_value, condition, value) ->
if condition then value else default_value
Or in JavaScript:
app.filter('cond', function() {
return function(default_value, condition, value) {
if (condition) {
return value;
} else {
return default_value;
}
};
});
Then, you can use it like this:
{{'http://domain.com/missing.jpg' |cond:movie.poster_path:('http://domain.com/'+movie.poster_path)}}

Resources