Split table data into multiple pages for printing - javascript

This is more of an architectural issue, but basically, I have a server-side React app that renders a bunch of charts and tables with page breaks in-between, so that a puppeteer instance can open the page then print, and send that printed report back to the user in another app.
I need to be able to take some data that is normally rendered into a table format on this app, but make it printable so that the data extends as far as possible before a page break is required, then renders a new table past the page break (so it appears on a new page when printing), and continues until all of the data is rendered into the report. Essentially, I need pagination on a table, without the user interaction that pagination usually comes with.
The thing I'm struggling with is that the length of the data is dynamic, and so are the widths and heights of the rows.
Any suggestions on how to tackle this? The only thing I can think of so far is to basically hide the table, and measure the height of it after every row is attached, and compare that to the max height (the height of a standard letter size in pixels), and if it exceeds it, remove the row, add a page break, then start a new table.
Thanks in advance.
EDIT:
FYI, The solution mentioned here doesn't apply: How to apply CSS page-break to print a table with lots of rows?
This needs to be an entirely new table because I have custom headers and footers that are going above and below it (showing metadata like the name of the chart, how many rows are shown out of how many total, etc.), so it can't just be one continuous table that's split.
Here's a codepen with a shell of what I'm trying to do. If you open in debug view, and print it, you'll see in the print preview that the table is split up across two pages, but the footer I created will only be on the second page (where it needs to be on both pages, after the table). Additionally, the footer needs to display the dynamic count of rows that were able to fit on the page, so it can't be a static part of the table as a tfoot element. https://codepen.io/nicholaswilson/pen/GRWNzMa
So I'm trying to figure out now if I can mount the table to the DOM, but hide it, and calculate the height as I add rows to it so I can try my original method above. But I'm also open to other suggestions.

Alright, I think I got it. Still needs some tweaking (and there are probably more performant ways to do it) but this is my concept: https://codepen.io/nicholaswilson/pen/abJpLYE. Currently I'm splitting the tables after they've exceeded the height, but I'll be fixing that later. The concept is here.
Basically, the idea is to build a 4D array to represent the instances of tables that need to be rendered. Then in componentDidMount() and componentDidUpdate(), I can add new tables to the state as needed:
componentDidMount() {
const { tableData } = this.props;
if (this.state.currentRowIndex === 0) {
// just starting out
this.setState((state, props) => {
const tables = state.tables;
tables.push([tableData.data[state.currentRowIndex]]); // push first new table and first row
return {
tables,
currentRowIndex: state.currentRowIndex + 1,
currentTableIndex: 0
};
});
}
}
componentDidUpdate() {
const { tableData } = this.props;
if (this.state.currentRowIndex < tableData.data.length) {
this.setState((state, props) => {
const tables = state.tables;
const currentTableHeight = this.tableRefs[this.state.currentTableIndex]
.clientHeight;
console.log(
`Table ${this.state.currentTableIndex} height: ${currentTableHeight}`
);
if (currentTableHeight > MAX_TABLE_HEIGHT_IN_PIXELS) {
// push a new table instead based on this condition
tables.push([tableData.data[state.currentRowIndex]]);
return {
tables,
currentRowIndex: state.currentRowIndex + 1,
currentTableIndex: state.currentTableIndex + 1
};
} else {
tables[state.currentTableIndex].push(
tableData.data[state.currentRowIndex]
); // push new row to existing table
return {
tables,
currentRowIndex: state.currentRowIndex + 1,
currentTableIndex: state.currentTableIndex
};
}
});
}
}
See the codepen for the rest of the implementation.

Related

Executing JavaScript on multi-row table rendering

