I'm anxious to see an example of pagination in a flux environment, I can't wrap my mind around how that would work. I've taken a look at redux's example, but that's not really pagination, just a "load more" button. What I'm looking for is a way to paginate possibly millions of records (so you must use lazy loading).
Here are a few of the pitfalls I'm running into:
1) Someone could load page 20 without loading pages 1-19 (by clicking on a hyperlink, for example).
2) If someone edited a record inline, and then that record no longer satisfied the filter used to include in that list, we'll need to load more data to fill in the empty space left behind.
3) Monitoring props for changes to the page number, you'll need to load more data if that page hasn't been loaded yet.
I would love some examples that note how to overcome these pitfalls. Let me know if you have any suggestions. Thanks!
I added an example based on your title and the last sentence. To address your specific questions:
Of course you can load from page 20, just start from higher request params
Editing the record wouldn't change the state until committed right? So once you committed the edit change the filter would apply and your record would be removed if you edited the criteria that had been set by your filter
You would have your result set fetched further ahead as you paginated - if you clicked next it would load the next one in sequence forward, or if you clicked result start at 40 it would fetch 40-X where X is the count per fetch that you specify. The code example I found uses 10, like most applications, so you would fetch starting at 40, but would get 40 to 50.
This page. Basically Use an Event List Store to hold the data for the child objects to access, the pagination component peice itself and finally, there's the Pagination store that
"updates the current State of the active page and provide a function to calculate the number of pages available to the Pagination based on the Total amount of items and how many of those items are to be displayed per page"
I believe to implement this code you would need an api request with the query parameters such as search keywords and result set preferences. This code was designed to send an api call which returns a json response that could be broken down and presented accordingly (this example does in sets of 10.)
For another working example and there are probably many others, but off hand here is one that I know of personally. This code provided below was Posted from Adam Ellsworth and credits go to him for the code:
EventListStore.js
// requires go here ...
var events = [], // Default Event listing
total = 0, // Default number of Available Events
start = 0, // Default start
end = 9, // Default end
amt = 9; // Number of Events to list per page (0-based)
processTurnPage: function (page) {
start = (page - 1) * amt;
end = start + amt;
}
var EventListStore = assign({}, EventEmitter.prototype, {
...
getTotal: function () {
return total;
},
getStart: function () {
return start;
},
getEnd: function () {
return end;
},
getAmountPerPage: function () {
return amt;
},
...
// emitChange, addChangeListener, removeChangeListener
});
EventListStore.dispatchToken = EventListDispatcher.register(function (payload) {
var action = payload.action,
data = payload.action.data;
switch (action.actionType) {
case PageConstants.TURN_PAGE:
processTurnPage(data);
// Omitted:
// Call the API to get new event data based on our new Page params
EventListStore.emitChange();
break;
}
});
Pagination.Jsx
// requires go here ...
var LIMIT = 12; // The amount of clickable <li> to show
function getNavigation (count, per) {
/**
* This is where we build our <li /> elements. I'm omitting our code because
* there are too many ways in which pagination can be displayed, and ours
* is specific to our needs.
*
* What is returned below is just the gist of it.
*/
var pages = []; // what we'll store our JSX <li /> elements in.
var pages = PaginationStore.getTotalPageCount(count, per);
/**
* Translate our 0-based pagination data to a user-friendly representation
* by starting at 1
*/
for (var i = 1; i <= pages; i++) {
pages.push(
<li className="page" data-value={i} key={i}>
{i}
</li>
);
}
return pages;
}
var Pagination = React.createClass({
componentDidUpdate: function (prevProps, prevState) {
var self = this;
$('.page').unbind().on('click', function (e) {
var value = $(this).data('value');
if (value != self.state.page) {
EventViewActions.turnPage(value);
}
});
},
/**
* Note here that in our EventList.jsx Component we're instantiating our
* <Pagination /> component thusly:
*
* <Pagination total={this.state.total} per={this.state.amount} />
*/
render: function () {
var navigation = getNavigation(this.props.total, this.props.per);
return (
<ul>
{navigation}
</ul>
);
}
});
Pagination.jsx
// requires ...
var _page = 1; // Default page
updatePage: function (page) {
console.log('changing page: ' + _page + ' -> ' + page);
_page = page;
}
var PaginationStore = assign({}, EventEmitter.prototype, {
getPage: function () {
return _page;
},
getTotalPageCount: function (total, per) {
var pages = total - 1;
if (pages > 0) {
if (per < pages) {
return Math.ceil(pages / per);
}
return 1; // only one page of items
} else {
return 0; // no items
}
},
...
});
PaginationStore.dispatchToken = EventListDispatcher.register(function (payload) {
var action = payload.action,
data = payload.action.data;
switch (action.actionType) {
case PageConstants.TURN_PAGE:
updatePage(data)
PaginationStore.emitChange();
break;
}
});
Related
I am trying to get the code below to capture the <span></span> content. The JQuery code seen below within a function in a custom JS file deployed on the product details page doesn't get the control as the product price changes when a variant of the configurable product is selected from the drop-down. Could you please point out what is not right in here?
define([
'jquery',
'domReady!'
], function($) {
var docloch=document.location.href,new_title="";
// Commented out as the Tab is not opening by default.
// $("#tab-label-product.info.description").click();
setTimeout(function() {
$("#pmm_shipping").html('<br><div style="margin:-40px 0 15px">'+get_pmm_shipping_text()+'</div>');
}, 750); // end of setTimeout
// make the word CLOSEOUT red on product pages
if ($("h1").text().indexOf("CLOSEOUT!")>-1 || $("h1").text().indexOf("OPEN BOX!")>-1){
var newT= $("h1").html();
newT = newT.replace("CLOSEOUT!",'<span style="color:#FF0000">CLOSEOUT!</span>');
newT = newT.replace("OPEN BOX!",'<span style="color:#FF0000">OPEN BOX!</span>');
$("h1").html(newT);
}
// THIS IS WHERE I AM TRYING TO CAPTURE THE CHANGED SPAN CONTENT
$(".price-box").on("change", ".normal-price .price-wrapper span.price", function() {
alert('Price Changed');
console.log('New Price Captured ');
// Get the value of the span element
var price = $(".price-box .normal-price .price-wrapper span.price").html();**
// Clean and Convert the value to cents
var priceCents = parseInt(parseFloat(price.replace(/[^\d.]/g,'')) * 100);
// If value is different from existing Klarna product placement value, update it.
// and then call Klarna with refresh-event to refresh the placement.
var oldPurchaseAmt = $(".product-add-form klarna-placement").attr("data-purchase-amount");
if (priceCents !== oldPurchaseAmt) {
$(".product-add-form klarna-placement").attr("data-purchase-amount", priceCents);
// Trigger event to refresh
window.KlarnaOnsiteService = window.KlarnaOnsiteService || [];
window.KlarnaOnsiteService.push({ eventName: 'refresh-placements' });
}
});
}); // end of $ function
Try text instead of html
var price = $(".price-box .normal-price .price-wrapper span.price").text();
If you want to detect changes in the DOM you need a MutationObserver. This is an expensive operation, and usually indicates code could be refactored, but it will suit your purposes as you've worded the question.
I've reduced your problem to the bare essentials. We have a <span> that contains a price, and we create a MutationObserver that watches for changes, massages the data (removes the "$" character and doubles the value, but this is just to illustrate that you can massage data). The modified value is then written to a second <span>.
I'm using native javascript to query the DOM, but you can use JQuery.
const targetNode = document.querySelector('#watchme');
const answerNode = document.querySelector('#answer');
// change price periodically
const changePrice = () => {
const newPrice = Math.floor( Math.random() * 16384 ) / 100;
targetNode.innerText = '$' + newPrice;
};
window.setInterval(changePrice, 2048);
// set up Mutation Observer
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
const priceAsString = mutation.target.innerText;
const doublePrice = Number( priceAsString.replace(/^\$/g,'') ) * 2;
answerNode.innerText = doublePrice;
});
});
observer.observe(targetNode, { childList: true });
<p>Original Price is: <span id="watchme"></span></p>
<p>Double Price is: <span id="answer"></span></p>
I am creating a frontend to a patient booking system with Vue.js, which is simply a dynamic web form. The user first selects a type of treatment, then the practitioner they want to see, and finally the appointment time. All data is obtained via RESTful API calls using axios.
The available options in each form field are filtered using the previous choice. For example, the user will only be presented with the available times of their selected practitioner, and the practitioner can only be selected from the group of practitioners who can perform the chosen treatment.
Filtering the practitioners based on the selected treatment works just fine.
However, filtering the appointments based on the selected practitioner does not work -- it's out of sync: the appointments are loaded for the previously selected practitioner. I have checked the backend, which is fine, and the API calls are in-sync (i.e. the person_id matches the id of the newly selected practitioner).
What is causing this problem and how do I fix it?
Here is the Vue.js code that performs this filtering:
var app = new Vue({
el: '#app',
data: {
appointments: [],
practitionerId: 0,
practitioners: [],
treatmentId: 0,
treatments: [],
},
mounted: function () {
axios.get('/api/treatments')
.then(response => this.treatments = response.data);
},
watch: {
// filter available practitioners by the selected treatment
treatmentId: function () {
// get the allowed role ids for the selected treatment
var allowedRoleIds = '';
const allowedRoles = this.treatments[this.treatmentId - 1]['allowed_roles'];
for (var i = 0; i < allowedRoles.length; i++) {
allowedRoleIds += allowedRoles[i]['id'];
if (i + 1 < allowedRoles.length) {
allowedRoleIds += ',';
}
}
// load the practitioners using the allowed role ids
axios.get('/api/people?role_ids=' + allowedRoleIds)
.then(response => this.practitioners = response.data);
},
// filter the available appointments by the selected practitioner
practitionerId: function () {
axios.get('/api/appointments?person_id=' + this.practitionerId)
// ERROR!!! This is out of sync.
.then(response => this.appointments = response.data);
}
}
});
The problem can be resolved by adding a watcher to the appointments variable.
All I needed to do was add the following code within watch: { ... }:
appointments: function () {
// now it works -- even without any function body
}
This seems really odd to me. I should not need to create a watcher for a variable in order to have that variable updated in the function body of another watcher.
I have either missed something in the Vue.js documentation about watchers or this is a bug. If someone can shed some light on this in the comments that would be great!
You need to refresh practitionerId after fetching people from RESTful API.
For example, in treatmentId watch:
axios.get('/api/people?role_ids=' + allowedRoleIds).then(response => {
this.practitioners = response.data;
// refresh practitionerId whenever fetch new people
const selectedPractitionerId = this.practitionerId;
this.practitionerId = 0;
// if selected practitioner exists in new people
practitioners.forEach(p => {
if (p.id == selectedPractitionerId) {
this.practitionerId = p.id;
}
}) // you can omit this search if you force the user to select new practitioner whenever they change treatment
});
Here's a running example of what I've got so far:
https://codesandbox.io/s/github/BruceL33t/mobx-action-synchronous-execution-order/tree/master/
store.js:
import { observable, action } from "mobx";
import Sensor from "../models/Sensor";
export default class RootStore {
#observable sensors = new Map();
constructor() {
let self = this;
const sensorIds = [
"sensor1",
"sensor2",
"sensor3",
"sensor4",
"sensor5",
"sensor6",
"sensor7",
"sensor8",
"sensor9",
"sensor10"
];
for (let sensor of sensorIds) {
self.sensors.set(sensor, new Sensor(5));
}
// setInterval simulates some incoming data (originally from SignalR, and roughly each second)
setInterval(function() {
let out = {};
const x = +new Date(); // unix timestamp
for (let sensor of sensorIds) {
const y = Math.floor(Math.random() * 10000) + 1;
const m = { x: x, y: y };
out[sensor] = m;
}
self.addMeasurement(out); // the problem starts here.
}, 1000);
}
// the problem!
#action
addMeasurement(sensorMeasurementMap) {
let self = this;
// this timeout is to try and simulate a race condition
// since each measurement is incoming each second,
// here some of them will take as long as 6 seconds to add,
// due to the timeout.
// the point is that they should always be added,
// in the order they were called in.
// so if the first measurement takes 20 seconds to be added,
// the next measurements that were received on 2, 3, 4, 5..., 19th second etc,
// should all "wait" for the prev measurement, so they're added
// in the right order (order can be checked by timestamp, x)
setTimeout(() => {
const keys = self.sensors.keys();
if (keys.length === 0) {
// never really gonna happen, since we already set them above
} else {
for (const key in sensorMeasurementMap) {
if (self.sensors.keys().indexOf(key) > -1) {
self.sensors.get(key).add(sensorMeasurementMap[key]);
} else {
// also not gonna happen in this example
}
}
}
}, Math.floor(Math.random() * 20 + 1) * 1000);
}
}
Sensor.js:
import Queue from './Queue';
import {observable, action} from 'mobx';
export default class Sensor {
#observable queue;
constructor(n) {
this.n = n;
this.queue = new Queue(this.n);
}
#action add(measurement) {
this.queue.add(measurement);
}
}
Queue.js:
import {observable, action} from 'mobx';
export default class Queue {
#observable data;
constructor(maxSize) {
this.maxSize = maxSize;
this.size = 0;
this.data = [];
}
#action add(measurement) {
let removedItem = undefined;
if(this.size >= this.maxSize) {
let temp = this.data[0];
removedItem = temp && temp.y ? temp.y+'' : undefined;
this.data.shift();
}
this.data.push(measurement);
if (removedItem === undefined && this.size < this.maxSize) {
this.size++;
}
return removedItem;
}
}
There's a few comments in the code but you absolutely need to see the output https://codesandbox.io/s/github/BruceL33t/mobx-action-synchronous-execution-order/tree/master/ to understand it.
Let me also try to explain it here, what this is all about.
This is basically a overly simplified version of a part of a real application, where setInterval is just used instead to simulate a SignalR event handler to indicate incoming data each second. The incoming data is what we create inside the setInterval func above the addMeasurement action.
So given some incoming data is received each second, we want to add this to the observable map sensors on the store. Since this data is used for drawing charts in the real application, we need to make sure it is indeed added in the order which the actions are invoked in - no matter how long the action takes to complete.
In the real application I saw some inconsistency in the order of how the data were pushed to the MobX state, so I isolated it and extracted the relevant parts into this example and tried to exaggerate it a bit by using the setTimeout func inside the addMeasurement action.
Since each data is fetched each second, but some measurements could take up to 20 seconds to fetch (not realisticly, but to clearly show race condition problem), as the code is right now, it often happens that we end up with something like:
[
{"x":1519637083193,"y":4411},
{"x":1519637080192,"y":7562},
{"x":1519637084193,"y":1269},
{"x":1519637085192,"y":8916},
{"x":1519637081192,"y":7365}
]
Which really should never happen, since 1519637083193 is greater/later than 1519637080192.
This is a real problem when drawing charts from this data and ordering it afterwards is way too expensive, so I'm looking for a way to improve this code so we can trust each addMeasurement is only fired once the previous action has completely finished. Or at least a way to update the MobX state in the right order
Hope it makes sense.
should all "wait" for the prev measurement, so they're added in the right order (order can be checked by timestamp, x).
Could you elaborate on that? How could one ever know that no timestamp larger than the current one will be received in the future, and hence wait indefinitely? Isn't what you are looking for just a sorted insertion to the array of measurements (instead of waiting)?
If sorted insertion doesn't solve the problem, I would probably do the following (untested):
lastAddition = Promise.resolve() // start with already finishied addition
addMeasurement(sensorMeasurementMap) {
this.lastAddition = this.lastAddition.then(() => {
return new Promise((resolve, reject) => {
setTimeout(action(() => {
const keys = self.sensors.keys();
if (keys.length === 0) {
// never really gonna happen, since we already set them above
} else {
for (const key in sensorMeasurementMap) {
if (self.sensors.keys().indexOf(key) > -1) {
self.sensors.get(key).add(sensorMeasurementMap[key]);
} else {
// also not gonna happen in this example
}
}
}
resolve()
}), Math.floor(Math.random() * 20 + 1) * 1000);
})
})
}
}
N.B.: Note that I moved action inside, as you need it at the place where you are actually modifying the state, not where the scheduling happens
I have a jQuery datatable that immediately loads ON READY. After that, the datatable is reloaded every 30 seconds. This feature is functioning properly.
I have added a search feature that automatically reloads the datatable with new search results. This part is also functioning properly.
The problem I am experiencing is when I am using the search feature, and the new search results are returned. After 30 seconds, the new results are cleared and the datatable reloads with all of the original records.
Here is what I am currently attempting:
$(document).ready(function()
{
var searchCriteria = "";
displayBookings(searchCriteria);
var idle = 0;
var idleInterval = setInterval(timer, 30000);
$(this).mousemove(function(e){idle = 0;});
$(this).keypress(function(e){idle = 0;});
function timer()
{
idle = idle + 1;
if(idle > 2)
{
displayBookings(searchCriteria);
console.log('table reloaded');
}
}
$('#searchPending').on('click', function()
{
var isPending = 'Y';
var searchCriteria = {
isPending: isPending
};
displayBookings(searchCriteria);
});
});
The function displayBookings() takes searchCriteria. If searchCriteria is blank, then a basic query is fired. Obviously is searchCriteria contains parameters, then the same query is fired with a WHERE clause attached. I did not disclose the code for displayBookings().
All I need to do is stop the 30 second interval if the #searchPending button is clicked.
Clear the interval so it will stop loading.
clearInterval(idleInterval)
specifically in your code:
$('#searchPending').on('click', function()
{
clearInterval(idleInterval)
var isPending = 'Y';
var searchCriteria = {
isPending: isPending
};
displayBookings(searchCriteria);
});
Rather than start and stop the timer interval, since you'll run into a bit of a race condition, you can just have the "refresh" (your "timer" function) refresh using the latest search criteria. To do this, just pass the same object into your displayBookings function. E.g.
const search = { criteria: "" };
$(...).click(() => {
search.criteria = 'change it...';
displayBookings(search.criteria);
});
setInterval(() => displayBookings(search.criteria), 30000);
This way, if a refresh happens, it will use the latest search.criteria. You can achieve the same result with minimal change in your code by simply removing the var from the second searchCriteria. Currently, without removing the var, your outer criteria is being "shadowed" by your inner.
I alluded to debouncing1 in one of my comments. I misread the code and debouncing is not what you want. Instead, you want to only "refresh" if there hasn't been any user activity within some threshold. Here's an alternative from the approach you used:
let lastInteraction = 0;
function interact() {
lastInteraction = Date.now();
}
$(this).mousemove(interact);
$(this).keypress(interact);
Then in your refresh function:
if (Date.now() - lastInteraction > threshold) { ...
Implementing both the central criteria and revised idle check:
$(document).ready(function() {
const idle = {
threshold: 1000,
lastInteraction: 0,
interact() {
idle.lastInteraction = Date.now();
},
isIdle() {
return Date.now() - idle.lastInteraction > idle.threshold;
}
};
const search = { criteria: "" };
$(this).mousemove(idle.interact);
$(this).keypress(idle.interact);
setInterval(() => {
if (idle.isIdle()) {
displayBookings(search.criteria);
}
}, 30000);
$('#searchPending').on('click', () => {
search.criteria = { isPending: 'Y' };
displayBookings(search.criteria);
});
displayBookings(search.criteria);
});
1 The Wikipedia article linked to discusses debouncing with a keyboard. It's the same concept. You'd use debouncing on your displayBookings function if you plan on having it execute live as the user is typing. This would prevent too many HTTP requests from happening in a short duration of time.
How do you implement infinite scroll on data that you get from firebase. So far I found an angularjs directive, that works really great but I'm having difficulty implementing it with fireable as firebase returns all data in one single request and this is not what I want.
Few weeks ago, I made a JS function that allowed an infinite scrolling in my app.
First, a set of data is displayed when the user visit the website:
// Add a callback that is triggered for each message.
var n = 25; // Step size for messages display.
$(window).load(function() {
lastMessagesQuery = messagesRef.limit(n);
lastMessagesQuery.on('child_added', function (snapshot) {
var message = snapshot.val();
$('<div/>').text(message.text).prependTo($('#messagesDiv'));
$('#messagesDiv')[0].scrollTop = $('#messagesDiv')[0].scrollHeight;
});
$('#messagesDiv').fadeTo(1000, 1);
});
Then, the function that makes possible the infinite scrolling:
// Pagination.
var i = 0; // Record variable.
function moreMessages () {
i += n; // Record pagination updates.
moreMessagesQuery = messagesRef; // Firebase reference.
moreMessagesQuery.on('value', function (snapshot) {
var data = snapshot.exportVal(); // Fetch all data from Firebase as an Object.
var keys = Object.keys(data).reverse(); // Due to the Keys are ordered from the oldest to the newest, it nessesary to change its sequence in order to display Firebase data snapshots properly.
var total_keys = Object.keys(data).length;
var k = keys[i]; // Key from where to start counting. Be careful what Key you pick.
if (i < total_keys) { // Stop displaying messages when it reach the last one.
lastMessagesQuery = messagesRef.endAt(null, k).limit(n); // Messages from a Key to the oldest.
lastMessagesQuery.on('child_added', function (snapshot) {
var message = snapshot.val();
$('<div/>').text(message.text).appendTo($('#messagesDiv')).hide().fadeIn(1000); // Add set of messages (from the oldest to the newest) at the end of #messagesDiv.
});
}
});
}
Finally, the infinite scrolling:
// Load more messages when scroll reach the bottom.
$(window).scroll(function() {
if (window.scrollY == document.body.scrollHeight - window.innerHeight) {
moreMessages();
}
});
It works great with small data sets. I hope this helps you to solve your problem (or gives you more ideas).
UPDATE October 2015
Firebase has growth since my original response, which means now it's pretty easy to achieve an infinite scrolling just using its Javascript API:
First, I recommend to create an Index in your Firebase. For this answer, I create this one:
{
"rules": {
".read": true,
".write": false,
"messages": {
".indexOn": "id"
}
}
}
Then, let's make some magic with Firebase:
// #fb: your Firebase.
// #data: messages, users, products... the dataset you want to do something with.
// #_start: min ID where you want to start fetching your data.
// #_end: max ID where you want to start fetching your data.
// #_n: Step size. In other words, how much data you want to fetch from Firebase.
var fb = new Firebase('https://<YOUR-FIREBASE-APP>.firebaseio.com/');
var data = [];
var _start = 0;
var _end = 9;
var _n = 10;
var getDataset = function() {
fb.orderByChild('id').startAt(_start).endAt(_end).limitToLast(_n).on("child_added", function(dataSnapshot) {
data.push(dataSnapshot.val());
});
_start = _start + _n;
_end = _end + _n;
}
Finally, a better Infinite Scrolling (without jQuery):
window.addEventListener('scroll', function() {
if (window.scrollY === document.body.scrollHeight - window.innerHeight) {
getDataset();
}
});
I'm using this approach with React and it's blazing fast no matter how big your data is.