I have a poll application, in which a user can vote for an option in a given poll. in the html template, i use ng-show to show weather the user has voted for this option or this poll or if its an unvoted poll for the user:
<div data-ng-repeat="option in poll.poll_options" class="list-group-item">
<span data-ng-if="option.option_thumb == '2'" class="glyphicon glyphicon-thumbs-up"></span>
<span data-ng-if="option.option_thumb == '1'" class="glyphicon glyphicon-thumbs-down"></span>
<div data-ng-show="optionVoted(option,authentication.user._id)">
<span data-ng-bind="option.option_text"></span>
</div>
<div data-ng-hide="optionVoted(option,authentication.user._id)">
<div data-ng-show="pollVoted(poll._id,votes)">
<a data-ng-click="updateVote()">
<span data-ng-bind="option.option_text"></span> - update
</a>
</div>
<div data-ng-hide="pollVoted(poll._id,votes)">
<a data-ng-click="createVote(poll,option,authentication.user._id,$index)">
<span data-ng-bind="option.option_text"></span> - new
</a>
</div>
</div>
<span class="option-votes"> - {{option.votes.length}}</span>
</div>
these are the above mentioned functions to determine if the option / poll has been voted by the user:
// check if option is voted
$scope.optionVoted = function(option,userId){
for (var i = 0; i < option.votes.length; i++){
if (option.votes[i].user === userId){
return true;
}
}
};
//check if poll is voted
$scope.pollVoted = function(pollId,votes){
for (var i = 0; i < votes.length; i++){
if (votes[i].poll === pollId){
return true;
}
}
}
and here is the function to create a new vote:
// create a vote
$scope.createVote = function(poll,option,userId,index){
var vote = new Votes({
user:userId,
poll:poll._id,
poll_option:option._id
});
vote.poll_option_id = option._id;
vote.$save(function(vote){
option.votes.push(vote);
$scope.$apply();
}, function(errorResponse) {
$scope.error = errorResponse.data.message;
});
}
what happens on the front end, is that the option which has been now voted is updated (not showing an a tag anymore). what i need, is that the other options in the poll will update as well, and now instead of create() they will show update(), without refreshing the page.
how can I get the other html DOM elements of options in the poll to update?
In html, replace the functions in ng-show by an object property :
ng-show="option.voted", for example.
and update option.voted in createVote function.
(adapt this with userId etc.)
Make sure you are pushing the new vote onto the correct object in the scope. It looks like you are displaying data from $scope.poll.poll_options in your view, but you are adding to options.votes in your createVote function.
Related
In my application I have saved the data when we click on it(we can add the multiple data by entering some data and save the multiple data by clicking the save button).
.component.html
<ng-container *ngFor="let categoryDetail of selectedCategoryDetails">
<div class="__header">
<div>
<b>{{ categoryDetail.category }}</b>
</div>
</div>
<div
class="clinical-note__category__details"
*ngIf="categoryDetail.showDetails">
<ul>
<li class="habit-list"
*ngFor="let habits of categoryDetail.habitDetails" >
<div class="target-details">
<b>{{ clinicalNoteLabels.target }}: </b
><span class="habit-list__value">{{ habits.target }}</span>
</div>
</li>
</ul>
<div class="habit-footer">
<span class="m-l-10"
[popoverOnHover]="false"
type="button"
[popover]="customHabitPopovers"><i class="fa fa-trash-o" ></i> Delete</span>
</div>
<div class="clinical-note__popoverdelete">
<popover-content #customHabitPopovers [closeOnClickOutside]="true">
<h5>Do you want to delete this habit?</h5>
<button
class="btn-primary clinical-note__save" (click)="deletedata(habits);customHabitPopovers.hide()">yes </button>
</popover-content></div>
</div>
</ng-container>
In the above code when we click on delete button it will show some popup having buttons yes(implemented in above code) and now so my requirement is when we clcik on yes button in from the popover it has to delete the particular one.
.component.ts
public saveHealthyHabits() {
let isCategoryExist = false;
let categoryDetails = {
category: this.clinicalNoteForm.controls.category.value,
habitDetails: this.healthyHabits.value,
showDetails: true,
};
if (this.customHabitList.length) {
categoryDetails.habitDetails = categoryDetails.habitDetails.concat(
this.customHabitList
);
this.customHabitList = [];
}
if (this.selectedCategoryDetails) {
this.selectedCategoryDetails.forEach((selectedCategory) => {
if (selectedCategory.category === categoryDetails.category) {
isCategoryExist = true;
selectedCategory.habitDetails = selectedCategory.habitDetails.concat(
categoryDetails.habitDetails
);
}
});
}
if (!this.selectedCategoryDetails || !isCategoryExist) {
this.selectedCategoryDetails.push(categoryDetails);
}
this.clinicalNoteForm.patchValue({
category: null,
});
this.healthyHabits.clear();
public deletedata(habits){
if (this.selectedCategoryDetails) {
this.selectedCategoryDetails.forEach((selectedCategory) => {
if (selectedCategory.category ==categoryDetails.category) {
isCategoryExist = true;
this.selectedCategoryDetails.splice(habits, 1);
}
});
}
}
The above code I have written is for saving the data(we can enter multiple data and save multiple )
Like the above I have to delete the particular one when we click on yes button from the popover.
Can anyone help me on the same
If you're iterating in your html like:
<... *ngFor="let categoryDetails of selectedCategoryDetails" ...>
and your button with deletedata() is in the scope of ngFor, you can:
Change your iteration to include index of an item and trackBy function for updating the array in view:
<... *ngFor="let categoryDetails of selectedCategoryDetails; let i = index; trackBy: trackByFn" ...>
On the button click pass the index to deletedata() method like:
deletedata(index)
Create your deletedata method like:
deletedata(index:number){
this.selectedCategoryDetails.splice(index, 1);
// other code here, like calling api
// to update the selectedCategoryDetails source
// etc.
}
Create trackByFn method like:
trackByFn(index,item){
return index;
}
EDIT: Without index
If you want to iterate over selectedCategoryDetails in the ts file, without using ngFor with index in your html, you can have your deletedata like this:
deletedata(categoryDetails:any){
for (let i = this.selectedCategoryDetails.length - 1; i >= 0; i--) {
if (this.selectedCategoryDetails[i] === categoryDetails.category) {
this.selectedCategoryDetails.splice(i, 1);
}
}
}
It will iterate over selectedCategoryDetails backwards and remove the categoryDetails if it finds it in the array of objects.
Now, you only need to pass the categoryDetails to deletedata in your html:
(click)="deletedata(categoryDetails);customHabitPopovers.hide()"
It is not like it is slow on rendering many entries. The problem is that whenever the $scope.data got updated, it adds the new item first at the end of the element, then reduce it as it match the new $scope.data.
For example:
<div class="list" ng-repeat="entry in data">
<h3>{{entry.title}}</h3>
</div>
This script is updating the $scope.data:
$scope.load = function() {
$scope.data = getDataFromDB();
}
Lets say I have 5 entries inside $scope.data. The entries are:
[
{
id: 1,
title: 1
},
{
id: 2,
title: 2
},
......
]
When the $scope.data already has those entries then got reloaded ($scope.data = getDataFromDB(); being called), the DOM element for about 0.1s - 0.2s has 10 elements (duplicate elements), then after 0.1s - 0.2s it is reduced to 5.
So the problem is that there is delay about 0.1s - 0.2s when updating the ng-repeat DOM. This looks really bad when I implement live search. Whenever it updates from the database, the ng-repeat DOM element got added up every time for a brief millisecond.
How can I make the rendering instant?
EDITED
I will paste all my code here:
The controller:
$scope.search = function (table) {
$scope.currentPage = 1;
$scope.endOfPage = false;
$scope.viewModels = [];
$scope.loadViewModels($scope.orderBy, table);
}
$scope.loadViewModels = function (orderBy, table, cb) {
if (!$scope.endOfPage) {
let searchKey = $scope.page.searchString;
let skip = ($scope.currentPage - 1) * $scope.itemsPerPage;
let searchClause = '';
if (searchKey && searchKey.length > 0) {
let searchArr = [];
$($scope.vmKeys).each((i, key) => {
searchArr.push(key + ` LIKE '%` + searchKey + `%'`);
});
searchClause = `WHERE ` + searchArr.join(' OR ');
}
let sc = `SELECT * FROM ` + table + ` ` + searchClause + ` ` + orderBy +
` LIMIT ` + skip + `, ` + $scope.itemsPerPage;
sqlite.query(sc, rows => {
$scope.$apply(function () {
var data = [];
let loadedCount = 0;
if (rows != null) {
$scope.currentPage += 1;
loadedCount = rows.length;
if (rows.length < $scope.itemsPerPage)
$scope.endOfPage = true
for (var i = 0; i < rows.length; i++) {
let item = rows.item(i);
let returnObject = {};
$($scope.vmKeys).each((i, key) => {
returnObject[key] = item[key];
});
data.push(returnObject);
}
$scope.viewModels = $scope.viewModels.concat(data);
}
else
$scope.endOfPage = true;
if (cb)
cb(loadedCount);
})
});
}
}
The view:
<div id="pageContent" class="root-page" ng-controller="noteController" ng-cloak>
<div class="row note-list" ng-if="showList">
<h3>Notes</h3>
<input ng-model="page.searchString" id="search"
ng-keyup="search('notes')" type="text" class="form-control"
placeholder="Search Notes" style="margin-bottom:10px">
<div class="col-12 note-list-item"
ng-repeat="data in viewModels track by data.id"
ng-click="edit(data.id)"
ontouchstart="touchStart()" ontouchend="touchEnd()"
ontouchmove="touchMove()">
<p ng-class="deleteMode ? 'note-list-title w-80' : 'note-list-title'"
ng-bind-html="data.title"></p>
<p ng-class="deleteMode ? 'note-list-date w-80' : 'note-list-date'">{{data.dateCreated | displayDate}}</p>
<div ng-if="deleteMode" class="note-list-delete ease-in" ng-click="delete($event, data.id)">
<span class="btn fa fa-trash"></span>
</div>
</div>
<div ng-if="!deleteMode" ng-click="new()" class="add-btn btn btn-primary ease-in">
<span class="fa fa-plus"></span>
</div>
</div>
<div ng-if="!showList" class="ease-in">
<div>
<div ng-click="back()" class="btn btn-primary"><span class="fa fa-arrow-left"></span></div>
<div ng-disabled="!isDataChanged" ng-click="save()" class="btn btn-primary" style="float:right">
<span class="fa fa-check"></span>
</div>
</div>
<div contenteditable="true" class="note-title"
ng-bind-html="selected.title" id="title">
</div>
<div contenteditable="true" class="note-container" ng-bind-html="selected.note" id="note"></div>
</div>
</div>
<script src="../js/pages/note.js"></script>
Calling it from:
$scope.loadViewModels($scope.orderBy, 'notes');
The sqlite query:
query: function (query, cb) {
db.transaction(function (tx) {
tx.executeSql(query, [], function (tx, res) {
return cb(res.rows, null);
});
}, function (error) {
return cb(null, error.message);
}, function () {
//console.log('query ok');
});
},
It is apache cordova framework, so it uses webview in Android emulator.
My Code Structure
<html ng-app="app" ng-controller="pageController">
<head>....</head>
<body>
....
<div id="pageContent" class="root-page" ng-controller="noteController" ng-cloak>
....
</div>
</body>
</html>
So there is controller inside controller. The parent is pageController and the child is noteController. Is a structure like this slowing the ng-repeat directives?
Btw using track by is not helping. There is still delay when rendering it. Also I can modify the entries as well, so when an entry was updated, it should be updated in the list as well.
NOTE
After thorough investigation there is something weird. Usually ng-repeat item has hash key in it. In my case ng-repeat items do not have it. Is it the cause of the problem?
One approach to improve performance is to use the track by clause in the ng-repeat expression:
<div class="list" ng-repeat="entry in data track by entry.id">
<h3>{{entry.title}}</h3>
</div>
From the Docs:
Best Practice: If you are working with objects that have a unique identifier property, you should track by this identifier instead of the object instance, e.g. item in items track by item.id. Should you reload your data later, ngRepeat will not have to rebuild the DOM elements for items it has already rendered, even if the JavaScript objects in the collection have been substituted for new ones. For large collections, this significantly improves rendering performance.
For more information, see
AngularJS ngRepeat API Reference -- Tracking and Duplicates
In your html, try this:
<div class="list" ng-repeat="entry in data">
<h3 ng-bind="entry.title"></h3>
</div>
After thorough research, I found my problem. Every time I reset / reload my $scope.viewModels I always assign it to null / empty array first. This what causes the render delay.
Example:
$scope.search = function (table) {
$scope.currentPage = 1;
$scope.endOfPage = false;
$scope.viewModels = []; <------ THIS
$scope.loadViewModels($scope.orderBy, table);
}
So instead of assigning it to null / empty array, I just replace it with the new loaded data, and the flickering is gone.
Im trying to achieve this piece of code but in my console it says thing is null which is weird because when I look in the console, sessionStorage isn't empty...
$(".btn-alert").click(function(){
var identifierOfSpan = $(this > "span").text();
for(var prop in sessionStorage){
var thing = JSON.parse(sessionStorage.getItem(prop))
if(thing.id == identifierOfSpan){
sessionStorage.removeItem(prop);
}
}
$(this).closest(".voyages").remove();
if(sessionStorage.length == 0){
alert("Message!");
location.href="reservation.html"
}
});
the button is supposed to delete the div and the sessionStorage item which looks like this
Html :
<div class="voyages">
<button class="btn btn-alert btn-md mr-2" tabindex="-1">delete the flight</button>
<span>ID : 4224762</span>
<div class="infos">
<img src="img/angleterre.jpg" alt="maroc">
<div>
<ul>
<li><h5>Angleterre, Londres (LON)</h5></li>
<li><h5>2 adulte(s)</h5></li>
<li><h5> Aucun enfants </h5></li>
<li><h5>Type : Couple</h5></li>
</ul>
</div>
</div>
<hr>
<h3>Options</h3>
<ul>
<li>voiture : 0</li>
<li>Hotel : 0 </li>
</ul>
<hr>
<h3>Prix :3713$</h3>
If I'm reading your question correctly, you want to...
Click on a button
Find the first sibling <span> element and parse a number out of its text content
Remove all sessionStorage items (JSON serialized objects) with matching id properties
For the ID, I highly recommend adding some data directly to the <button> to help you identify the right record. If you can, try something like
<button class="btn btn-alert btn-md mr-2" data-voyage="4224762"...
Try something like this
$('.btn-alert').on('click', function() {
const btn = $(this)
const id = btn.data('voyage')
// or, if you cannot add the "data-voyage" attribute
const id = btn.next('span').text().match(/\d+$/)[0]
// for index-based removal, start at the end and work backwards
for (let i = sessionStorage.length -1; i >= 0; i--) {
let key = sessionStorage.key(i)
let thing = JSON.parse(sessionStorage.getItem(key))
if (thing.id == id) {
sessionStorage.removeItem(key)
}
}
// and the rest of your code
btn.closest(".voyages").remove();
if(sessionStorage.length === 0) {
alert("Message!");
location.href = 'reservation.html'
}
})
The problem with using a for..in loop on sessionStorage is that you not only get any item keys added but also
length
key
getItem
setItem
removeItem
clear
I am NEW to MVC as well as AngularJs and have been toiling over this for days. Although I feel that I am getting closer....still no cigar.
The problem: I have a list of reports that are grouped on the report type(name). I am trying to use an accordion to show and hide the list of reports in each group.
My controller.js looks like this (I know that it is wrong):
window.app.controller('relatedReportsController', ['$scope', '$timeout', 'relatedReportsService',
function ($scope, $timeout, relatedReportService) {
initialize();
function initialize()
{
$scope.relatedReports = [];
$scope.rollupVisible = false;
}
function sortOn(collection, name)
{
collection.sort(
function (a, b) {
if (a[name] <= b[name]) {
return (-1);
}
return (1);
});
}
$scope.groupBy = function (attribute) {
$scope.Groups = [];
sortOn($scope.relatedReports, attribute);
for (var i=0; i< $scope.relatedReports.length; i++)
{
var report = $scope.relatedReports[i];
}
}
$scope.toggleRollup = function($event)
{
if (angular.element($event.targe).hasClass('glyph')) return;
relatedReportService.$promise.then(function (data) {
$scope.relatedReports = data;
})
}
}]);
My page looks like this:
<li class="fruitRollup header row" ng-controller="relatedReportsController">
<div class="suitcaseheader">
<span class="col-xs-10 zero firstlabel">{{group.Name}}</span>
<span class="col-xs-3 zero datepad">Date</span>
<span class="floatR2">View</span>
<span class="clear"></span>
</div>
<div class="eaten">
<ul class="data">
#*#foreach (var reportResult in resultGroup.OrderByDescending(r=>r.Date))
{*#
<li class="data row" ng-repeat="report in group.reports" ng-controller="relatedReportsController">
<div class="suitcase">
<span class="col-xs-10 zero accountNumberColumn"></span>
#*<span class="middle zero">#reportResult.Date.Replace("12:00:00","")</span>*#
<span class="middle zero">{{report.Date}}</span>
<span class="floatR2">
<a class="icon-view glyph" target="_blank" href="#Url.ActionEncodedParameters("ViewDocument", "DocumentSearch", new { id = reportResult.Id })"></a>
</span>
<span class="clear"></span>
</div>
As you can see I need a lot of help. Thanks in advance!
The code that I had written in the controller.js was not properly getting the data. In trying to use a combination of samples that I found on the internet, I was thoroughly confused. I figured this out yesterday. I didn't need to get the data through the controller.js because the data was already being fetched through my page controller.cs and viewmodel. All I ended up needing was to use ng-show to show and hide the sections.
Thanks for taking the time to try to help.
I am trying to create a computed observable and display it on the page, and I have done it this way before but I am starting to wonder if knockout has changed - Everything works except the binding on totalAmount - for some reason it never changes...Any ideas?
My model is as follows:
var cartItem = function(item){
this.itemName = item.title;
this.itemPrice = item.price;
this.itemID = item.id;
this.count=0;
}
var cartModel = {
self:this,
footers:ko.observableArray([{title:'item1',text:'this is item1 text',image:'images/products/items/item1.png',price:15.99,id:1},
{title:'item2',text:'this is item2 text',image:'images/products/items/item2.png',price:25.99,id:2},
{title:'item3',text:'this is item3 text',image:'images/products/items/item3.png',price:55.99,id:3},
{title:'item4',text:'this is item4 text',image:'images/products/items/item4.png',price:5.99,id:4},
]),
cart:ko.observableArray([]),
addToCart:function(){
if(cartModel.cart().length>0){
for(var i=0;i<cartModel.cart().length;i++){
if(this.id==cartModel.cart()[i].itemID){
cartModel.cart()[i].count += 1;
}
else{
cartModel.cart().push(new cartItem(this));
}
}
}else{
cartModel.cart().push(new cartItem(this));
}
console.log(cartModel.cart().length);
}
}
this.cartModel.totalAmount=ko.computed(function(){
return this.cart().length;
},this.cartModel);
ko.applyBindings(cartModel);
And here is the associated HTML:
<div data-bind="template:{name:'footerTemplate',foreach:cartModel.footers}">
<script type="text/html" id="footerTemplate">
<div class="row">
<span class="span2"><h3 data-bind="text: title"></h3></span>
<span class="span2"><img data-bind="attr:{src: image}"/></span>
<span class="span5" data-bind="text:text"></span>
<span class="span1" data-bind="text:price"></span>
<spand class="span2"><button data-bind="click:$parent.addToCart">add</button></span>
</div>
</script>
</div>
<div class="row">
<span class="span2" data-bind="text:totalAmount"></span>
</div>
You are calling the push method on the internal array, not on the observableArray wrapper, thus the changes are never notified.
i.e. instead of:
cartModel.cart().push(new cartItem(this));
use simply:
cartModel.cart.push(new cartItem(this));
For more info take a look at the official documentation for observableArray, and in particular at the Reading information from an observableArray and Manipulating an observableArray sections.