Executing JavaScript on multi-row table rendering - javascript

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);

Related

Split table data into multiple pages for printing

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.

multiple kendo grids using one script

I'm loading multiple partial views into the same cshtml page. All goes well until they need to use the scripts. As i'm using code like
var grid = $("#grid").data("kendoGrid");
var selected = grid.selected();
This code works fine with one grid, but starts showing issues when multiple grids are in place. The problem is that "#grid" is a reference to the name of the kendo grid. Is there a way to make this dynamic so it can be used by multiple grids?
I think the same problem would occur when there are multiple grids in the same page as it can't distinct what grid to refer to. Giving the grids different id's would work, but then the code in the script will return an undefined error on grid.selected().
Update:
So the solution of using
var grid = $(".k-grid").data("kendoGrid");
works to a certain point. It loads the data into the grid, but fails to do anything else. For example a part of my code for enabling an update and delete button doesn't work on the 2nd and 3rd partial view.
var grid = $(".k-grid").data("kendoGrid");
var selected = grid.select();
if (selected.length > 0) {
$("#btnCopy,#btnEdit,#btnDelete").removeClass("k-state-disabled");
} else {
$("#btnCopy,#btnEdit,#btnDelete").addClass("k-state-disabled");
}
Somehow the code only starts working for grid 2 and 3 after i have selected a row on grid 1, which is not as intended.
Instead of id(#Grid) you can use class(.k-grid):
var grid = $(".k-grid").data("kendoGrid");
The solution I found with help of a senior programmer is to save the grid data into a global variable like this.
var PartialGridData = PartialGridData || {};
After that I'm setting and changing the variable whenever changing the partial view.
PartialGridData.selectedGrid = $("#PartialGrid1").data("kendoGrid");
Where the name #PartialGrid1 is the name of the current grid.
This means I need to write this code as many times as I have grids, but it also fixes a lot of problems. After that I use it to select the correct data.
var grid = PartialGridData.selectedGrid;
var selected = grid.select();
if (selected.length > 0) {
$("#btnCopy,#btnEdit,#btnDelete").removeClass("k-state-disabled");
} else {
$("#btnCopy,#btnEdit,#btnDelete").addClass("k-state-disabled");
}
Another option would be to use e.sender.
function onRowSelect(e) {
var grid = e.sender;
var selected = grid.select();
if (selected.length > 0) {
$("#btnCopy,#btnEdit,#btnDelete").removeClass("k-state-disabled");
} else {
$("#btnCopy,#btnEdit,#btnDelete").addClass("k-state-disabled");
}
}
Both solutions have their drawbacks though. Not all methods get the variable e used for e.sender and changing partial views in a way that is not caught will cause the global variable not to be updated, so this has to be kept in check.

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...

jQuery DataTables using filtered Ember Data - Column widths don't resize

Little background information first:
I have an Ember component that uses a Handlebars {{#each results as |result|}}{{/each}} to populate a tbody element with a bunch of rows that contain all the data in the results.
Above this table, there is a toggle that sets whether the table should show all the entries or only the ones that have isEnabled set to true. This toggle sets the viewAll component property. When the toggle is flipped, the data changes correctly but the columns in the Datatable extend off the page (as seen below).
I've tried setting and observer on the viewAll property so that every time it was changed I called draw(). I've tried calling resultsTable.columns.adjust().draw(), but to to avail.
Anyone have any idea, what's going on?
For reference the environment uses Bootstrap 3, Ember 2.6.0, Datatables 1.10.12, and jQuery 2.2.4.
Here is the template (the table exists inside a Bootstrap panel-body):
<tbody>
{{#each results as |result|}}
<!--Ember truth-helpers used here-->
{{#if (or (and (not viewAll) result.isEnabled) viewAll)}}
<tr data-id={{result.id}}>
<td>{{result.time}}</td>
<td>{{result.a}}</td>
<td>{{result.b}}</td>
<td>{{result.c}}</td>
<td>{{result.d}}</td>
<td>{{result.e}}</td>
<td>{{result.f}}</td>
<td>{{result.g}}</td>
<td>{{result.h}}</td>
</tr>
{{/if}}
{{/each}}
</tbody>
Here is the component code (inside the didInsertElement hook):
Ember.run.scheduleOnce('afterRender', this, function() {
resultsTable = this.$("#tbl_results").DataTable(
{
"responsive":true,
"autoWidth": true,
"select": true
}
);
});
And the screenshot:
Screenshot showing row overflow
Any advice is much appreciated. Thanks!
So, as it stands now, with all of the comments ruled out, I've landed at this solution (as stated in one of my comments).
I placed an observer on the results set, and the viewAll property updated by the toggle:
updater : Ember.observer("results.#each", "viewAll", function(){
recreateTable(this.get("viewAll"), this.get("results"));
}),
That calls: (Which destroys the table and recreates it. Terribly inefficient, but the results set us usually decently small, and it's what I'm stuck with for now)
function recreateTable(viewAll, results){
let data = null;
if(resultsTable) {
resultsTable.clear(); // Clear first to avoid dupe data
resultsTable.destroy();
}
if (viewAll) {
data = results;
} else {
data = results.filterBy("isEnabled", true);
}
let dataArray = [];
data.forEach( function(item){
dataArray.push([
"<time class='timeago' datetime='" + item.get("time") + "'>" + item.get('time') + "</time>",
item.get("a"),
item.get("b"),
item.get("c"),
item.get("d"),
item.get("e"),
item.get("f"),
item.get("g"),
item.get("h")
]
);
});
resultsTable = Ember.$("#tbl_results").DataTable(
{
"aaData": dataArray,
"responsive":true,
"autoWidth": true,
"select": true
}
);
Ember.$("time.timeago").timeago();
}
For those curious, timeago is a neat little jQuery plugin that formats times as "3 minutes ago" instead of just lame time stamps. (Even SO uses it). It can be found here.

Sort ajax data in table with tablesorter.js

I'm trying to use tablesorter.js but im running into issues when using ajax to update my table. I followed the code on this page but it doesn't seem to be working properly. One thing i also notice is that the code doesnt work properly even on the example website. When i click "append new table data" it adds the data to the table but it isn't sorting it correctly. If i copy the javascript code and paste it into the console, it works fine and sorts the table correctly. The code im using is the following:
var updateTableSort = function(){
var table = $('#transaction-table')
//tells table sorter that table has been updated
table.trigger("update");
//re sorts after table has been updated based on
//current sort patern
var sorting=table.get(0).config.sortList;
table.trigger('sorton', [sorting]);
}
Again, if i copy and past this into console it works fine, but when i have it in my success ajax function, it doesnt sort the table properly. Any help figuring out what the issue is would be greatly appreciated
Try my fork of tablesorter. It automatically resorts the table after an update:
// pass anything BUT false and the table will resort
// using the current sort
$("table").trigger("update", [false]);
Here is the updated append data to the table using ajax demo:
$(function() {
$("table").tablesorter({ theme : 'blue' });
$("#ajax-append").click(function() {
$.get("assets/ajax-content.html", function(html) {
// append the "ajax'd" data to the table body
$("table tbody").append(html);
// let the plugin know that we made a update
// the resort flag set to anything BUT false (no quotes) will
// trigger an automatic
// table resort using the current sort
var resort = true;
$("table").trigger("update", [resort]);
// triggering the "update" function will resort the table using the
// current sort; since version 2.0.14
// use the following code to change the sort; set sorting column and
// direction, this will sort on the first and third column
// var sorting = [[2,1],[0,0]];
// $("table").trigger("sorton", [sorting]);
});
return false;
});
});

Categories