I'm using a custom Primefaces-based framework to display a datatable, and it looks like that:
<xy:dataTable id="tableId" value="#{lazyTableBean.dates}" var="date">
<xy:column id="nameColumnId">
<xy:outputText id="nameOutputId" value="date.name"/>
</xy:column>
<xy:column id="actionColumnId">
<xy:actionButton id="actionButtonId" label="Button"
action="#{someBean.someAction(date.id)}"/>
</xy:column>
</xy:dataTable>
Now I want to set the tooltip of the button. Since the actionButton component of that framework doesn't have the title attribute, I'm using JavaScript to alter it:
var rows = // getting the table content row components here
// iterating through table rows and setting the button tooltip to the name of the corresponding date
for (const row of rows) {
var myTooltip = row.children.item(0).textContent;
row.children.item(1).firstChild.setAttribute("title", myTooltip);
}
This basically works as it should when I import the JS script at the end of the file.
However, there are several AJAX events (e.g. when sorting or filtering the table, or when using pagination...) that reprint the table content. Since the JS script isn't triggered again, the tooltips aren't set in that case.
Now I've planned to simply import the script at some appropriate place (e.g. inside the component that gets rerendered) so that it's executed whenever the button is rendered. However, I haven't found quite the right place to make it work. When I'm putting it inside the column:
<xy:dataTable id="tableId" value="#{lazyTableBean.dates}" var="date">
<xy:column id="nameColumnId">
<xy:outputText id="nameColumnId" value="date.name"/>
</xy:column>
<xy:column id="actionColumnId">
<xy:actionButton id="actionColumnId" label="Button"
action="#{someBean.someAction(date.id)}"/>
<h:outputScript library="js" name="addTooltipToTableButtons.js" />
</xy:column>
</xy:dataTable>
This results in only the first row to correctly set their tooltip, all other rows keep their generic one. But on AJAX events, the correct behavior takes place, all rows set their tooltip correctly. The same behavior takes place if the script is also imported at the end. I guess this has to do with the table format of dynamically printing a number of rows with the same column components, but this is just guessing.
Putting it inside the table (directly before </xy:dataTable>) results in no script execution at all.
I'm totally new to JavaScript and we're just using this approach until our custom framework supports setting arbitrary attributes. I hope you have an idea (or an explanation why it won't work like that) - thanks in advance!
Greetings
In case anyone's interested in my solution, I used a MutationObserver to handle the events, in addition to the "normal" JS at page load.
The whole JS file looked like that:
var table = ...; // get table by normal means
for (var i = 0, row; row = table.rows[i]; i++) {
var tooltip = row.cells[0].textContent;
row.cells[1].firstChild.setAttribute(tooltip);
}
var observer = new MutationObserver(function( mutations ) {
mutations.forEach(function( mutation ) {
var newNodes = mutation.addedNodes;
if( newNodes !== null ) {
var $nodes = $( newNodes );
$nodes.each(function() {
var tooltip = this.cells[0].textContent;
this.cells[1].firstChild.setAttribute(tooltip);
});
}
});
});
var config = {
attributes: true,
childList: true,
characterData: true
};
observer.observe(table.children.item(1), config);

Ag-grid - having expandable height cells causes rendering issues

I have an Ionic/Angular project where I wish to use the Ag-grid as a (virtual) container for my potentially very large list of items. I am very new the the ag-grid, I have played a little in the past, but still relatively new to it
Ag-grid is the has come the closest by far to being able to do what I am trying to achieve.
Basically I have a list of items, where I wish to use an Angular component in each cell, and this component has an "expander", with a varying number of sub items.
To get a component in the grid, I followed this excellent tutorial, which seemed to work perfectly.
It almost all works, but I find when I expand items down the list a bit (eg item position say 50), the UI starts to jump around, and not render properly. If you expand the first few, it is ok, it is only when you get down the list a bit I have problems.
I have a sample project here on Github.
It looks like the following...
My setup of the grid is in home.page.ts, where I have the following gridOptions
gridOptions: GridOptions = {
// Makes col take up full grid
onGridSizeChanged: () => {
this.gridOptions.api.sizeColumnsToFit();
} ,
getRowHeight(params) {
return params.data.getHeight();
},
getRowNodeId: function (data) {
return data.equipment;
},
}
The getRowHeight above will call ListItem.getHeight, and you can see this returns the height based on whether or not is it expanded...
public getHeight(): number {
let height = 72;
if (this.isOpen)
height += (this.jobs.length - 1) * 40;
return height;
}
Also, when the expander is clicked the refresh function passed into the constructor is called.
public setIsOpen(val: boolean) {
this.isOpen = val;
this.refresh();
}
And this call the following in the home.page.ts to try and refresh the grid cells..
private refreshCells(): void {
this.gridOptions.api.refreshCells({ force: true });
this.gridOptions.api.resetRowHeights();
}
And that's basically it.
It is close to working, ie click the initial items works, but when I scroll down, sometimes the view just does not refresh properly (sometimes it does, other times not).
Is this achievable, and if so, what can I do get get it to work better?

Ag-grid: duplicate node id 107 detected from getRowNodeId callback , this could cause issues in your grid

I am going to do live data streaming on ag-grid datatable, so I used DeltaRowData for gridOptions and added getRowNodeId method as well which return unique value 'id'.
After all, I got a live update result on my grid table within some period I set, but some rows are duplicated so I can notice total count is a bit increased each time it loads updated data. The question title is warning message from browser console, I got bunch of these messages with different id number. Actually it is supposed not to do this from below docs. This is supposed to detect dups and smartly added new ones if not exist. Ofc, there are several ways to get refreshed data live, but I chose this one, since it says it helps to persist grid info like selected rows, current position of scroll on the grid etc. I am using vanilla js, not going to use any frameworks.
How do I make live data updated periodically without changing any current grid stuff? There is no error on the code, so do not try to speak about any bug. Maybe I am wrong with current implementation, Anyway, I want to know the idea or hear any implementation experience on this.
let gridOptions = {
....
deltaRowDataMode: true,
getRowNodeId = (data) => {
return data.id; // return the property you want set as the id.
}
}
fetch(loadUrl).then((res) => {
return res.json()
}).then((data) => {
gridOptions.api.setRowData(data);
})
...
If you get:
duplicated node warning
it means your getRowNodeId() has 1 value for 2 different rows.
here is part from source:
if (this.allNodesMap[node.id]) {
console.warn("ag-grid: duplicate node id '" + node.id + "' detected from getRowNodeId callback, this could cause issues in your grid.");
}
so try to check your data again.
if u 100% sure there is an error not related with your data - cut oof the private data, create a plinkr/stackblitz examples to reproduce your issue and then it would be simpler to check and help you.

javascript ordering asyn functions

I'm currently modifying a open source project (link) to fit my need.
my English is bad, maybe it will be clearer if you can click on the link page, it will take half of minute to load, browser may seems to be freeze for a moment.
this page uses slickgrid , when hovering mouse over table rows , it will render details of that row on a fixed-position layer on page(the bottom left). there is only one such detail layer on the page, when you move your mouse to another row, this layer will change to show details of that row. following code is how it achieve this :
this.grid.onMouseEnter.subscribe( function (e) {}) // use slickgrid to set event listeners
// inside the handler function
from event e, get the row number row.rc
// then calls the staticOverlayDetails function do the actual work. it will renders the presentation layer.
window.setTimeout(function(){staticOverlayDetails(rc.row);}, 30);
so this staticOverlayDetails function will render the layer ( a div element ) to show the detail of row number rc.row, which is where mouse cursor is at.
I want to collect all the rendered result layer html code so I can combine them into a single page for easy read. that means, I want to hover my mouse on row one, wait for layer to be rendered, copy and save the div element, then move mouse to the next row , wait for the layer to be rendered with this new row , ...repeat until all row are done.
function staticOverlayDetails pseudo code:
function staticOverlayDetails (rown) {
//step 1:generate html text structure for the row, but leaves an element blank, this element is a text description for the row. the text will be loaded from a txt file
...
...
// html_text contains an `div` for description text which is currently empty, the text need to be fetched from a text file url, the text file name is based on the content of row. for the purpose of when the text is fetched, the callingback function need to know where to insert and avoid insert wrongly ( if the mouse has already moved to another row, the layer is showing details of another row, so the description text mismatch), so here it set a random string as the unique id of the description `div`, so later the `insert` function can use the id string as selector to insert text.
description_selector = random_id
html_text= html_code_generated
//step 2:
// this function fetches a , setting a callback function `insert` to insert the content.
url=generate text file url from row content
load_description( url, description_selector )
//step 3: present html
$(fixed-layer).html(txt)
//at this time , the description div is blank. the callback function will insert content into div when fetched.
}
function load_description (url, description_selector) {
function insert ( descrp_text ) {
$(description_selector).html(descrp_text).hide.slide_down()
}
$.get(url, insert, 'text')
}
my plan is to loop through rown to change the fixed layer, then dumps out the fixed layer :
for ( i=0; i < row_counts ; i++ ){
staticOverlayDetails(i)
console.log($(fixed_layer_selector).html())
but the async $.get() call makes this attempt impossible. because only when insert function finished , the content in layer is stable and ready to be dumped, if I use this loop, when $.get returns, the presentation layer is already showing later layers, I don't know how to wait for it.
how can I make my code waiting for the calling back function of $.get() to finish, so I can save the full details for the row, then continue to loop with the next row ?
according to link , I tried to modify the $.get request in the function load_content_for_an_element sync :
function load_description (url, description_selector) {
function insert ( descrp_text ) {
$(description_selector).html(descrp_text).hide.slide_down()
}
$.get({url:url, success:insert, sync:false});
}
but the browser make request to https://host.address/[object%20Object] and it returns with 404 error. seems it convert the passed object to string, then treat it as url to fetch.
some additional info:
1, because this project has several tables each with different rendering logic, I want to make the changes as small as possible, that's why I want to dump the generated presentation layer instead of changing the generating code itself. all different tables render the same layer, that's the easiest way to do it.
2, I want to dump the code to generate an page to read, only need to dump once , so there is no worries about performance. actually I can use a mouse macro to do it , but I think that's too dumb, I want to learn the javascript way to do it :)
I'm so sorry my English is bad and I want to express such a complex situation, so please ask for more infos if my words are not clear, I'm so sorry to see other people waste time because of my mistake words...
Promises are your friend. Here's an example of how you would structure your code to use them:
// Set up a staging area
var staticOverlays = [];
loadStaticOverlayDetails(index) {
return $.get(url, insert, data....).then(function (data) {
staticOverlays[index] = data;
});
}
// Request your data
var promises = [];
for ( var i=0; i < 100; i++ ) {
promises.push(loadStaticOverlayDetails(i));
}
// Wait for the data to complete loading
$.when(promises).then(function insert() {
for ( i=0; i < staticOverlays.length; i++ ) {
var overlay = staticOverlays[i];
// step 1:generate html text structure for the row, but leaves an element blank
var txt = html_code_generated
// insert data into element in fixed layer;
$(fixed-layer).html(txt);
}
});
As an aside, if you're really making 100+ http requests you should consider moving some of this logic to the server because that many http requests will hurt your user experience...

Kendo UI Grid Excel Export exporting twice

I wrote up some javascript code to export an excel grid that would only export the records filtered on the page by the filter tab and not the total value of records brought back. So for example if the record pulls back 23 records but you only have the view set to 10 you'll only get 10 records exported. I'm loading this function on the grid initialization as I usually do, however, for some reason it always seems to print out an empty sheet first with just the header rows, followed by the actual sheet I need. So the code is working but I can't figure out how to stop the first empty sheet from downloading as well. I don't believe I'm calling the method twice and there's definitely no looping logic... I've read over the Telerik documentation and I figured I'd come to stackoverflow since the Telerik support team just seems to refer everyone back to the same documentation
excelExport: function (e) {
var items = grid.dataSource.pageSize();
sheet = e.workbook.sheets[0];
var newSheet = sheet.rows.splice(0, items + 1) // +1 is for the header row
sheet.rows = newSheet
},

Categories