Resetting row heights causes incorrect row offset to get calculated - react-virtualized

Inside of an InfiniteLoader I have a List, whose rowRenderer callback creates a <div> wrapped by CellMeasurer (I'm also using a cache for that). The rows can vary wildly in size, but this setup is working well for scrolling purposes.
However, elements within each row can dynamically change in height based on user input. In those cases, I call:
cellMeasurerCache.clear(rowIndex, 0);
this._listRef.recomputeRowHeights(rowIndex);
But in some cases, this causes the rows to jump around -- this is because the top values are getting calculated incorrectly. For example, I'm displaying a document with the following rows rendered in List:
rowIdx height top
3 668px 420px
4 8547 1088
5 2420 9635
Then the height of row 4 increases slightly (~50px). After running the above code with rowIndex == 4, I see the following rows:
rowIdx height top
5 2420px 3088px
6 1156 5508
7 4718 6664
8 1050 11382
As you can see, row 5 now has an incorrect top value, causing the rows to jump around. I'm assuming that the height of row 4 did not get measured correctly.
What's wrong?
Update 2017-11-08: It appears that calling cellMeasurerCache.clear(rowIndex, 0) replaces the height of that row with the value of estimatedRowSize, which is very different. It doesn't seem to re-measure, contrary to what the docs say.
CellMeasurer._maybeMeasureCell() isn't called for row 4 after it gets the default height. My rowRenderer callback (which creates CellMeasurer around my content) is only invoked starting at row 5. So the code has made the decision of which rows to show with row 4 having the much smaller default height, instead of calculating beforehand.

Not sure if this is the correct/best way to handle this, but I was able to get this working like so:
First, I used the callback version of CellMeasurer, storing the measure callback for later use (stored per row):
rowRenderer = ({ key, index, style, isScrolling, parent }) => {
const getContent = measure => {
// store callback for later use
this._measureCallbacks[index] = measure;
// ... get your content ...
return content;
};
return (
<CellMeasurer
key={key}
cache={cellMeasurerCache}
columnIndex={0}
parent={parent}
rowIndex={index}
>
{({ measure }) => (
<div key={key} style={style}>
{getContent(measure)}
</div>
)}
</CellMeasurer>
);
};
Later, when a row height changes, instead of calling cellMeasurerCache.clear(), I run:
// tell row to re-measure
this._measureCallbacks[rowIndex]();
// tell list to recompute row offsets
this._listRef.recomputeRowHeights(minRow);

Related

How to show 0 as blank in JS Tabulator?

I have a table in Tabulator and would like to show all 0 values in the cells as blank. What's the best way to achieve this?
My first attempt was to write a custom mutator:
mutator: function(value, data, type, params, component){
if (value == 0){
return "";
}
else{
return value;
}
}
This works but feels wrong. Further, I would like to create calculated fields on top of this and this doesn't work properly with the blank strings. Then I need to re-convert them back to zeroes in the next function.
Thanks for your help
you can create a function that returns a copy of the original array but with all 0 values in the cells as blank then you display it, and each time you need to calculate or update you do it based on/in the original one then you always display the copy
Just use a cell formatter. A mutator will change the data but I don't think you want this - you just want to change the display of the data.
See my Codepen here and take a look at the moneyColFormatter function.
Specifically, in the cell definition:
...
formatter: (cell) => this.moneyColFormatter(cell),
and then implement it somehow
private moneyColFormatter(cell): string {
// If we want to show 0 values as blank:
if (!cell.getValue()) {
return "";
}
return cell.getValue();
}
The Codepen also contains aggregated functions, calculated cols and custom dynamic columns.

Tabulator:have more than one calculation at the bottom of a column

I have recently discovered Tabulator.js and its possibility to have e. g. the average of all values of a column at the bottom.
I wondered whether one can also have the minimum and maximum instead of just one thing. I did not find that in the docs, but it might be possible by extending the library? (I would not want to have one at the top, and as I need more than two calculations, that would not be a solution anyway.)
This is similar to another answer recently for the topCalc property.
You can put, effectively, whatever you want in a footer row. Use the bottomCalc and bottomCalcFormatter properties of the column definition along with a custom function to get the appropriate content.
In the custom function you can use bult in functions such as the table.getCalcResult() method.
eg: return an array of sum, average, count
let table = new Tabulator("#my-table-id", {
...
columns:[
...
{
title: 'My col',
...,
bottomCalc: function(v, d, p) {
// v - array of column values
// d - all table data
// p - params passed from the column definition object
let res = table.getCalcResults();
// eg Get sum and count for a column with id 'C1'
let c1_calc = 0, c1_count = 0;
d.forEach(function(row) {
c1_calc += row["c1"];
c1_count++;
});
return [
(parseInt(res.bottom["COL_1_ID"], 10) + parseInt(res.bottom["COL_2_ID"], 10)),
(parseInt(res.bottom["COL_1_ID"], 10) + parseInt(res.bottom["COL_2_ID"], 10)) / 2,
c1_calc,
c1_count
];
}
}
],
...
);
Once you have the content you can use a custom formatter to display it nicely.
See Custom Calculation Function and Calculation Results in the docs.
I have updated my Codepen again to add a bottomCalc function and formatter. See the definition for the AggregateFn column, the getBottomAggregate function that implements it and the bottomAggregateFormatter function that formats it.
Hope that helps.

SAP UI5 to Implement "go to" specific page on the table

I am new to SAP UI5 development. Currently the table is using "growing" and "growingThreshhold", then users can click more to see data of next page. Since we have thousands of data in that table, it takes user time to click more and more again to load next page data. we try to implement a function, that user can enter the page number then click a button and go to the specific page.
<Table id="genTable" growing="true" growingThreshold="60" fixedLayout="false" selectionChange="onHandleSelectChange"
backgroundDesign="Solid" updateFinished="onHandleGeneratorQueueUpdateFinished">
Expected UI:
I added a bar then UI display is good.
<Bar design="SubHeader">
<contentMiddle>
<Input type="Number" id="pageNumber" width="50px"></Input>
<Button id="goToButton" text="Go to" type="Emphasized" press="onHandleGoTo"></Button>
</contentMiddle>
</Bar>
For the backend logic, I refer to below articles, but still doesn't work.
https://blogs.sap.com/2016/12/14/sapui5-pagination-in-sap.m-table-on-button-click-using-odata-service/
https://sapyard.com/advance-sapui5-19-pagination-in-table-control-with-top-and-skip-query-options/
I tried to use read, the it can get the data back from odata service, but the data can't be refreshed in the table.
oModel.read("/ViewQueueSet", {
urlParameters: {
"$top": top,
"$skip": count
},
filters: [new Filter("RoleCode", FilterOperator.EQ, "G")],
useBatch: true,
success: function (tdata) { //successful Read in the server
var json = new JSONModel();
json.setData(tdata);
that.getView().setModel(json,"sapmodel");
sap.ui.core.BusyIndicator.hide();
},
error: function () {
sap.ui.core.BusyIndicator.hide();
}
});
}
also tried to call bindItems
//that.getView().setModel(json,"sapmodel");
//oTable.setModel(json); //JSON is preferred data format
//oTable.bindItems("/results",that.oGenQueueTemplate);
that.getView().byId("genTable").setModel(json);
that.getView().byId("genTable").bindItems("/results",that.oGenQueueTemplate);
Another approach I tried is to use bindItems, it call send the request to odata service, but it doesn't add the parameter top and skip parameter.
oTable.bindItems({
path: "/ViewQueueSet",
model: "sapmodel",
filters: [new Filter("RoleCode", FilterOperator.EQ, "G")],
template: this.oGenQueueTemplate,
// urlParameters: {
// "$top": top,
// "$skip": count
// },
parameters: {
"$top": top,
"$skip": count
}
});
Anyone has any idea about how to implement this functionality?
before I go into detail, please consider using other controls and/or ux patterns. imagine having thousands or millions of elements in backend and user equests to scroll to page 9292929 => for a responsive table (sap.m.Table) you would need to load all elements up to that page. Maybe filtering or some completely different approach could be tha right one.
The correct way to do this is by getting the listbinding and ask it to load more elements. how to ask the binding, may depend on the type of binding as well.
oTable = ... // get a reference on table
oItemsBinding = oTable.getBinding("items");
oItemsBinding.getLength() // will give you total number of elements
oItemsBinding.isLengthFinal() // will tell you if the length is final
oItemsBinding.getCurrentContexts() // will give you array of all loaded contexts.
now a few words to length and the length being final. If you have a binding implementation that knows the total number of objects (e.g. json - since it loads all elements to client, or OData, if cont is implemented in backend) then getLength will tell you the total number of objects.
if the backend doesnt have the count feature implemented, the length becomes final once you reach the end of the list (backend gives you less elements than you require - e.g. top=10,skip=90 returns 10 elements => length 100, not final; top=10,skip=100 returns 4 elements => length=104 becomes final)
Now, you can have a look at various binding implementations. But be aware that there is a lot to consider (direction of growing - upwards/downwards), at least you dont need to think about filtering/sorting - as this is part of the binding.
There is a nice (private) feature in sap.m.Table (or in sap.m.ListBase, to be more precise), which is called GrowingEnablement. you can use it like this:
// dont forget if _oGrowingDelagate is not undefined or similar
oTable._oGrowingDelegate.requestNewPage()
this will load one more page => you could start from reading the implementation of this method if you want to load several pages in one go.
you could also do a simple trick:
// assume you have 20 elements per page (default)
// and want to get to 7th page (elements 121 - 140)
// ckecks for 7th page exists and 7th page not yet loaded are omitted
oTable.setGrowingThreshold(70) // half of 140, so following load will load second page => 71 to 140
oTable._oGrowingDelegate.requestNewPage() // this will load the second page 71 - 140
// once loading is finished (take care of asynchronity)
oItemsBinding.attachEventOnce("dataReceived", function(oEvent){
// reset the growing threshold to 20
oTable.setGrowingThreshold(20)
// scroll to first element of 7th page (index 120, since count starts from 0)
oTable.scrollToInex(120)
})

unset scrollToIndex after setting it

I'm using an InfiniteLoader with a Table and I have the indices of some entries that match a specific criteria stored in state. If the user would want to go through those entries, I would have to pass down to the Table each index (following a button click) and use it in scrollToIndex. However, after it's passed down, I would have to make it null again, otherwise if the user were to scroll up/down the Table, he would always end to the same index specified by scrollToIndex instead of the current scroll position.
Is there a better way to do that, other than setting the state twice, once with an index and then with a null value?
Hope my question is clear.
Table (and Grid and List) also have public methods for setting a 1-time scroll index. For Table the method is scrollToRow and you just pass it the index you want to scroll to. Maybe that would be more to your liking?
Edit: In response to the follow-up question of how you get a reference to Table if you also need to pass it to InfiniteLoader:
<InfiniteLoader {...infiniteLoaderProps}>
{({ onRowsRendered, registerChild }) => (
<Table
{...tableProps}
onRowsRendered={onRowsRendered}
ref={(ref) => {
this._tableRef = ref // Store for yourself
registerChild(ref) // And pass on to InfiniteLoader
}}
>
{/* Columns ... */}
</Table>
)}
</InfiniteLoader>

Web2py: Incorrect row when representing a referenced field

It's rare that I find an issue that hasn't already be answered but I've been searching for this for 3 days and haven't found anything yet.
I'm aiming to create a page for inputing records in a 'spreadsheet' like format. I've used inline editing in SQLFORM.grid from this slice.
The problem I'm having is that when one of the fields is a reference to another table, the row being use in the lambda function is taking the row of the reference table rather than the row in the grid.
Here is an example:
Model
db.define_table('people',
Field('name', 'string'),
format = '%(name)s',
)
db.define_table('animals',
Field('name', 'string'),
Field('pet_owner', 'reference people'),
format = '%(name)s',
)
Controller
def index():
#process submitted form
if len(request.post_vars) > 0:
print request.post_vars
for key, value in request.post_vars.iteritems():
(field_name,sep,row_id) = key.partition('_row_')
if row_id:
db(db.animals.id == row_id).update(**{field_name:value})
db.animals.name.represent = lambda value,row: SQLFORM.widgets.string.widget(db.animals.pet_owner,value, **{'_name':'name_row_%s' % row.id})
db.animals.pet_owner.represent = lambda value,row: SQLFORM.widgets.options.widget(db.animals.pet_owner,value, **{'_name':'pet_owner_row_%s' % row.id})
grid = SQLFORM.grid(db.animals,
selectable= lambda ids : redirect(URL('animals',vars=request._get_vars)),
)
grid.elements(_type='checkbox',_name='records',replace=None) #remove selectable's checkboxes
return dict(grid=grid)
At first it appears that the grid is working correctly. However, when inspecting the drop-downs for the reference fields, if two consecutive rows have the same value (e.g. two animals with the same owner) the same row is used in the name (pet_owner_row_1) which means that the value being passed to process the submitted form is not an integer (e.g. 3) but as the integers separated by pipes (e.g. '|3|3|').
I've confirmed that this is where the issue is by changing the represent to
db.animals.pet_owner.represent = lambda value,row: row
which shows the exact same row data for different animals.
Here is an image showing the inspector on the form items: http://i.stack.imgur.com/oFtF8.png
How can I get the row id of the grid's row rather than the id of the reference?
Any help is greatly appreciated!
This is a bug that has recently been fixed in the master branch but not released yet. In the meantime, a workaround is to temporarily change the field to an integer type:
db.animals.pet_owner.type = 'integer'
db.animals.pet_owner.represent = lambda value,row: SQLFORM.widgets.options.widget(
db.animals.pet_owner,value, **{'_name':'pet_owner_row_%s' % row.id})

Resources