Get table inside a div container - puppeteer

I'm trying to get a table inside a div this is what it looks like in html structure
<div class="table_container">
<table id="main_table">
</table>
</div>
let pageContent = await page.evaluate(() => document.getElementsByClassName("table_container")[0].getElementsByTagName("table")[0].innerHTML);
but this is not working. What's wrong with my code

Your code is "correct", so I will try to explain what is happening here, hope this help you. Looking to your selector:
document.getElementsByClassName("table_container")[0].getElementsByTagName("table")[0].innerHTML
<div class="table_container">
<table id="main_table">#
##</table>
</div>
The result of this will be a string with some spaces (marked as #), or even nothing if the html not have spaces between tags. You are getting this result because you get the first div.table_container and then you get the table at index [0] and then the innerHTML of a empty table. You can add some content to table to retrieve a more "concrete" result, like adding a body to table:
<div class="table_container">
<table id="main_table">
<tbody>
<tr>
<td>Foo</td>
</tr>
</tbody>
</table>
</div>
Result
<tbody>
<tr>
<td>Foo</td>
</tr>
</tbody>
Puppeteer code
const puppeteer = require("puppeteer");
(async function() {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto("http://localhost:5000"); // used a simple npx serve with a static html file
const content = await page.evaluate(() => {
return document.getElementsByClassName("table_container")[0].getElementsByTagName("table")[0].innerHTML;
});
console.log(content);
await browser.close();
})();

Related

How to apply fixed-header prop to v-table if height of 100vh is not possible?

I'm using Vuetify and have some content above a table. The table should have a fixed-header but this requires a height prop. A height of 100% didn't work and I can't use 100vh because I have some content above.
Reproduction link
<template>
<v-app>
<v-main>
<v-alert type="info" title="some other content goes here" />
<v-table :fixed-header="true" height="100%"> <!-- height=100% doesn't work -->
<thead>
<tr>
<th>Header</th>
</tr>
</thead>
<tbody>
<tr v-for="i in 100">
<td>x</td>
</tr>
</tbody>
</v-table>
</v-main>
</v-app>
</template>
Do you have any ideas how to setup a fixed header with a correct height?
I think the answer of Igor Moraru is almost correct. The problem is that it is not able to deal with new table rows joining the table.
Reproduction link
You'll have to compute the table height anyway. One way is to substract the alert height from the container height. What is left is the table height.
<template>
<v-app>
<v-main ref="maincontainer">
<v-alert ref="alert" type="info" title="some other content goes here" />
<v-table :fixed-header="true" :height="height">
<thead>
<tr>
<th>Header</th>
</tr>
</thead>
<tbody>
<tr v-for="i in 100">
<td>x</td>
</tr>
</tbody>
</v-table>
</v-main>
</v-app>
</template>
<script>
import {ref, onMounted} from "vue"
export default {
setup(){
let maincontainer = ref(null)
let alert = ref(null)
let height = ref('500px')
onMounted(() => {
let mainHeight = maincontainer.value.$el.clientHeight
let alertHeight = alert.value.$el.clientHeight
height.value = `${mainHeight - alertHeight}px`
})
return {
maincontainer,
alert,
height
}
}
}
</script>
Fixed reproduction link
UPDATE:
For a more generic solution (independent of the intermediate elements between the table and the container) you can get the position of the table into the viewport (using getBoundingClientRect) and calculate the top and bottom offsets, then subtract them from the viewport height. This way you don't care about what elements are above or below the table but only about the space that they require.
Note: For this solution to work, initially the table should not have a fixed height (or 100% height).
UPDATE:
Refactored the solution to allow recomputing the table height on demand.
The main points to consider: using a computed is not applicable for this situation because the css props are not reactive in Vue. Instead make the computing in a dedicated function and call it when needed (you can trigger it even from a window resize watcher).
Used the OP setup for a more focused solution.
<script setup lang="ts">
import { ref, computed, onMounted, nextTick } from "vue";
import TheAlert from "./TheAlert.vue";
const props = defineProps<{ mainContainer: VMain }>();
const tableComponent = ref<VTable>();
const values = ref([]);
const tableHeight = ref(null)
const refreshTableHeight = async () => {
if (!tableComponent.value) {
tableHeight.value = null;
}
// Unset table height and wait for it to rerender
tableHeight.value = null
await nextTick()
// Recompute the table height with new data
const mainRectangle = props.mainContainer.$el.getBoundingClientRect();
const tableRectangle = tableComponent.value.$el.getBoundingClientRect();
const topOffset = tableRectangle.top;
const bottomOffset = mainRectangle.bottom - tableRectangle.bottom;
tableHeight.value = window.innerHeight - bottomOffset - topOffset;
}
function insertNewRow() {
values.value.push(new Date());
refreshTableHeight()
}
onMounted(() => {
refreshTableHeight()
})
</script>
<template>
<the-alert />
<v-container>
<v-btn #click="insertNewRow">Insert new row</v-btn>
</v-container>
<v-table ref="tableComponent" fixed-header :height="tableHeight"> <!-- Do not format here the height to allow for null values -->
<thead>
<tr>
<th>Header</th>
</tr>
</thead>
<tbody>
<tr v-for="v in values" :key="v">
<td>{{ v }}</td>
</tr>
</tbody>
</v-table>
</template>
FIXED reproduction link.
You can simply achieve this by calculating the height on the fly using calc() function. You have to calculate the pixels of the elements that are above the v-data-table and then subtract it from 100vh.
height="calc(100vh - 60px)"
In Vue - You can get the element height dynamically by attaching a ref to it and then access the clientHeight.
this.$refs.alertBox.$el.clientHeight
This will return 60 as per the alert box component you added.
Live Demo :
const { createApp } = Vue
const { createVuetify } = Vuetify
const vuetify = createVuetify()
const app = createApp({
template: '#app-template'
}).use(vuetify).mount('#app')
<script src="https://unpkg.com/vue#next/dist/vue.global.js"></script>
<script src="https://unpkg.com/#vuetify/nightly#4.0.0-next-20230205.0/dist/vuetify.js"></script>
<link rel="stylesheet" href="https://unpkg.com/#vuetify/nightly#4.0.0-next-20230205.0/dist/vuetify.css"/>
<script type="text/x-template" id="app-template">
<v-app>
<v-alert type="info" title="some other content goes here" />
<v-table fixed-header height="calc(100vh - 60px)">
<thead>
<tr>
<th>Header</th>
</tr>
</thead>
<tbody>
<tr v-for="i in 100">
<td>x</td>
</tr>
</tbody>
</v-table>
</v-app>
</script>
<div id="app"></div>

Can I print a table with spanning rows and columns without overlapping borders?

I have an issue, that I'm trying to use an HTML page to generate a table, which is then converted to a PDF for printing. The issue is that the design of the table includes row spanning td elements and column spanning headers which gives weird behaviour on page break.
Specifically when there is a page break, the table headers are repeated (as expected), but if the break is in the middle of one of the row spanning td elements then rest of the td element is drawn over the column spanning header on the new page:
Below is a simple example that will allow you to replicate the issue (javascript used just for the convenience of building up a large amount of repeat data, the issue occurs even without javascript involvement).
const numRowGroups = 15;
const rowSpan = 5;
const tableBody = document
.getElementById("example-table")
.getElementsByTagName("tbody")[0];
const createMergeColumn = (listEntries) => {
const mergeColumn = document.createElement("td");
mergeColumn.rowSpan = rowSpan;
const list = document.createElement("ul");
listEntries.forEach((text) => {
const listItem = document.createElement("li");
listItem.textContent = text;
list.appendChild(listItem);
});
mergeColumn.appendChild(list);
return mergeColumn;
};
const createFirstMergeColumn = () =>
createMergeColumn(["Lorum", "Ipsum", "Dolor", "Sit", "Amet"]);
const createSecondMergeColumn = () => createMergeColumn(["Foo", "Bar", "Baz"]);
const createThirdColumn = () => {
const column = document.createElement("td");
column.textContent = "Some text here";
return column;
};
[...Array(numRowGroups)].forEach(() => {
const firstRow = document.createElement("tr");
firstRow.appendChild(createFirstMergeColumn());
firstRow.appendChild(createSecondMergeColumn());
firstRow.appendChild(createThirdColumn());
tableBody.appendChild(firstRow);
[...Array(rowSpan - 1)].forEach(() => {
const row = document.createElement("tr");
row.appendChild(createThirdColumn());
tableBody.appendChild(row);
});
});
console.log(tableBody);
<!DOCTYPE html>
<html>
<head>
<title>Example issue with printing tables</title>
<meta charset="UTF-8" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap#4.6.1/dist/css/bootstrap.min.css"
integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn"
crossorigin="anonymous"
/>
</head>
<body class="container">
<div class="row">
<div class="col">
<table id="example-table" class="table-bordered w-100">
<thead>
<tr>
<th colspan="3" class="bg-primary">
Spanning column with long text to demonstrate my point
</th>
</tr>
<tr>
<th>First column</th>
<th>Second column</th>
<th>Third column</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</body>
</html>
What I'm looking for is a way to not have any part of the content on the previous page drawn over the headers on the next page. I'm happy if the solution involves page breaking earlier (at the start of the row spanning elements for example), or there is a solution that just gives better rending with page breaks in the locations they are currently.

Extract a value from a HTML response with Cheerio using Postman

I'm trying to get a value from request, that returns a HTML response, using Postman. I'm using Cheerio inside the script section.
The response looks like this:
<table class="etlsessions-table" cellpadding="0" cellspacing="0">
<thead>
<tr>
<th class="someclass1">
<div>
<span>info1</span>
</div>
</th>
<th class="someclass2">
<div>
<span>info2</span>
</div>
</th>
<th class="someclass3">
<div>
<span>info3</span>
</div>
</th>
<th class="someclass2">
<div>
<span>info4</span>
</div>
</th>
</tr>
</thead>
<tbody>
<tr class="someclass5">
<td class="someclass">
<nobr>info5</nobr>
</td>
<td class="someclass6">
<nobr>info6</nobr>
</td>
<td class="someclass3">info7</td>
<td class="someclass7">
someurl1
</td>
</tr>
</tbody>
</table>
How I can get the info6 value from the someclass6 class?
As Cheerio is built-in to the Postman sandbox environment, you can use it to get the value of the element.
I'm not sure of your complete use-case but you could add something basic like this to the Tests script and print the value to the console:
const $ = cheerio.load(pm.response.text()),
elementValue = $('.someclass6 nobr').text();
console.log(elementValue)
To emphasis Danny was saying
Use the jQuery selector API to get different elements on the page reading dom
const $ = cheerio.load(pm.response.text());
console.log $('.someclass6 nobr').text(); //derive any element from class
which has value someclass6
Or you can write it like this
console.log($("td[class$='someclass6'] nobr").text()); //derive any text value within the td tag from the class which has the value someclass6
console.log($("td").attr('class')) //to fetch values from the attribute(s) of tag td
To store it as a collection variable in postman for useage in other api calls
const $ = cheerio.load(pm.response.text());
var storesomeclass6 = $('.someclass6 nobr').text();
pm.collectionVariables.set("someclass6", storesomeclass6 );
Postman is a software that will allow you to make calls to API endpoints, so basically your program will be written in node.js and you will call the endpoint with postman.
In this case and using cheerio, the code will look like something like this :
function getResponse() {
return fetch(`${YOUR API END POINT URL}`)
.then(response => response.text())
.then(body => {
const $ = cheerio.load(body);
const $info6= $('.someclass6 td );
const info6= $info6.first().contents().filter(function() {
return this.type === 'text';
}).text().trim();
const response= {
info6,
};
return response;
});
}
Best of luck !

send data from two models of Mongoose inside Express.js to display in view list data

I have a Shopping Website I want to be able to display two model names "jeans" and "shirt" in a view
I need to pass data from two models I know how to access data from two models in one controller but I do not know how to send data from that controller
Controller:
var mongoose = require('mongoose'),
errorHandler = require('./errors.server.controller'),
Shirt = mongoose.model('Shirt'),
Jeans = mongoose.model('Jeans');
exports.list = function(req, res) {
Jeans.find().sort('-created').populate('user', 'displayName').exec(function(err, jeans) {
Ajean = jeans;
});
Shirt.find().sort('-created').populate('user', 'displayName').exec(function(err, shirts) {
Ashirt = shirts;
});
Shirt.find().sort('-created').populate('user', 'displayName').exec(function(all) {
// res.jsonp(Ashirt);
// res.jsonp(Ajean);
});
};
View:
<div class="list-group">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Name</th>
<th>Color</th>
</tr>
</thead>
<tbody>
<tr data-ng-repeat="item in items">
<td data-ng-bind="item.name"></td>
<td data-ng-bind="item.color"></td>
</tr>
</tbody>
</table>
</div>
I know that I cannot use "res.jsonp();" more than once .
when I use "Ajean" it give me data for "jeans" and
when I use "Ashirt" it give me data from "shirts" model
But I want to be able to show both data from both "shirt" model and "jean" model
How Can I do That?
Do I need to merge two Json?
How should I change my view to see that data?
Thanks!
You could try nesting the queries and then merge the resulting arrays with Array.concat:
Jeans.find().sort('-created').populate('user', 'displayName').exec(function(err, jeans) {
Shirt.find().sort('-created').populate('user', 'displayName').exec(function(err, shirts) {
var all = shirts.concat(jeans);
res.jsonp(all);
});
});

parse html within ng-repeat

I've been searching for hours now and I can't seem to find how to parse HTML code when retrieving using ng-repeat. I checked out $sce.trustAsHtml but I don't know how to apply it in my code.
html file:
<div ng-app="myApp">
<div ng-controller="MyCtrl">
<h2>My Links</h2>
<table class="table table-bordered">
<thead>
<tr>
<td>Name</td>
<td>URL</td>
</tr>
</thead>
<tbody>
<tr ng-repeat="stuff in myStuff()">
<td>{{stuff.name}}</td>
<td>{{stuff.url}}</td>
</tr>
</tbody>
</table>
</div>
javascript
var myApp = angular.module('myApp', []);
myApp.controller('MyCtrl', function ($scope) {
$scope.myStuff = function() {
return [
{
'name':'Google',
'url':'Google'
},
{
'name':'Yahoo',
'url':'Yahoo'
},
{
'name':'Microsoft',
'url':'Microsoft'
}
];
};
});
It's displaying the HTML source but I want it compile the code. Is my JS approach wrong? I'll be applying it to a json file using $http directive once I figure this out. Can anyone shed some light? I have my code at http://jsfiddle.net/s2ykvy8n/
Thanks.
Include ng-sanitize script and in your module add dependency.
var myApp = angular.module('myApp', ['ngSanitize']);
and just use ng-bind-html
<td ng-bind-html="stuff.url"></td>
and you are good to go:-
Plnkr
With what you are doing, the html in the binding will be displayed as textcontent in the element while processed by interpolation directive.
My first inclination (since from your example, you are just returning links) is to advise you to remove the html from your returned json and just return the url as a string.
Format:
{
'name': 'Google',
'url': 'http://google.com'
}
Then your HTML looks like this,
<tbody>
<tr ng-repeat="stuff in myStuff()">
<td>{{stuff.name}}</td>
<td>{{stuff.name}}</td>
</tr>
</tbody>
But, if you MUST have HTML strings in your json file, I would take a look at $compile. There are examples towards the bottom that can help you on how you can create a directive to compile your 'url' string to HTML