Meteor / Blaze: updating list properties with minimal redraw - javascript

I'm encountering serious performance issues related to Blaze redraw. I know that my data structure is at fault but I can't find a way to fix it. This is a bit of a long one: I'll do my best to lay it out clearly.
The goal / the problem
I want to create a schedule view where users can schedule tests. Users should be able to drag and drop test slots, but the resulting redraw is so slow that the thing is unworkable.
The data
I have a Sittings collection which stores details about testing times. Each sitting has a list of scheduleGroups (i.e. exam1 and exam2 happen on Tuesday, exam3 and exam4 happen on Wednesday) The structure looks more or less like this:
var sitting = {
"_id" : "sittingId",
"name" : "Amazing Sitting",
"locationIds" : [
"room1_id",
"room2_id"
],
"startDate" : ISODate("2015-07-01T04:00:00Z"),
"endDate" : ISODate("2015-07-02T04:00:00Z"),
"scheduleGroups" : [
{
"_id" : "dEb3mC7H8RukAgPG3",
"name" : "New group",
"booths" : [
{
"boothNumber" : 1,
"slots" : [
{
"examId" : "exam1",
"dayNumber" : 1,
"startTime" : {
"hour" : 8,
"minutes" : 0
},
"endTime" : {
"hour" : 9,
"minutes" : 30
},
"candidateId" : "odubbD42kHDcR8Egc",
"examinerIds" : [
"GQmvyXbcmMckeNKmB",
"GfoBy4BeQHFYNyowG"
]
}
]
}
]
"locationId" : "room1_id"
}],
"examIds" : [
"exam1",
"exam2",
"exam3"
]
};
The HTML
Sitting is defined as global context in Router.current().data hook. This is part of the redraw problem. Anyway, the following HTML produces the following layout (img below).
<template name='scheduler'>
<div class='container'>
<div class='tabpanel'>
<div class='tab-content' id='scheduler-content'>
{{#each getScheduleGroups}}
<div class='container-fluid schedule-wrapper'>
<div class='row'>
<div class='scheduler-inner-box placeholder-box'></div>
{{#each booths}}
<div class='group-column-head scheduler-inner-box'>{{boothNumber}}</div>
{{/each}}
</div>
<!-- Sittings can take place over several days, normally 2 days -->
{{#each sittingDays}}
<div class='day-wrapper'>
<h3 class='text-center'>Day {{this}}</h3>
<!-- This helper returns a list of start times: [June 1 9AM, June 1 915AM....] -->
{{#each getScheduleTimes ..}}
<div class='row time-row'>
<div class='group-row-head scheduler-inner-box'>
<span class='box-content'>{{this}}</span>
</div>
<!-- Loop through each booth, so that each slot has a square for that boot
i.e. June 1 9AM has space for booth 1, and booth 2, and booth 3, etc
-->
{{#each ../../booths}}
<!-- Paper starting now tells us if there is a slot scheduled to begin at this time or wher its empty-->
<div class='scheduler-inner-box {{#unless paperStartingNow ../.. ..}}open-booth{{/unless}}'>
{{#with paperStartingNow ../.. .. boothNumber}}
<!--
getSlotStyle calculates height and colour, depending on how long the paper is and whether
it's ready to go: i.e. does it have a candidate and the right number of examiners?
-->
<div class='paper-tile tile' {{getSlotStyle this ../boothNumber 'paper'}}>
<span class='slot-name'>{{getSlotName}}</span>
</div>
{{#if slotHasCandidate}}
<div class='candidate-tile tile' {{getSlotStyle this ../boothNumber 'candidate'}}>
<span class='slot-name candidate-name'>{{getCandidateDisplay}}</span>
</div>
{{/if}}
{{#each slotExaminers}}
<div class='examiner-tile tile' {{getSlotStyle .. ../../boothNumber 'examiner' index}}>
<span class='slot-name examiner-name'>{{profile.name}}</span>
</div>
{{/each}}
{{/with}}
</div>
{{/each}}
</div>
{{/each}}
</div>
{{/each}}
</div>
{{/each}}
</div>
</div>
</div>
The Issue
When a user drops a slot (purple tile) onto an empty space, or drags a candidate or examiner to another tile, I update the collection and let Meteor redraw. However the whole thing redraws and I can't find a way to isolate.
Here's the code for moving a whole slot, candidates examiners and all.
moveSlot: function (originalBoothNumber, originalSlot, dropBoothNumber, newSlot) {
check( originalBoothNumber, Number );
check( originalSlot, Object );
check( dropBoothNumber, Number );
check( newSlot, Object );
//pull the old slot
var originalBooth = _.findWhere( this.booths, {boothNumber: originalBoothNumber} );
originalBooth.slots = _.without( originalBooth.slots, originalSlot );
//insert the new one
var dropBooth = _.findWhere( this.booths, {boothNumber: dropBoothNumber} );
dropBooth.slots.push( newSlot );
var modifier = {
$set: {}
};
modifier["$set"]["scheduleGroups." + this.getIndex() + ".booths"] = this.booths;
return Sittings.update({_id: this.getSitting()._id }, modifier);
},
Do I need to change the way I store Sitting.booths? If anyone can recommend a better data structure that will trigger a redraw of just the slot, I'd really appreciate it. I've found a hack that puts every slot into a session variable but there's got to be another way.
Thanks for your patience and your help,
db

Problem
It's because you store the whole set of slots inside one document of booth. Once you update a slot, the whole document is consider updated.
Solutions
IMHO, I don't think you need to change the structure, unless you have build a feature to search for slot or booth. You can set it up by create a series of reactiveVariable or a reactiveDictionary, your template should depends only on that reactive variable. Every time you updated, consider to sync between two of them, either manually or automatically.
PS. I'm not doing any affiliate with this site: https://www.sportsplus.me. But if you sign up -> home -> mlb -> anycontest -> click on plus, you can see their masterpiece infinite scroll with partly redraw. (Open your dev-console and watch the changes in element). They do exactly the same thing you're trying to do with minimum redraw.

Related

How to add values to an array from variables in Vue.Js to make bar chart

I'm trying to add values, stored in variables, to be added to an array to display as a bar chart in Vue.js
I tried adding values by using series[0]=ServiceArea1;.
This is what I got so far:
barChart: {
data: {
series [0] : ServiceArea1Total,
series [1] : ServiceArea2Total,
series [2] : ServiceArea3Total,
series [3] : ServiceArea4Total,
series [4] : ServiceArea5Total,
labels: ['Western', 'Northern', 'Eastern', 'Southern', 'Central'],
series: [ ]
}
}
If I enter the values like so
series: [[542, 443, 320, 780, 553],]
the output comes however since I am calling the values from a database, I cannot enter the values statically.
The HTML part is below:
<div class="row">
<div class="col-md-6">
<chart-card :chart-data="barChart.data"
:chart-options="barChart.options"
:chart-responsive-options="barChart.responsiveOptions"
chart-type="Bar">
<template slot="header">
<h4 class="card-title">Statewide Service Area Kids</h4>
<p class="card-category">Click on a service area to view more information</p>
</template>
<template slot="footer">
<div class="legend">
<i class="fa fa-circle text-info"></i> Service Area
</div>
<hr>
<div class="stats">
<i class="fa fa-check"></i> Data information certified
</div>
</template>
</chart-card>
</div>
It depends on how the data is structured when it comes back from the database. At the most basic, you'd loop through an array or object from the database and call Array.push for each element.. Ex: series.push(x)
This may help:
How do I push items into an array in the data object in Vuejs? Vue seems not to be watching the .push() method
Assuming that you have a model like :
let barChart = {
services: [
ServiceArea1Total,
ServiceArea2Total,
ServiceArea3Total,
ServiceArea4Total,
ServiceArea5Total,
],
labels: ['Western', 'Northern', 'Eastern', 'Southern', 'Central'],
series: []
}
You are storing your services call in barChart.services, your labels in barChart.labels (which is not used here) and your total series in barChart.series.
For getting all your data from your services and storing it in barChart.series you have to do something like :
barChart.services.forEach( function( service ){
barChart.series.push( service() );
});
With that code you gonna call all your functions in services and push the data in the array series for each one.
May I have miss understood what you are trying to do ?

Increment value of scope counter by 1 gives 10 $digest() iterations reached. Aborting error message

I am trying to work with angularjs and given is the JSFiddle for what I am trying to do
<div id="page-main-body">
<div id="container" ng-repeat="hero in heroModel">
<div id="page-header" ng-if="heroCounter === 1">
I am header
</div>
<div id="page-body">
<p>Name: {{ hero.Name }}</p>
<p>Super hero identity: {{ hero.SuperHeroName }}</p>
</div>
<div id="page-footer" ng-if="heroCounter === 2">
I am footer
</div>
{{ incrementHeroCounter() }} {{ resetHeroCounter() }}
</div>
</div>
My issue is if my model contains 4 elements it works fine, but if i remove one element from the model I don't see the footer div and I get an error message in the console which says
10 $digest() iterations reached. Aborting! Watchers fired in the last 5 iterations:
So I think this is because I am updating the $scope variable and based on the variable value i am drawing my header and footer.
Everything works fine if my data model contains 4 or 6 elements but if i change the element count from an even count (2,4) to an odd count (1,3) I get the $digest error
I also read the angular documentation for this error but it seems like my model too remains the same and nothing changes, only variable which changes is the $scope variable of counter here and I know that may cause a change in the digest cycle but how do i cache it or use a different technique to achieve what i am trying to do here
I tried couple of previous stack links but was unable to get a good direction from it, I am not sure if for a simple thing like this i should use service or a $rootScope maybe there's a better way of doing this.
Given below is how my code looks like you may also look at the fiddle and let me know what i am doing wrong here
var app = angular.module("myApp", []);
app.controller("myCtrl", function($scope) {
$scope.heroCounter = 1;
$scope.incrementHeroCounter = function() {
$scope.heroCounter++;
};
$scope.resetHeroCounter = function() {
if ($scope.heroCounter === 3) {
$scope.heroCounter = 1;
}
};
$scope.heroModel = [
{
SuperHeroName: "Superman",
Name: "Clark kent"
},
{
SuperHeroName: "Batman",
Name: "Bruce Wyane"
},
{
SuperHeroName: "Iron man",
Name: "Tony Stark"
},
{
SuperHeroName: "Shazam",
Name: "Billy Batson"
}
];
});
i want to print the header and then 2 div elements with data and then the footer, the third and fourth item should be printed with their own header and footer.
Use the $index special property:
<div id="page-main-body">
<div id="container" ng-repeat="hero in heroModel">
<div id="page-header" ng-hide="$index === 1">
I am header
</div>
<div id="page-body">
<p>Name: {{ hero.Name }}</p>
<p>Super hero identity: {{ hero.SuperHeroName }}</p>
</div>
<div id="page-footer" ng-hide="$index === 0">
I am footer
</div>
̶{̶{̶ ̶i̶n̶c̶r̶e̶m̶e̶n̶t̶H̶e̶r̶o̶C̶o̶u̶n̶t̶e̶r̶(̶)̶ ̶}̶}̶ ̶{̶{̶ ̶r̶e̶s̶e̶t̶H̶e̶r̶o̶C̶o̶u̶n̶t̶e̶r̶(̶)̶ ̶}̶}̶
</div>
</div>
The $index special property is exposed on the local scope of each ng-repeat template instance. It is a number iterator offset of the repeated element (0..length-1).
For more information, see
AngularJS ng-repeat Directive API Reference - Overview
You can use the $index property instead of your own counter. This special property is exposed on the local scope of each ng-repeat template instance. It is a number iterator offset against the repeated element.

Included template getting loaded before model is available from controller - Angular

I have created a basic tool using vanilla javascript/html/css to perform certain functions across all emails associated with the business. To maintain the code base simplicity, i have started to porting the application using angularJS, creating modules/controllers etc. However, i got stuck with below situation. It looks like from the logs that my included html template gets called before data is ready from the controller.
I am placing an ajax call from the server which roughly takes ~1 sec. But by the time, I guess Angular executes the included template and because of no model availability, it renders nothing on the application side.
A look into my code:
index.html
<div class="tab-pane active fade in" id="results">
<section ng-controller = "messageSelectionController" ng-include src="'templates/messageSelectionTemplate.html'"></section>
</div>
messageSelectionController.js
app.controller('messageSelectionController',function($scope,$log){
//Generic ajax call
var makeRequest = function(paramObj,successCallback,errorCallBack,url,type){
$.ajax({
method : type ? type : "GET",
url : url ? url : "https://xx.xx.xx/xx/xx",
data : paramObj,
xhrFields : { withCredentials : true }
}).then(successCallback,errorCallBack);
};
var messageSelectionSuccessHandler = function(data){
$scope.messageselectionlist = JSON.parse(data).response.docs;
$log.info($scope.messageselectionlist);
};
var messageSelectionErrorHandler = function(data){
$log.info(data.responseText);
};
var data = {
q : "",
fq:[
'View:xx',
'Platform:xx'
],
start : 0,
rows : 9999
};
makeRequest(data, messageSelectionSuccessHandler,messageSelectionErrorHandler);
});
messageSelectionTemplate.html
<ul>
<li ng-repeat="item in messageselectionlist">
<article class="">
<span class="label label-success">MessageSelection</span>
<a> "Platform : {{item.Platform}} | View : {{item.View}} | Locale : {{item.Locales[0]}}"
<i class = "ocon-flag-{{item.Locales[0] | lowercase }}"></i>
</a>
<span class = "label label-inverse">{{item.Description}}</span>
</article>
</li>
</ul>
I tried to put logs to see when the messageSelectionTemplate is loaded, using below code:
$scope.$on('$includeContentLoaded', function(event, target){
console.log(event); //this $includeContentLoaded event object
console.log(target); //path to the included resource, 'snippet.html' in this case
});
and here's the logs:
-----templates/messageSelectionTemplate.html angular.js:13424
-----Array[654]
Can someone let me know how to defer the included template loading until the modal for template is not available. Thanks!

how to show array element one by one on page while array is property of an object which inside of services

i have this a testPaper object in Services :
this.testPapers=[{
courseName: 'Languages' , moduleName: 'Arabic',
paperName: 'Arabic-V',
paperDate: '15/08/2014',
// paperStartTime:
//papertEndTime:
// PaperDuration: '00:10:00',
marksForEachQuestion: 5,
totalMarks: 15,
Question: ['Mirwaha meaning in English?','Jwwaz meaning in English?','Hafila meaning in English?']
}
while the controller:
.controller('questionStart',function($scope, $routeParams,$rootScope,crudService){
// $scope.allCourses= crudService.courses;
// $scope.allModules=crudService.modules;
$scope.allTestPapers=crudService.testPapers;
}
and UI
<ol>
<li data-ng-repeat="p in allTestPapers | filter : mSelectedCourse
| filter: mSelectedModule
| filter : mSelectedPaper">
<hr>
<div class="row">
<div class="col-xs-12 col-sm-3">
Paper Name :<b>{{p.paperName}}</b>
</div>
<div class="col-xs-12 col-sm-3">
Total Marks:<b>{{p.totalMarks}}</b>
</div>
<div class="col-xs-12 col-sm-3">
Marks For Each Question: <b>{{p.marksForEachQuestion}}</b>
</div>
<div class="col-xs-12 col-sm-3">
Total Question: <b>{{p.Question.length}}</b>
</div>
</div>
<hr>
<h4>{{p.Question}}</h4>
<!--Which logic need to put in above line to not show all question at once instead one at a time
and when user click a button then next question will show??? how to do this-->
<button data-ng-click="">next</button>
</ol>
now from this i want to show the Question array on the page but one by one (mean from 0 to last item but one question show at page then on button click i want to change the question).
How can i do this;
So here is how you should go on about it, starting from the UI, which will have a heading for question, an input box for answer and a next button:
<h3 id='q'>Questions will appear here<h3>
Answer: <input type:'text' id='a'><br>
<button onclick="showQuestion()">Next</button>
Then your tesPapers, for this example i used two papers:
var testPapers = [{
courseName: 'Languages' , moduleName: 'Arabic',
paperName: 'Arabic-V',
paperDate: '15/08/2014',
// paperStartTime:
//papertEndTime:
// PaperDuration: '00:10:00',
marksForEachQuestion: 5,
totalMarks: 15,
Question: ['Mirwaha meaning in English?','Jwwaz meaning in English?','Hafila meaning in English?']
},{
courseName: 'Languages' , moduleName: 'Arabic',
paperName: 'Arabic-V',
paperDate: '15/08/2014',
// paperStartTime:
//papertEndTime:
// PaperDuration: '00:10:00',
marksForEachQuestion: 5,
totalMarks: 15,
Question: ['Mirwaha meaning in English?','Jwwaz meaning in English?','Hafila meaning in English?']
}];
Now all the question logic, its simple.. you need to have two counters, one for paperNumber other for Question number.. when current paper's questions end, you need to increment the paperNumber and when all papers end, simply display End of all papers.
var paperNumber = 0;
var questionNumber = 0;
function showQuestion(){
document.getElementById('q').innerHTML = testPapers[paperNumber].Question[questionNumber];
questionNumber++;
if(questionNumber > testPapers[paperNumber].Question.length){
document.getElementById('q').innerHTML = "End of this Paper";
questionNumber = 0;
paperNumber++;
if(paperNumber >= testPapers.length){
document.getElementById('q').innerHTML = "End of All Papers";
}
}
}
OfCourse you can change the UI however you want but the core logic should remain the same, specially the question and papers looping mechanism.
See the DEMO here
You need to inject the service into the controller for the required view. Then you should be able to use the ng-repeat directive:
<p ng-repeat="quest in serviceName.testPaper[0].Question">{{quest}}</p>
edit as per comments:
in controller:
var index = 0;
$scope.currentQuestion = serviceName.testPaper[0].Question[index];
$scope.nextQuestion = function() {
$scope.currentQuestion = serviceName.testPaper[0].Question[index+1];
};
in html
<p>{{currentQuestion}}</p>
<button ng-click="nextQuestion()">Next Question</button>

KnockoutJS showing a sorted list by item category

I just started learning knockout this week and everything has gone well except for this one issue.
I have a list of items that I sort multiple ways but one of the ways I want to sort needs to have a different display than the standard list. As an example lets say I have this code
var BetterListModel = function () {
var self = this;
food = [
{
"name":"Apple",
"quantity":"3",
"category":"Fruit",
"cost":"$1",
},{
"name":"Ice Cream",
"quantity":"1",
"category":"Dairy",
"cost":"$6",
},{
"name":"Pear",
"quantity":"2",
"category":"Fruit",
"cost":"$2",
},{
"name":"Beef",
"quantity":"1",
"category":"Meat",
"cost":"$3",
},{
"name":"Milk",
"quantity":"5",
"category":"Dairy",
"cost":"$4",
}];
self.allItems = ko.observableArray(food); // Initial items
// Initial sort
self.sortMe = ko.observable("name");
ko.utils.compareItems = function (l, r) {
if (self.sortMe() =="cost"){
return l.cost > r.cost ? 1 : -1
} else if (self.sortMe() =="category"){
return l.category > r.category ? 1 : -1
} else if (self.sortMe() =="quantity"){
return l.quantity > r.quantity ? 1 : -1
}else {
return l.name > r.name ? 1 : -1
}
};
};
ko.applyBindings(new BetterListModel());
and the HTML
<p>Your values:</p>
<ul class="deckContents" data-bind="foreach:allItems().sort(ko.utils.compareItems)">
<li><div style="width:100%"><div class="left" style="width:30px" data-bind="text:quantity"></div><div class="left fixedWidth" data-bind="text:name"></div> <div class="left fixedWidth" data-bind="text:cost"></div> <div class="left fixedWidth" data-bind="text:category"></div><div style="clear:both"></div></div></li>
</ul>
<select data-bind="value:sortMe">
<option selected="selected" value="name">Name</option>
<option value="cost">Cost</option>
<option value="category">Category</option>
<option value="quantity">Quantity</option>
</select>
</div>
So I can sort these just fine by any field I might sort them by name and it will display something like this
3 Apple $1 Fruit
1 Beef $3 Meat
1 Ice Cream $6 Dairy
5 Milk $4 Dairy
2 Pear $2 Fruit
Here is a fiddle of what I have so far http://jsfiddle.net/Darksbane/X7KvB/
This display is fine for all the sorts except the category sort. What I want is when I sort them by category to display it like this
Fruit
3 Apple $1 Fruit
2 Pear $2 Fruit
Meat
1 Beef $3 Meat
Dairy
1 Ice Cream $6 Dairy
5 Milk $4 Dairy
Does anyone have any idea how I might be able to display this so differently for that one sort?
Your view shouldn't contain logic beyond that necessary to render it. Thus, your foreach binding
data-bind="foreach:allItems().sort(ko.utils.compareItems)" should become a computed observable.
You should move the <option> data into your model and take advantage of the options data-bind.
To address the actual question, you'll take advantage of template binding and containerless if binding.
The template binding will allow you to change the look/feel of the view based on the selected sort type. So 2 templates are available, the default-template which handles the regular display and the category-template specifically for category based rendering.
<script type="text/html" id="category-template">
<ul class="deckContents" data-bind="foreach:sortedItems">
<li>
<!-- ko if: $root.outputCategory($index()) -->
<div data-bind="text:category"></div>
<!-- /ko -->
<span class="indented" data-bind="text:name"></span>
<span class="indented" data-bind="text:cost"></span>
<span class="indented" data-bind="text:category"></span>
</li>
</ul>
The html usage: <div data-bind='template: { name: currentTemplate, data: $data}'></div> where currentTemplate is an computed observable that returns the template id based on sort type.
In some way or another you must assign priority to the categories. I have done this by declaring var categoryPriority = ["Fruit", "Meat", "Dairy"].
Have a look at my fiddle. I didn't address the fixedWidth used by the default-template so you'll need to handle the CSS styling to line it up the way you want.
Edit: Is there a way to dynamically add an item to the list and have it show up in the sorted list automatically?
Pushing a new item: When you want knockout to "notify" other elements then you don't want to read the observableArray by using allItems() before performing the push. Instead, you'll push into the observableArray using allItems.push which in-turn will cause knockout to trigger computed observables (that depend on this observable) to evaluate, subscriptions to execute, DOM elements to update ... etc.
Computed Dependencies: In order for a computed to "depend" on another observable it has to be read inside of the provided evaluator function. Since, sortedItems only reads sortType that is the only "trigger" for re-evaluation. Thus, changing the allItems.sort to allItems().sort causes sortedItems to evaluate whenever changes are made to allItems.
See How Dependency Tracking Works
By using sort method we can sort the list by ascending order.
I am taking your example and explain this.
food = [{
"name":"Apple",
"quantity":"3",
"category":"Fruit",
"cost":"$1",
},{
"name":"Ice Cream",
"quantity":"1",
"category":"Dairy",
"cost":"$6",
},{
"name":"Pear",
"quantity":"2",
"category":"Fruit",
"cost":"$2",
},{
"name":"Beef",
"quantity":"1",
"category":"Meat",
"cost":"$3",
},{
"name":"Milk",
"quantity":"5",
"category":"Dairy",
"cost":"$4",
}];
self.allItems = ko.observableArray(food);
Here you have an array. By using below code to sort this list
self.allItems.sort(function (left, right) {
return left.name() == right.name() ? 0 : (left.name() < right.name() ? -1 : 1)
});
like wise if u want to sort by cost ..is the same..Replace name to cost..u will get it.

Categories

Resources