I use vue-infinitegrid and I have realized in a browser that a backend API is called three times. Some code first (git):
<GridLayout
ref="ig"
:options="options"
:layoutOptions="layoutOptions"
#append="onAppend"
#layout-complete="onLayoutComplete"
#image-error="onImageError"
>
<div slot="loading">Loading...</div>
<div class="item" v-for="(item) in list" :key="item.key">
<ViewItem :item="item"/>
</div>
</GridLayout>
data() {
return {
start: 0,
loading: false,
list: [],
isEnded: false,
options: {
isOverflowScroll: false,
useFit: true,
useRecycle: true,
horizontal: false,
align: 'center',
transitionDuration: 0.2,
},
layoutOptions: {
margin: 15,
align: 'center',
},
pageSize: 3,
};
},
methods: {
async onAppend({ groupKey, startLoading }) {
this.$log.debug(`onAppend group key = ${groupKey}`);
const { list } = this;
if (this.isEnded) return;
const items = await this.loadItems();
startLoading();
this.list = list.concat(items);
},
async loadItems() {
const start = this.start || 0, size = parseFloat(this.pageSize), { tag } = this;
this.$log.debug(`loadItems start = ${start}, size = ${size}`);
let res = await this.$store.dispatch('GET_ITEM_STREAM', { start, size, tag });
if (res.length === 0) { // todo or smaller than requested
this.$log.debug('loadItems no data');
this.isEnded = true;
this.$refs.ig.endLoading();
return res;
}
if (this.exceptItem) {
res = res.filter(item => item._id !== this.exceptItem._id);
}
this.start = start + res.length;
this.$log.debug('loadItems finished');
return res;
},
onLayoutComplete({ isLayout, endLoading }) {
this.$log.debug(`onLayoutComplete isLayout = ${isLayout}`);
if (!isLayout) {
endLoading();
}
},
And some logs:
onAppend group key =
ItemList.vue:71 loadItems start = 0, size = 3
items.js:132 GET_ITEM_STREAM {"start":0,"size":3}
See more tips at https://vuejs.org/guide/deployment.html
ItemList.vue:83 loadItems finished
ItemList.vue:87 onLayoutComplete isLayout = false
ItemList.vue:62 onAppend group key =
ItemList.vue:71 loadItems start = 3, size = 3
items.js:132 GET_ITEM_STREAM {"start":3,"size":3}
ItemList.vue:62 onAppend group key =
ItemList.vue:71 loadItems start = 3, size = 3
items.js:132 GET_ITEM_STREAM {"start":3,"size":3}
2 ItemList.vue:83 loadItems finished
ItemList.vue:87 onLayoutComplete isLayout = false
ItemList.vue:62 onAppend group key =
ItemList.vue:71 loadItems start = 6, size = 3
items.js:132 GET_ITEM_STREAM {"start":6,"size":3}
ItemList.vue:62 onAppend group key =
ItemList.vue:71 loadItems start = 6, size = 3
items.js:132 GET_ITEM_STREAM {"start":6,"size":3}
2 ItemList.vue:83 loadItems finished
ItemList.vue:87 onLayoutComplete isLayout = false
I can see that start is incremented after onAppend is called. It looks like some concurrency issue, that the infinitegrid component does not wait until the REST call is finished and fires new event. Has anybody any experience with this component and knows how to handle this situation when I need to wait for a backend response?
Update
I have replaced async call with fixed data and it started to work correctly. So the trouble is with async.
// let items = await this.$store.dispatch('GET_ITEM_STREAM', { start, size, tag });
let items = [{ ...
Update:
Code sandbox with minimum reproducible scenerio: https://w56p2.csb.app/
The symptoms are different now, probably exhibiting the root cause - the event is emitted before the previous is processed.
https://github.com/naver/egjs-infinitegrid/issues/365
https://naver.github.io/egjs-infinitegrid/storybook/?path=/story/loading-bar-with-data-delay--grid-layout
In startLoading and endLoading, the loading bar appears and disappears, and some functions are temporarily disabled (moveTo, useFit).
The append and prepend work and must be prevented through the isProcessing method.
onAppend({ groupKey, startLoading, currentTarget }) {
if (currentTarget.isProcessing()) {
return;
}
}
Related
By default on the start event noUiSlider adds an active class to the active handle but removes it when the event has ended. I want a way to show the user that the handle has been dragged already so changing the colour via a css class would be ideal.
I cannot tell which of the handles has been dragged from the data it provides.
Here is my function which initiates noUISlider
setRangeSlider(e) {
const min = 0;
const max = 1000000;
noUiSlider.cssClasses.target += ' c-range-slider';
const options = {
start: [min || 0, max || this.maxAvailablePrice.id],
step: 50000,
tooltips: true,
connect: true,
range: {
min,
max,
},
format: {
to(value) {
return formatCurrency([value]);
},
from(value) {
return stripFormatting(value);
},
},
};
this.slider = noUiSlider.create(e, options);
this.slider.on('start', (values) => {
console.log('SearchFiltersPriceComponent -> setRangeSlider -> this.slider', this.slider);
});
}
When I console out this.slider from the start event it prints out all sorts of useful information but I cannot find which handle has just been dragged and how to target that handle to add a class to it.
this.slider.target will return the slider element and handle parameter in event callback function will return the index of the handle that has been dragged. so these two can be used together to locate a particular handle. see the code for example
setRangeSlider(e) {
const min = 0;
const max = 1000000;
noUiSlider.cssClasses.target += ' c-range-slider';
const options = {
start: [min || 0, max || this.maxAvailablePrice.id],
step: 50000,
tooltips: true,
connect: true,
range: {
min,
max,
},
format: {
to(value) {
return formatCurrency([value]);
},
from(value) {
return stripFormatting(value);
},
},
};
this.slider = noUiSlider.create(e, options);
this.slider.on('start', (values, handle, unencoded, tap, positions, noUiSlider) => {
let sliderElem = this.slider.target;
let handleElem = document.querySelectorAll("div.noUi-handle[data-handle='" + handle + "']")[0];
console.log('SearchFiltersPriceComponent -> setRangeSlider -> this.slider', handleElem);
handleElem.classList.add("mystyle");
});
this.slider.on('end', (values, handle, unencoded, tap, positions, noUiSlider) => {
let sliderElem = this.slider.target;
let handleElem = document.querySelectorAll("div.noUi-handle[data-handle='"+handle+"']")[0];
handleElem.classList.remove("mystyle");
});
}
I have a scenario where I have a set of data where a filename maps to a set of graph line data. On a checkbox change,
it will either delete from or add to the map (not a JS map - just a JS object). The add operation works fine, at first.
The delete operation also appears to work, and after deleting a filename from the map the React state appears to have
updated correctly. However, when adding another item react seems to "resurrect" a very old state that represents the
collection of all files ever added to the map. This behavior is caused by a specific setState() call, which I highlight
below. Been going round in circles debugging this and have hit a dead end. The reason state is cloned in the console.log()
call is that I found out that the Chrome console.log calls are asynchronous.
I was being a bit more choosey about what I was deep cloning but to remove any uncertainty I just deep cloned everything.
What exactly break is.
Select file1.
1.1 file 1 data displayed on graph
Select file2.
2.1 file 2 data displayed on graph
Delete file 2
3.1 Only file 1 data not displayed in graph
Delete file 1
4.1 Nothing displayed on graph. this.state appears to reflect this both in the console.log output and the
react development tools inspection of the element state.
Select any file, but lets say file 3
5.1 Graph displays data for file1, file2, and file3.
5.2 This is wrong - in 4.1 saw that the state showed no graph data was mapped. The error is introduced
by a setState() call that only updates one, unrelated flag, that is used to show a modal dialog.
If I remove the call this.setState({fetching_data: true},... from getFIleData() then everything works. For some reason
if that call is there, it receives as older state.
If anyone can shed light on this, I would be most grateful, as I have run out of ideas ;)
class ResultsList extends React.Component
{
state = {
fetching_data: false,
// Data on all available files, for which server can provide data
//
// [ {
// date: (5) [mm, dd, hh, mm, ss]
// pm: "pmX"
// fullname: "mm_dd_hh_mm_ss_pmX_TAG"
// tag: "TAG"
// serno: "1234"
// },
// ...
// ]
data : [],
selected_col: "",
// Contents of data files that have been requested from server. If item is in this
// dictionary then is is "selected", i.e. displayed on the graph. When "deselected"
// should be removed from this dictionary.
//
// { filename1 : {
// data: {dataset1: Array(45), dataset2: Array(45), xvalues: Array(45)}
// path: "blah/blah"
// status: 200
// status_str: "ok"
// >>>> These bits are augmented, the above is from server
// FAM_colour: string,
// HEX_color: string,
// <<<<
// },
// filename2 : {
// ...
// }
// }
file_data: {},
file_data_size: 0,
graph: null,
};
createGraphDataSetsFromFileData = (srcFileData) => {
const newGraphDatasets = [];
let idx_prop = 0;
for (var prop in srcFileData) {
if (Object.prototype.hasOwnProperty.call(srcFileData, prop)) {
newGraphDatasets.push(
{
label: 'dataset1_' + prop,
fill: false,
lineTension: 0.5,
backgroundColor: 'rgba(75,192,192,1)',
borderColor: srcFileData[prop].FAM_colour,
borderWidth: 2,
data: srcFileData[prop].data['dataset1'],
}
);
newGraphDatasets.push(
{
label: 'dataset2_' + prop,
fill: false,
lineTension: 0.5,
backgroundColor: 'rgba(75,192,192,1)',
borderColor: srcFileData[prop].HEX_colour,
borderWidth: 2,
data: srcFileData[prop].data['dataset2'],
}
);
idx_prop = idx_prop + 1;
}
}
return newGraphDatasets;
};
getFIleData = (filename) => {
console.log("GETTING OPTICS");
console.log(cloneDeep(this.state));
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// If I remove this setState call then everything works
//
// Display the "fetching data" modal dialog
this.setState({fetching_data: true},
() => {
console.log("££££££ DIALOG SHOW COMPLETED ££££££"); console.log(cloneDeep(this.state));
}
);
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
fetch(`http://${this.SERVER_IP_PORT}/api/v1/get_result/${filename}`)
.then(response => response.json())
.then(json => {
console.log("JSON REPLY RECEIVED")
if (json.status !== 200) {
alert("Failed to fetch results list")
}
else {
const EXPECTED_NO_CYCLES = 45 // Oh so terribly hacky!!!
if (json.data['dataset1'].length === EXPECTED_NO_CYCLES) {
this.setState( (prevState) => {
console.log("JSON SETSTATE PREV STATE AS"); console.log(prevState);
// Clone the file_data map and add the new data to it
let newFileData = cloneDeep(prevState.file_data);
newFileData[filename] = cloneDeep(json); // TODO - FIXME - BAD this is a shallow copy
newFileData[filename].FAM_colour = COLD_COLOURS[prevState.file_data_size % COLD_COLOURS.length];
newFileData[filename].HEX_colour = WARM_COLOURS[prevState.file_data_size % WARM_COLOURS.length];
// Create new graph data from the file_data map
let newGraph = null;
if (newGraph === null) {
newGraph = {
labels : cloneDeep(json.data['xvalues']),
datasets: this.createGraphDataSetsFromFileData(newFileData)
}
}
else {
newGraph = cloneDeep(prevState.graph)
newGraph.labels = cloneDeep(json.data['xvalues']);
newGraph.datasets = this.createGraphDataSetsFromFileData(newFileData)
}
const retval = {
file_data: newFileData,
file_data_size: prevState.file_data_size + 1,
graph : newGraph
};
console.log("------- returning:"); console.log(retval);
return retval;
}, () => {console.log("££££££ OPTICS STAT EUPDATE APPLIED ££££££"); console.log(cloneDeep(this.state)); });
}
else {
alert("Assay test run contains imcomplete data set");
}
}
})
.catch( error => {
alert("Failed to fetch results list: " + error);
})
.finally( () => {
console.log("##################################################################")
this.setState({fetching_data:false},
() => {console.log("££££££ OPTICS STAT FINALLY NOT FETCH APPLIED ££££££"); console.log(cloneDeep(this.state)); });
});
};
//
// THIS FUNCTION IS THE onChange FOR A CHECKBOX and is passed the filename of the item clicked
handleRowSelectionChange = (fullname) => {
if (this.state.file_data.hasOwnProperty(fullname)) {
console.log("CHECKBOX NOT TICKED")
this.setState(
(prevState) => {
// Delete the file from the map
let newFileData = cloneDeep(prevState.file_data);
delete newFileData[fullname];
let rv = {
file_data: newFileData,
file_data_size: prevState.file_data_size - 1,
graph : {
datasets: this.createGraphDataSetsFromFileData(newFileData),
labels: cloneDeep(prevState.graph.labels)
}
}
console.log("______"); console.log(rv);
return rv;
},
() => {
console.log("======== DELETE UPDATE APPLIED =======");
console.log(cloneDeep(this.state));
}
);
}
else {
console.log("CHECKBOX IS TICKED");
this.getFIleData(fullname);
}
}
If I select two files, I expect 4 datasets in the graph and the state reflects this:
If I then delete these lines, I expect to see no graph data, and the state appears to reflect this:
But! If I then click on a third file...
The old state is introduced, specifically by
this.setState({fetching_data: true},
() => {
console.log("££££££ DIALOG SHOW COMPLETED ££££££"); console.log(cloneDeep(this.state));
}
);
in the getFileData function. If this is removed, then no stale state is introduced.
I built a small boat visualizer. I am using AISHub APIs. After fetching data from the APIs I am able to obtain a json file with the vessels I am interested in and inject these vessels inside a table.
The user has to manually update the page pushing the refresh button on top left of the page to see the new updated table.
The problem: How to set a state to refresh the google-map content automatically every minute instead of the user doing it manually?
Below the code:
GoogleMap.js
class BoatMap extends Component {
constructor(props) {
super(props);
this.state = {
buttonEnabled: true,
buttonClickedAt: null,
progress: 0,
ships: [],
type: 'All',
shipTypes: [],
activeShipTypes: [],
logoMap: {}
};
this.updateRequest = this.updateRequest.bind(this);
this.countDownInterval = null;
}
async componentDidMount() {
this.countDownInterval = setInterval(() => {
if (!this.state.buttonClickedAt) return;
const date = new Date();
const diff = Math.floor((date.getTime() - this.state.buttonClickedAt.getTime()) / 1000);
if (diff < 90) {
this.setState({
progress: diff,
buttonEnabled: false
});
} else {
this.setState({
progress: 0,
buttonClickedAt: null,
buttonEnabled: true
});
}
}, 500);
await this.updateRequest();
const shipTypeResults = await Client.getEntries({
content_type: 'competitors'
});
console.log(shipTypeResults);
const shipTypes = shipTypeResults.items.map((data) => data.fields);
const logoMap = shipTypes.reduce((acc, type) => {
return {
...acc,
[type.name]: type.images.fields.file.url
};
}, {});
console.log({ shipTypes });
this.setState({
logoMap
});
}
componentDidUpdate(prevProps, prevState) {
if (this.state.type !== prevState.type) {
}
}
componentWillUnmount() {
clearInterval(this.countdownInterval);
}
async updateRequest() {
const url = 'http://localhost:3001/hello';
console.log(url);
const fetchingData = await fetch(url);
const ships = await fetchingData.json();
console.log(ships);
this.setState({
buttonEnabled: false,
buttonClickedAt: new Date(),
progress: 0,
ships
});
setTimeout(() => {
this.setState({ buttonEnabled: true });
});
}
render() {
return (
<div className="google-map">
<GoogleMapReact
bootstrapURLKeys={{ key: 'KEY' }}
center={{
lat: this.props.activeShip ? this.props.activeShip.latitude : 42.4,
lng: this.props.activeShip ? this.props.activeShip.longitude : -71.1
}}
zoom={8}
>
</GoogleMapReact>
</div>
);
}
}
What I have done so far:
A good way would be using a setTimeout() but would that be correct? Where should that be applied and how?
setTimeout(function () {
location.reload();
}, 60 * 1000);
Or maybe setting an interval as a refresh rate?
I am a bit confused on what would the best way to approach this.
On your request function i guess u want to disable the button while the api doesn't return, so maybe move this piece above the requests:
this.setState({
buttonEnabled: false,
buttonClickedAt: new Date(),
progress: 0,
ships
});
If im wrong you could remove the timeout from the second setState and call as a callback on the first like this:
this.setState({
buttonEnabled: false,
buttonClickedAt: new Date(),
progress: 0,
ships
}, () => {
this.setState({ buttonEnabled: true });
});
on the last part instead of location.reload() set a interval calling the update on ur componentDidMount:
let updateInterval = setInterval(() => {
this.updateRequest();
}, 60 * 1000);
this.setState({updateInterval})
then on the componentWillUnmount you clear the interval this.state.updateInterval
I'm using VueJS 2 and Firestore for this project.
I have an infinite loading method, where I need the next item to load, when the user hits the bottom of the page on scroll.
Everything is in the same method called getStraps()
The idea is to have a first set of items to load, and when the user hits the bottom, then it should load the next batch of items.
Main problem: The first two items are loading as usual, and then another one is loading. But then when I scroll another time, the 4th item repeats to be the same as the 3rd as so on. The variable "lastVisible" doesn't seem to update, so it won't load the following items with "startAfter"
Video: http://recordit.co/TwqEb4SeWe
getStraps() {
var strapper = db.collection("straps");
var first = strapper.limit(2);
return first.get().then(documentSnapshots => {
var lastVisible =
documentSnapshots.docs[documentSnapshots.docs.length - 1];
console.log("first last visible", lastVisible);
const straps = [];
documentSnapshots.forEach(doc => {
const data = {
id: doc.id,
title: doc.data().title,
price: doc.data().price,
skin: doc.data().skin,
type: doc.data().type,
imgs: doc.data().imgs[0].url,
colors: doc.data().colors,
desc: doc.data().desc,
date: doc
.data()
.date.toString()
.slice(0, 15)
};
straps.push(data);
});
this.straps = straps;
var next = strapper.startAfter(lastVisible).limit(1);
window.onscroll = () => {
let bottomOfWindow =
document.documentElement.scrollTop + window.innerHeight ===
document.documentElement.offsetHeight;
if (bottomOfWindow) {
this.fetchingData = true;
console.log("fetch", this.fetchingData);
return next.get().then(documentSnapshots => {
var lastVisible =
documentSnapshots.docs[documentSnapshots.docs.length - 1];
console.log("last last", lastVisible);
if (documentSnapshots.empty) {
this.fetchingData = false;
this.noMoreStraps = true;
} else {
documentSnapshots.forEach(doc => {
const straps = this.straps;
const data = {
id: doc.id,
title: doc.data().title,
price: doc.data().price,
skin: doc.data().skin,
type: doc.data().type,
imgs: doc.data().imgs[0].url,
colors: doc.data().colors,
desc: doc.data().desc,
date: doc
.data()
.date.toString()
.slice(0, 15)
};
console.log("more data", data);
straps.push(data);
this.fetchingData = false;
});
this.straps = straps;
}
});
}
};
});
},
I am using mirage for creating fake data.
scenario/default.js
export default function(server) {
server.createList('product', 48);
server.loadFixtures();
}
Above I am creating 48 products and from controller I am calling
this.store.query('product', {
filter: {
limit: 10,
offset: 0
}
}).then((result) => {
console.log(result);
});
and in mirage/config.js
this.get('/products', function(db) {
let products = db.products;
return {
data: products.map(attrs => ({
type: 'product',
id: attrs.id,
attributes: attrs
}))
};
});
now my question is, how to load 10 products per page? I am sending in filter 10 as page size and offset means page number.
what changes should be done to config.js to load only limited products?
In your handler in mirage/config.js:
this.get('/products', function(db) {
let images = db.images;
return {
data: images.map(attrs => ({
type: 'product',
id: attrs.id,
attributes: attrs
}))
};
});
You are able to access the request object like so:
this.get('/products', function(db, request) {
let images = db.images;
//use request to limit images here
return {
data: images.map(attrs => ({
type: 'product',
id: attrs.id,
attributes: attrs
}))
};
});
Have a look at this twiddle for a full example.
Where the this twiddle has the following:
this.get('tasks',function(schema, request){
let qp = request.queryParams
let page = parseInt(qp.page)
let limit = parseInt(qp.limit)
let start = page * limit
let end = start + limit
let filtered = tasks.slice(start,end)
return {
data: filtered
}
})
You'll just adapt it for your use like this:
this.get('products',function(db, request){
let qp = request.queryParams
let offset = parseInt(qp.offset)
let limit = parseInt(qp.limit)
let start = offset * limit
let end = start + limit
let images = db.images.slice(start,end)
return {
data: images.map(attrs => ({
type: 'product',
id: attrs.id,
attributes: attrs
}))
}
})
An example with todos, you can adapt it to your own use case.
// Fetch all todos
this.get("/todos", (schema, request) => {
const {queryParams: { pageOffset, pageSize }} = request
const todos = schema.db.todos;
if (Number(pageSize)) {
const start = Number(pageSize) * Number(pageOffset)
const end = start + Number(pageSize)
const page = todos.slice(start, end)
return {
items: page,
nextPage: todos.length > end ? Number(pageOffset) + 1 : undefined,
}
}
return todos
});