Update class on one progress bar where there are multiple progress bars - javascript

I'm having trouble updating the class on bootstrap progress bar.
I have multiple progress bars on a single page and I want to only update the class on one at the time if it goes over for example 20%. From "progress progress-success" to "progress progress-danger"
I have tried many variations, my code looks now like this where I'm trying to set unique id on
every bar by ng-repeat (AngularJS).
Javascript :
$scope.result = ProjectServices.projects().get(function(d) {
$scope.tasktime = []
console.log($scope.result.Tasks);
var tasks = $scope.result.Tasks;
for(var i = 0; i < tasks.length; i++) {
var prosent = ( tasks[i].EstimatedTimeLeft / tasks[i].EstimatedTime ) * 100;
console.log(prosent);
var extra = '#bar' + i;
if(prosent > 20 ) {
$(extra).removeClass("progress progress-success");
$(extra).addClass("progress progress-danger");
}
$scope.tasktime.push(prosent);
}
HTML : note $index is angularjs syntax to generate numbers from 0...?
<div id="bar{{$index}}" class="progress progress-success">
<div class="bar" style="width:{{tasktime[$index] | number:2}}%">{{tasktime[$index] | number:1}}% </div>
</div>
But it dosent seem to pick up the element, because I know it goes into the if statement.
My extra variable var extra = '#bar' + i; dosen't seem to be legit either any idead how to update single progress bar and generate id for the jQuery selector command?

I think in this example you would be better served to use the ng-class attribute and let angular make the class changes instead of forcing jquery into your controller which generally you wouldn't want to do:
http://docs.angularjs.org/api/ng.directive:ngClass
<div id="bar{{$index}}" class="progress " ng-class='{"progress-success":tasktime[$index].prosent<=20,"progress-danger":tasktime[$index].prosent>20}'>
<div class="bar" style="width:{{tasktime[$index] | number:2}}%">{{tasktime[$index] | number:1}}% </div>
</div>
A quick note I realized the prosent isn't defined, however you could just as easily define a function in your controller:
$scope.prosent = function(item) {
return ( item.EstimatedTimeLeft / item.EstimatedTime ) * 100
}
And the html would be: ng-class='{"progress-success":prosent(item) or prosent(tasktime[$index] depending on your repeater/etc.
Lastly, it seems like perhaps you have not shared all of your code.. I would set up an ng-repeat and do it like this:
<div ng-repeat='task in tasktime' class='progress' ng-class='{"progress-success":task.prosent<=20,"progress-danger: task.prosent>20'>
<div class="bar" style="width:{{task | number:2}}%">{{task | number:1}}% </div>
etc

Related

What's the best way to append an element using angular?

My objective is to show a grid of products and ads between them.
warehouse.query({limit: limit, skip: skip}).$promise
.then(function(data) {
for (var i = 0; i < data.length; i++) {
var auxDate = new Date(data[i].date);
data[i].date = auxDate.toISOString();
}
Array.prototype.push.apply($scope.products, data);
//add an img ad
var warehouseElem = angular.element(document.getElementsByClassName('warehouse')[0]);
var newAd = $sce.trustAsHtml('<img src="/ad/?r=' + Math.floor(Math.random()*1000) + '"/>');
warehouseElem.append(newAd);
skip += 9
});
Doesn't work.
I already tried simply using pure javascript like,
var warehouseElem = document.getElementsByClassName('warehouse')[0];
var newAd = document.createElement('img');
warehouseElem.appendChild(newAd);
Also doesn't work.
I suppose I need to do something with angular, can't find out what. I think it's sanitize but maybe I just don't know how to use it.
Remember I need to inject an img every once in a while between products.
This is a job for ng-repeat!
<div ng-repeat="data in datas">
<div>[show data here]</div>
<img src="/ad/?r=' + Math.floor(Math.random()*1000) + '"/>
</div>
If you have bind your "datas" in scope and Math too like this in your controller like this it should works
$scope.datas // this is your list of products
$scope.Math = Math;
If you don't want to spam add for each line you can use ng-if with $index like this :
<div ng-if="$index%2==0">
<img src="/ad/?r=' + Math.floor(Math.random()*1000) + '"/>
</div>
This will make it display add every 2 lines.
Since you seemed to come from a jQuery-like (or native DOM manipulation) background, I suggest you to read that post : "Thinking in AngularJS" if I have a jQuery background?.
This will explain you why in angular you almost don't manipulate DOM and quite some other things (only in directives).
EDIT : to fix the grid problem, just merging my two html block build your array of datas like this :
$scope.myArray = [product[0], ad[0] or just an empty string it will work still, product[1], ad[1]]
And the html
<div ng-repeat="data in datas">
<div ng-if="$index%2==0">[show data here]</div>
<img ng-if="$index%2==1 src="/ad/?r=' + Math.floor(Math.random()*1000) + '"/>
</div>
In AngularJS you should generally avoid doing DOM manipulation directly and rather rely on angular directives like ng-show/ng-hide and ng-if to dynamically hide sections of a template according to the specific case.
Now back to the problem at hand.
Assuming that you are trying to render a list of products loaded with the code displayed above and display an ad for some of them, you can try the following.
<!-- place the img element in your template instead of appending -->
<div ng-repeat="product in products">
<!-- complex product template-->
<!-- use ng-if to control which products should have an ad -->
<img ng-src="product.adUrl" ng-if="product.adUrl" />
</div>
Then in your controller set adUrl for products that should have an ad displayed.
warehouse.query({limit: limit, skip: skip}).$promise
.then(function(data) {
for (var i = 0; i < data.length; i++) {
var hasAd = // set to true if this product should have an add or not
var auxDate = new Date(data[i].date);
data[i].date = auxDate.toISOString();
if(hasAd){
data.adUrl = "/ad/?r=" + Math.floor(Math.random()*1000);
}
}
Array.prototype.push.apply($scope.products, data);
skip += 9
});
I am most probably assuming too much. If that is the case please provide more details for your specific case.
If you declare a scope variable,
$scope.newAd = $sce.trustAsHtml('<img src="/ad/?r=' + Math.floor(Math.random()*1000) + '"/>');
and in your HTML template, have a binding like
<div ng-bind-html="newAd"></div>,
it should work.

Conditionally bind data in AngularJS

I have an array of tasks. They have titles and and labels.
function Task(taskTitle, taskType) {
this.title = taskTitle;
this.type = taskType;
}
$scope.tasks = [];
I end up declaring a bunch of tasks with different types and adding them to the array
In my html, I show a column of cards, filtered by type of task:
<div ng-model="tasks">
<div class="card" ng-repeat="abc in tasks track by $index" ng-show="abc.type==0">
<p> {{ abc.title }} </p>
</div>
</div>
I want to bind the first card displayed in this filtered view to some other div. I'll be processing an inbox, so I'll whittle this list of cards down to zero. Each time I 'process' a card and remove it from the list, I need the data to refresh.
<div ng-model="firstCardInFilteredArray">
<h4>Title of first card:</h4>
<p> This should be the title of the first card! </p>
</div>
My intuition was to do something like this (in javascript):
// pseudo-code!
$scope.inboxTasks = [];
for (i=0; i<tasks.length(); i++) {
if (tasks[i].type == 0) {
inboxTasks.append(tasks[i]);
}
}
and somehow run that function again any time the page changes. But that seems ridiculous, and not within the spirit of Angular.
Is there a simple way in pure javascript or with Angular that I can accomplish this conditional binding?
You can filter your ng-repeat: https://docs.angularjs.org/api/ng/filter/filter
<div ng-model="tasks">
<div class="card" ng-repeat="abc in filteredData = (tasks | filter: {type==0}) track by $index">
<p> {{ abc.title }} </p>
</div>
</div>
Additionally, by saving the filtered data in a separate list you can display the next task like this:
<div>
<h4>Title of first card:</h4>
<p> filteredData[0].title </p>
</div>
Your data will automatically update as you "process" tasks.
The other answers helped point me in the right direction, but here's how I got it to work:
HTML
<input ng-model="inboxEditTitle" />
JS
$scope.filteredArray = [];
$scope.$watch('tasks',function(){
$scope.filteredArray = filterFilter($scope.tasks, {type:0});
$scope.inboxEditTitle = $scope.filteredArray[0].title;
},true); // the 'true' keyword is the kicker
Setting the third argument of $watch to true means that any changes to any data in my tasks array triggers the watch function. This is what's known as an equality watch, which is apparently more computationally intensive, but is what I need.
This SO question and answer has helpful commentary on a similar problem, and a great fiddle as well.
More on different $watch functionality in Angular
To update inboxTasks, you could use $watchCollection:
$scope.inboxTasks = [];
$scope.$watchCollection('tasks', function(newTasks, oldTasks)
{
for (i=0; i<newTasks.length(); i++)
{
if(newTasks[i].type == 0)
{
$scope.inboxTasks.append(tasks[i]);
}
}
});

Performance very slow, javascript, mustache rendering templates in a loop

I'm having terrible performance problems using .append and mustache, as you can see i'm looping over events and rendering a template that i am appending in to the DOM - can anyone suggest how make this a lot more efficient, currently the app grinds to a halt in Chrome..
Anyhelp will be be very much welcome, this is a major problem for me at the moment.
_.each(currentEvents.events, function(list, index) {
var template = "{{#.}}<div data-start-time='{{start_time}}' data-end-time='{{end_time}}' data-event-id='{{event_id}}' data-event-location-id='{{event_location_id}}' data-type-id='{{event_type_id}}' class='venue-event default-event event-type-{{event_type_id}}'>\
<div class='event-inner clearfix'>\
<div class='venue-event-type'>{{event_type}}</div>\
<div class='venue-event-time'>{{start_time}} - {{end_time}}</div>\
<div class='venue-event-title'>{{event_title}}</div>\
<div class='venue-event-sponsor'>{{sponsor}}</div>\
</div>\
</div>{{/.}}"
var eventOutput = Mustache.render(template, list);
$('[data-event-location-id=' + list.event_location_id + ']').append(eventOutput);
});
The var template should be init outside the loop (only once!):
var i, eventLength=currentEvents.events;
var template = "{{#.}}<div data-start-time='{{start_time}}' data-end-time='{{end_time}}' data-event-id='{{event_id}}' data-event-location-id='{{event_location_id}}' data-type-id='{{event_type_id}}' class='venue-event default-event event-type-{{event_type_id}}'>\
<div class='event-inner clearfix'>\
<div class='venue-event-type'>{{event_type}}</div>\
<div class='venue-event-time'>{{start_time}} - {{end_time}}</div>\
<div class='venue-event-title'>{{event_title}}</div>\
<div class='venue-event-sponsor'>{{sponsor}}</div>\
</div>\
</div>{{/.}}";
This mustache parse method helps speeds up future uses of render method (documentation):
Mustache.parse(template);
using _.each or forEach it more expensive than the native for loop (pay attention that I didn't use the currentEvents.events.length expression in the for condition - cause we prefer it to be calculated only once):
for (i = 0 ; i < eventLength ; i++ ) {
outPutArray.push(Mustache.render(template, list));
});
For this line to work you should change your flow design, from this jquery selector i can see your ids are already has been appended inside the DOM, and now you only go and append to each one o them the relevent HTML, this is wrong, you should have do the appending process to the ids which we can not see in your code and make it work togheder with the appending of the additional markup. like:
$('#parent-div').append(outPutArray.join(" "));
All code together:
var i, eventLength=currentEvents.events;
var template = "{{#.}}<div data-start-time='{{start_time}}' data-end-time='{{end_time}}' data-event-id='{{event_id}}' data-event-location-id='{{event_location_id}}' data-type-id='{{event_type_id}}' class='venue-event default-event event-type-{{event_type_id}}'>\
<div class='event-inner clearfix'>\
<div class='venue-event-type'>{{event_type}}</div>\
<div class='venue-event-time'>{{start_time}} - {{end_time}}</div>\
<div class='venue-event-title'>{{event_title}}</div>\
<div class='venue-event-sponsor'>{{sponsor}}</div>\
</div>\
</div>{{/.}}";
Mustache.parse(template);
for (i = 0 ; i < eventLength ; i++ ) {
outPutArray.push(Mustache.render(template, list));
});
$('#parent-div').append(outPutArray.join(" "));

changing the order that angular and the browser choose to do things

I am having two problems with the order that things are happening in angular and the browser. I am using laravel as the backend and the following is happening.
User clicks a link and laravel routes the user to /trainers/all. trainers/all includes html which includes html which includes trainersController and ratingsCtrl. trainerscontroller makes a get request to the server for the trainers json object. ng-repeat loops through this object and displays information for each trainer including an image and their rating.
ratingsCtrl then takes their rating and turns it into a number of stars using angular.ui rating functionality.
The problem is two fold, the browser is requesting the images before angular has put the url slug from the json object into the img object. The browser seems to try multiple times failing until angular has done its work.
The second is that angular tries to take the value of the rating before trainersController has put it in the dom element as well. this second error stops the ng repeat so only one trainer is displayed, if I remove the ratingctrl then the ngrepeat completes with no issues besides the img issue. If I hardcode the rating value it works also.
here is the error
TypeError: undefined is not a function
at Ia.% (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.7/angular.min.js:145:85)
at Ia.% (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.7/angular.min.js:145:85)
at t.constant (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.7/angular.min.js:157:136)
at Ia.% (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.7/angular.min.js:145:85)
at t.constant (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.7/angular.min.js:157:136)
at https://ajax.googleapis.com/ajax/libs/angularjs/1.2.7/angular.min.js:48:165
at q (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.7/angular.min.js:7:380)
at E (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.7/angular.min.js:47:289)
at https://ajax.googleapis.com/ajax/libs/angularjs/1.2.7/angular.min.js:56:32
at f (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.7/angular.min.js:42:399)
here is the relevant code
app.js
var trainercompareapp = angular.module("trainercompare", ['ui.bootstrap']);
trainercompareapp.config(function($interpolateProvider) {
$interpolateProvider.startSymbol('%%');
$interpolateProvider.endSymbol('%%');
});
function trainersController($scope, $http) {
$http.get('/trainers').success(function(trainers) {
$scope.trainers = trainers;
});
}
var RatingCtrl = function ($scope) {
$scope.rate = 7;
$scope.max = 10;
$scope.isReadonly = false;
$scope.hoveringOver = function(value) {
$scope.overStar = value;
$scope.percent = 100 * (value / $scope.max);
};
$scope.ratingStates = [
{stateOn: 'glyphicon-ok-sign', stateOff: 'glyphicon-ok-circle'},
{stateOn: 'glyphicon-star', stateOff: 'glyphicon-star-empty'},
{stateOn: 'glyphicon-heart', stateOff: 'glyphicon-ban-circle'},
{stateOn: 'glyphicon-heart'},
{stateOff: 'glyphicon-off'}
];
};
html
<div class="col-xs-12 container" ng-controller="trainersController">
<div ng-repeat="trainer in trainers">
<div class="col-xs-6">
<div class="row">
<img class="col-xs-6"src="%% trainer.user.images[0]['s3Url'] %%">
</div>
<div class="row">
<div class="col-xs-7" ng-controller="RatingCtrl">
<rating
class="rating"
value="%% trainer.rating %%"
max="max"
readonly="true"
on-hover="hoverOver(value)"
on-leave="overStar = null">
</rating>
</div>
<div class="col-xs-4">
View Profile
</div>
</div>
<hr>
</div>
</div>
</div>
Ugh, changing value="%% trainer.rating %% to just value="trainer.rating" made it work like a charm

Move Through Object List

<div id="team-name">{{teams[0].name}}</div>
<button id="next">Next</button>
When the "next" button is hit I would like the team-name to be the next team name in the list, i.e. 0 becomes 1?
I have a feeling I need JS to do this - but I am not sure how I would use JS to do this.
Also, the list is generated from the server.
UPDATE
{{ }} is part of a templating system - Jinja2 to be precise.
The teams list is passed into the webpage through Jinja2 - so the webpage has access to the entire teams list - I hope that makes sense.
class Team(db.Model):
__tablename__ = 'Team'
name = db.Column(db.String(21))
matches_total = db.Column(db.Integer())
matches_won = db.Column(db.Integer())
matches_lost = db.Column(db.Integer())
Make a list containing the names available as team_names and update your template like this:
<div id="team-name" data-index="0" data-entries="{{ team_names|tojson }}">{{teams[0].name}}</div>
<button id="next">Next</button>
In case you are using flask which seems to be the case, pass this to your render_template() call:
team_names=[t.name for t in Team.query]
Then you can use the following jQuery snippet to do what you want:
$('#next').on('click', function(e) {
e.preventDefault();
var nameElem = $('#team-name');
var entries = nameElem.data('entries');
var index = (nameElem.data('index') + 1) % entries.length;
nameElem.text(entries[index]).data('index', index);
})
Note: This answer assumes the list is not too big.

Categories