Apollo Client - FetchMore with useLazyQuery, variables do not update (for offset pagination) - apollo-client

Intended outcome:
We did a pagination implementation, following the apollo docs (https://www.apollographql.com/docs/react/pagination/offset-based/). Using a read/merge function
We expect that the read function is called with the new variables, once we call the fetchMore function and update the variables manually.
Actual outcome:
We do not manage to get the pagination working, the data is fetched correctly but is not displayed properly. The bad display is caused by the variables that are not updated. The "read" function always receives the variables of the original query.
useEffect(() => {
if (isDefined(fetchMore)) {
fetchMore({
variables: {
offset:
currentPage === 1
? (currentPage - 1) * total
: (currentPage - 1) * total + 1,
first: total,
searchString: searchString
}
})
} else {
getData({
variables: {
offset: 0,
first: total,
searchString: searchString
}
})
} }, [currentPage, fetchMore, searchString, getData])
We have also tried to manually update our variables, since we do not use useQuery, but instead useLazyQuery.
client
.watchQuery({
query: GET_REPORTS,
variables: {
offset: 0,
first: total,
searchString: searchString
}
})
.setVariables({
offset:
currentPage === 1
? (currentPage - 1) * total
: (currentPage - 1) * total + 1,
first: 25,
searchString
})
Our typePolicy:
typePolicies: {
Query: {
fields: {
REPORT: {
keyArgs: false,
read(existing, {args}) {
**//here we would expect for our args to change, the moment we try fetch our
//second page**
return existing && existing.slice(args.offset, args.offset + args.first)
},
// The keyArgs list and merge function are the same as above.
merge(existing, incoming, { args: { offset = 0 } }) {
const merged = existing ? existing.slice(0) : []
for (let i = 0; i < incoming.length; ++i) {
merged[offset + i] = incoming[i]
}
return merged
}
}
}
}
}

I had the same issue earlier, turns out since you are using useLazyQuery, you have to query the function first, be using fetchMore.
Example
const [getList,{loading, fetchMore}] = useLazyQuery (GET_LIST)
Then implement a condition like so;
if(! fetchMore){ getList(....passargs) }else{ fetchMore (...passargs) }
This is because fetchMore will be undefined without calling getList.

Related

How to let vue component wait until the data is ready for render?

vue component won't wait for data from controller using axios get, it prompt error:
index.vue?d4c7:200 Uncaught (in promise) TypeError: Cannot read property 'ftth' of undefined
my code are below:
<template>
<div class="dashboard-editor-container">
<el-row style="background:#fff;padding:16px 16px 0;margin-bottom:32px;">
<line-chart :chart-data="lineChartData"/>
</el-row>
</div>
</template>
<script>
import LineChart from './components/LineChart';
import axios from 'axios';
const lineChartData = {
all: {
FTTHData: [],
VDSLData: [],
ADSLData: [],
},
};
export default {
name: 'Dashboard',
components: {
LineChart,
},
data() {
return {
lineChartData: lineChartData.all,
};
},
created() {
this.getData();
},
methods: {
handleSetLineChartData(type) {
this.lineChartData = lineChartData[type];
},
async getData() {
axios
.get('/api/data_graphs')
.then(response => {
console.log(response.data);
var data = response.data;
var i = 0;
for (i = Object.keys(data).length - 1; i >= 0; i--) {
lineChartData.all.FTTHData.push(data[i]['ftth']);
lineChartData.all.VDSLData.push(data[i]['vdsl']);
lineChartData.all.ADSLData.push(data[i]['adsl']);
}
});
},
},
};
</script>
Do I have to use watch method?
First, because you have such a nested data structure you'll want a computed property to return whether the data is loaded or not. Normally, you could do this check in the template.
computed: {
isDataLoaded() {
const nestedLoaded = Object.keys(this.lineChartData).map(key => this.lineChartData[key].length !== 0)
return this.lineChartData && nestedLoaded.length !== 0
}
}
You can use v-if="isDataLoaded" to hide the element until the data has been loaded.
It is not exactly clear how response.data looks like, but because you're using Object.keys I'm assuming it's an object.
If you need to loop over the keys then when using numeric indexes you most likely won't get an object. So you need to get the key and index i and use that value to access the object. Change this:
for (i = Object.keys(data).length - 1; i >= 0; i--) {
lineChartData.all.FTTHData.push(data[i]['ftth']);
lineChartData.all.VDSLData.push(data[i]['vdsl']);
lineChartData.all.ADSLData.push(data[i]['adsl']);
}
to this:
const keys = Object.keys(data)
for (i = keys.length - 1; i >= 0; i--) {
lineChartData.all.FTTHData.push(data[keys[i]]['ftth']);
lineChartData.all.VDSLData.push(data[keys[i]]['vdsl']);
lineChartData.all.ADSLData.push(data[keys[i]]['adsl']);
}
But for looping over object's keys is easier to use this:
for (let key in data) {
lineChartData.all.FTTHData.push(data[key]['ftth']);
lineChartData.all.VDSLData.push(data[key]['vdsl']);
lineChartData.all.ADSLData.push(data[key]['adsl']);
}
The alternative syntax will feed you keys and in my opinion is easier to read.
For the mean time:
Use set Axios Timeout 5000ms
axios
.get('/api/data_graphs', { timeout: 5000 })
.then(response => {
console.log(response.data);
var data = response.data;
var i = 0;
for (i = Object.keys(data).length - 1; i >= 0; i--) {
lineChartData.all.FTTHData.push(data[i]['ftth']);
lineChartData.all.VDSLData.push(data[i]['vdsl']);
lineChartData.all.ADSLData.push(data[i]['adsl']);
}
this.lineChartIsLoaded = true;
});
Use v-if in vue component
<line-chart v-if="lineChartIsLoaded" :chart-data="lineChartData" :date-data="dateData" />
Set lineChartIsLoaded to false at default
const lineChartIsLoaded = false;
You can write dummy data in your data properties before real ones are loading
all: {
FTTHData: ["Loading..."],
VDSLData: ["Loading..."],
ADSLData: ["Loading..."],
},

How to make ajax call on end of each block with infinite scrolling in ag-grid?

I am using ag-grid with angular 4.
I am using infinite scrolling as the rowModelType. But since my data is huge, we want to first call just 100 records in the first ajax call and when the scroll reaches the end, the next ajax call needs to be made with the next 100 records? How can i do this using ag-grid in angular 4.
This is my current code
table-component.ts
export class AssaysTableComponent implements OnInit{
//private rowData;
private gridApi;
private gridColumnApi;
private columnDefs;
private rowModelType;
private paginationPageSize;
private components;
private rowData: any[];
private cacheBlockSize;
private infiniteInitialRowCount;
allTableData : any[];
constructor(private http:HttpClient, private appServices:AppServices) {
this.columnDefs = [
{
headerName: "Date/Time",
field: "createdDate",
headerCheckboxSelection: true,
headerCheckboxSelectionFilteredOnly: true,
checkboxSelection: true,
width: 250,
cellRenderer: "loadingRenderer"
},
{headerName: 'Assay Name', field: 'assayName', width: 200},
{headerName: 'Samples', field: 'sampleCount', width: 100}
];
this.components = {
loadingRenderer: function(params) {
if (params.value !== undefined) {
return params.value;
} else {
return '<img src="../images/loading.gif">';
}
}
};
this.rowModelType = "infinite";
//this.paginationPageSize = 10;
this.cacheBlockSize = 10;
this.infiniteInitialRowCount = 1;
//this.rowData = this.appServices.assayData;
}
ngOnInit(){
}
onGridReady(params) {
this.gridApi = params.api;
this.gridColumnApi = params.columnApi;
//const allTableData:string[] = [];
//const apiCount = 0;
//apiCount++;
console.log("assayApiCall>>",this.appServices.assayApiCall);
const assaysObj = new Assays();
assaysObj.sortBy = 'CREATED_DATE';
assaysObj.sortOder = 'desc';
assaysObj.count = "50";
if(this.appServices.assayApiCall>0){
console.log("this.allTableData >> ",this.allTableData);
assaysObj.startEvalulationKey = {
}
}
this.appServices.downloadAssayFiles(assaysObj).subscribe(
(response) => {
if (response.length > 0) {
var dataSource = {
rowCount: null,
getRows: function (params) {
console.log("asking for " + params.startRow + " to " + params.endRow);
setTimeout(function () {
console.log("response>>",response);
if(this.allTableData == undefined){
this.allTableData = response;
}
else{
this.allTableData = this.allTableData.concat(response);
}
var rowsThisPage = response.slice(params.startRow, params.endRow);
var lastRow = -1;
if (response.length <= params.endRow) {
lastRow = response.length;
}
params.successCallback(rowsThisPage, lastRow);
}, 500);
}
}
params.api.setDatasource(dataSource);
this.appServices.setIsAssaysAvailable(true);
this.appServices.assayApiCall +=1;
}
else{
this.appServices.setIsAssaysAvailable(false)
}
}
)
}
}
I will need to call this.appServices.downloadAssayFiles(assaysObj) at the end of 100 rows again to get the next set of 100 rows.
Please suggest a method of doing this.
Edit 1:
private getRowData(startRow: number, endRow: number): Observable<any[]> {
var rowData =[];
const assaysObj = new Assays();
assaysObj.sortBy = 'CREATED_DATE';
assaysObj.sortOder = 'desc';
assaysObj.count = "10";
this.appServices.downloadAssayFiles(assaysObj).subscribe(
(response) => {
if (response.length > 0) {
console.log("response>>",response);
if(this.allTableData == undefined){
this.allTableData = response;
}
else{
rowData = response;
this.allTableData = this.allTableData.concat(response);
}
this.appServices.setIsAssaysAvailable(true);
}
else{
this.appServices.setIsAssaysAvailable(false)
}
console.log("rowdata>>",rowData);
});
return Observable.of(rowData);
}
onGridReady(params: any) {
console.log("onGridReady");
var dataSource = {
getRows: (params: IGetRowsParams) => {
this.info = "Getting datasource rows, start: " + params.startRow + ", end: " + params.endRow;
console.log(this.info);
this.getRowData(params.startRow, params.endRow)
.subscribe(data => params.successCallback(data));
}
};
params.api.setDatasource(dataSource);
}
Result 1 : The table is not loaded with the data. Also for some reason the service call this.appServices.downloadAssayFiles is being made thrice . Is there something wrong with my logic here.
There's an example of doing exactly this on the ag-grid site: https://www.ag-grid.com/javascript-grid-infinite-scrolling/.
How does your code currently act? It looks like you're modeling yours from the ag-grid docs page, but that you're getting all the data at once instead of getting only the chunks that you need.
Here's a stackblitz that I think does what you need. https://stackblitz.com/edit/ag-grid-infinite-scroll-example?file=src/app/app.component.ts
In general you want to make sure you have a service method that can retrieve just the correct chunk of your data. You seem to be setting the correct range of data to the grid in your code, but the issue is that you've already spent the effort of getting all of it.
Here's the relevant code from that stackblitz. getRowData is the service call that returns an observable of the records that the grid asks for. Then in your subscribe method for that observable, you supply that data to the grid.
private getRowData(startRow: number, endRow: number): Observable<any[]> {
// This is acting as a service call that will return just the
// data range that you're asking for. In your case, you'd probably
// call your http-based service which would also return an observable
// of your data.
var rowdata = [];
for (var i = startRow; i <= endRow; i++) {
rowdata.push({ one: "hello", two: "world", three: "Item " + i });
}
return Observable.of(rowdata);
}
onGridReady(params: any) {
console.log("onGridReady");
var datasource = {
getRows: (params: IGetRowsParams) => {
this.getRowData(params.startRow, params.endRow)
.subscribe(data => params.successCallback(data));
}
};
params.api.setDatasource(datasource);
}

How to batch additions to arrays/lists returned by rxjs observables?

have an observable that returns arrays/lists of things: Observable
And I have a usecase where is is a pretty costly affair for the downstream consumer of this observable to have more items added to this list. So I'd like to slow down the amount of additions that are made to this list, but not loose any.
Something like an operator that takes this observable and returns another observable with the same signature, but whenever a new list gets pushed on it and it has more items than last time, then only one or a few are added at a time.
So if the last push was a list with 3 items and next push has 3 additional items with 6 items in total, and the batch size is 1, then this one list push gets split into 3 individual pushes of lists with lengths: 4, 5, 6
So additions are batched, and this way the consumer can more easily keep up with new additions to the list. Or the consumer doesn't have to stall for so long each time while processing additional items in the array/list, because the additions are split up and spread over a configurable size of batches.
I made an addAdditionalOnIdle operator that you can apply to any rxjs observable using the pipe operator. It takes a batchSize parameter, so you can configure the batch size. It also takes a dontBatchAfterThreshold, which stops batching of the list after a certain list size, which was useful for my purposes. The result also contains a morePending value, which you can use to show a loading indicator while you know more data is incomming.
The implementation uses the new requestIdleCallback function internally to schedule the batched pushes of additional items when there is idle time in the browser. This function is not available in IE or Safari yet, but I found this excelent polyfill for it, so you can use it today anyways: https://github.com/aFarkas/requestIdleCallback :)
See the implementation and example usage of addAdditionalOnIdle below:
const { NEVER, of, Observable } = rxjs;
const { concat } = rxjs.operators;
/**
* addAdditionalOnIdle
*
* Only works on observables that produce values that are of type Array.
* Adds additional elements on window.requestIdleCallback
*
* #param batchSize The amount of values that are added on each idle callback
* #param dontBatchAfterThreshold Return all items after amount of returned items passes this threshold
*/
function addAdditionalOnIdle(
batchSize = 1,
dontBatchAfterThreshold = 22,
) {
return (source) => {
return Observable.create((observer) => {
let idleCallback;
let currentPushedItems = [];
let lastItemsReceived = [];
let sourceSubscription = source
.subscribe({
complete: () => {
observer.complete();
},
error: (error) => {
observer.error(error);
},
next: (items) => {
lastItemsReceived = items;
if (idleCallback) {
return;
}
if (lastItemsReceived.length > currentPushedItems.length) {
const idleCbFn = () => {
if (currentPushedItems.length > lastItemsReceived.length) {
observer.next({
morePending: false,
value: lastItemsReceived,
});
idleCallback = undefined;
return;
}
const to = currentPushedItems.length + batchSize;
const last = lastItemsReceived.length;
if (currentPushedItems.length < dontBatchAfterThreshold) {
for (let i = 0 ; i < to && i < last ; i++) {
currentPushedItems[i] = lastItemsReceived[i];
}
} else {
currentPushedItems = lastItemsReceived;
}
if (currentPushedItems.length < lastItemsReceived.length) {
idleCallback = window.requestIdleCallback(() => {
idleCbFn();
});
} else {
idleCallback = undefined;
}
observer.next({
morePending: currentPushedItems.length < lastItemsReceived.length,
value: currentPushedItems,
});
};
idleCallback = window.requestIdleCallback(() => {
idleCbFn();
});
} else {
currentPushedItems = lastItemsReceived;
observer.next({
morePending: false,
value: currentPushedItems,
});
}
},
});
return () => {
sourceSubscription.unsubscribe();
sourceSubscription = undefined;
lastItemsReceived = undefined;
currentPushedItems = undefined;
if (idleCallback) {
window.cancelIdleCallback(idleCallback);
idleCallback = undefined;
}
};
});
};
}
function sleep(milliseconds) {
var start = new Date().getTime();
for (var i = 0; i < 1e7; i++) {
if ((new Date().getTime() - start) > milliseconds){
break;
}
}
}
let testSource = of(
[1,2,3],
[1,2,3,4,5,6],
).pipe(
concat(NEVER)
);
testSource
.pipe(addAdditionalOnIdle(2))
.subscribe((list) => {
// Simulate a slow synchronous consumer with a busy loop sleep implementation
sleep(1000);
document.body.innerHTML += "<p>" + list.value + "</p>";
});
<script src="https://unpkg.com/rxjs#6.5.3/bundles/rxjs.umd.js"></script>

VueJS: how can i use two computed properties inside one v-for?

I have this computed property:
computed: {
filteredCars: function() {
var self = this
return self.carros.filter(function(carro) {
return carro.nome.indexOf(self.busca) !== -1
})
},
},
and i'm using v-for like this:
<tr v-for="carro in filteredCars">
<td>{{carro.nome}}</td>
<td>{{carro.marca}}</td>
<td>{{carro.categoria}}</td>
<td>{{carro.motor}}</td>
<td>{{carro.cambio}}</td>
<td>{{carro.preco}}</td>
</tr>
but I need to create another computed property to limit my data quantity, how i call it inside the same v-for?
I'm trying to use filteredCars + another filter, in this case something like 'limit' filter from vue 1.x. I've done an example using Vue 1.x but i need to do using Vue 2.x.
Vue.filter('limit', function (value, amount) {
return value.filter(function(val, index, arr){
return index < amount;
});
<tr v-for="carro in carros | limit upperLimit>
...
</tr>
Just use Array.prototype.slice (Array.prototype.splice should work too) in the computed property.
data: {
carros: [...],
upperLimit: 30
},
computed: {
filteredCars: function() {
const arr = this.carros.filter(function(carro) {
return carro.nome.indexOf(self.busca) !== -1
});
if (arr.length > this.upperLimit) {
return arr.slice(0, this.upperLimit + 1);
}
return arr;
},
}

Correct way to get a count of matching elements in Nightwatch?

I'm trying to test if a todo app has the right number of elements.
The docs seem to deal almost exclusively with single elements, so I had to use the Selenium Protocol functions. Would this be the right way to test the count of matching selectors (in this case, checking for 2 li elements)?
client.elements('css selector','#todo-list li', function (result) {
client.assert.equal(result.value.length, 2);
});
This works in my test, but I wasn't sure if there were gotchas around using a callback for this. Also not sure why Nightwatch doesn't have any helper functions dealing with more than one element.
I found the following very elegant solution within a VueJS template. It shows how to add a custom assertion into Nightwatch that counts the number of elements returned by a selector. See http://nightwatchjs.org/guide#writing-custom-assertions for details of how to write custom assertions within Nightwatch.
Once installed, usage is as simple as:
browser.assert.elementCount('#todo-list li', 2)
The plugin:
// A custom Nightwatch assertion.
// the name of the method is the filename.
// can be used in tests like this:
//
// browser.assert.elementCount(selector, count)
//
// for how to write custom assertions see
// http://nightwatchjs.org/guide#writing-custom-assertions
exports.assertion = function (selector, count) {
this.message = 'Testing if element <' + selector + '> has count: ' + count;
this.expected = count;
this.pass = function (val) {
return val === this.expected;
}
this.value = function (res) {
return res.value;
}
this.command = function (cb) {
var self = this;
return this.api.execute(function (selector) {
return document.querySelectorAll(selector).length;
}, [selector], function (res) {
cb.call(self, res);
});
}
}
This code was added to vuejs-templates by yyx990803 in 2016. So full credit goes to yyx990803.
Just to reassure you I do a similar thing when trying to grab all matching elements, ex:
browser.elements("xpath","//ul[#name='timesList']/h6", function(result){
els = result.value;
var i = 0;
els.forEach(function(el, j, elz){
browser.elementIdText(el.ELEMENT, function(text) {
dates[i] = text.value;
i++;
});
});
});
Alternatively, if you want to be sure that n number of elements exist, you can use a combination of :nth-of-type/:nth-child selectors and nightwatch's expect.
For example, if you want to test if #foo has nine direct children:
function(browser) {
browser
.url('http://example.com')
.expect.element('#foo > *:nth-child(9)').to.be.present;
browser.end();
}
Or if #bar has three direct article children:
function(browser) {
browser
.url('http://example.com')
.expect.element('#bar > article:nth-of-type(3)').to.be.present;
browser.end();
}
You could use expect.elements(<selector>).count():
browser.expect.elements('div').count.to.equal(10);
browser.expect.elements('p').count.to.not.equal(1);
I adapted Chris K's answer to support XPath expressions by using the built-in method this.api.elements:
exports.assertion = function elementCount(selector, count) {
this.message = 'Testing if element <' + selector + '> has count: ' + count
this.expected = count
this.pass = function pass(val) {
return val === this.expected
}
this.value = function value(res) {
return res.value.length
}
this.command = function command(callback) {
return this.api.elements(this.client.locateStrategy, selector, callback)
}
}
For usage instructions and credits see his answer
And if you like a bit of TypeScript, here is an assertion that will confirm the element count:
import { NightwatchCallbackResult, NightwatchAssertion, NightwatchAPI } from "nightwatch";
module.exports.assertion = function (selector: string, count: number, description?: string) {
this.message = description || `Testing if element <${selector}> has count: ${count}`;
this.expected = count;
this.pass = (value: number) => value === this.expected;
this.value = (result: number) => result;
this.command = (callback: (result: number) => void): NightwatchAPI => {
const self: NightwatchAssertion = this;
return self.api.elements(this.client.locateStrategy, selector, (result: NightwatchCallbackResult) => {
callback(result.value.length);
});
}
}
Use like this:
browser.assert.elementCount('body', 1, 'There is only one body element');

Resources