docx
docx copied to clipboard
Unable to apply keepNext to vertically merged table cells
I have a table of grouped rows. So, imagine a table with 10 rows. The first column has a vertically merged cell across 5 rows and a second vertically merged cell across the remaining 5 rows. The rest of the columns don't have any merged cells. My goal is to specify "keepNext" on the paragraphs contained within cells in rows 1-4 and 6-9. This prevents a page break in the middle of a group of rows. Pages may only break between groups - assuming a group can fit on a page.
The only problem is, I only define one TableCell object with a "rowSpan" of 5 in that column per group. I don't actually define TableCell objects for the additional merged cells. When I open up the XML in the zipped word document, I see that only the first cell (vMerge=restart) has a paragraph with a "keepNext" property. The remaining 4 merged cells (vMerge=continue) do not contain a paragraph, much less a "keepNext" property. What would be the best way to apply a keepNext to the next 3 cells? When I modify the XML manually, it has the desired effect.
I'm using docx version 5.3.0.
This may be a bug, need investigation, so horizontally merged cells have keepNext?
This may be a bug, need investigation,
I probably wouldn't characterize this as a bug. Maybe a design flaw in the new version of docx unless you can help me find a means to accomplish my end. In the previous major version of docx, we added the paragraph to all the vertically merged cells, but only the first cell had text content and all but the last cell's paragraph had "keepNext". Since "keepNext" is a paragraph property, it is awkward to design a generic solution. The solution you had before was to grant full control over each individual cell in the merged set.
Lacking full control, a developer-friendly alternate solution would be to add a "keepNext" option to a TableRow object. Under the hood, you would add that property to all paragraphs in all cells in that row or (for empty, "continue", vertically merged cells) add a paragraph with that property. This would effectively prevent a page break between such rows and the next row.
so horizontally merged cells have keepNext?
Horizontally merged cells work entirely differently. Instead of "vMerge" being applied to multiple merged cells. You just have one cell with a "gridSpan" property. This effectively means all horizontally merged cells have the one "keepNext" paragraph, which works fine for my purposes.
I just did a little experimenting in Microsoft Word. I think I am wrong in one little detail. The "keepNext" does not have to be applied to all paragraphs in all cells in a row. It just needs to be applied to all paragraphs in the FIRST cell in a row (and there must be at least one paragraph in that cell). So the only reason I'm seeing a problem is because my rowSpan is on the first column. If it was the 2nd or later column, it wouldn't have any impact on my ability to keep the rows together.
But this is just a little detail. Conceptually, I still think it makes sense to add a TableRow option called "keepNext" that will do the right thing under-the-hood to keep the next row on the same page as the current row.
For those that are experiencing this same problem, I came up with a workaround based on the discovery that only the first column controls whether rows in a table will stick together when using "keepNext".
Context First of all, the table should have a "fixed" layout. And, I assume the first column in your table has cells with "rowSpan" since that is the only way you can experience this "bug" or design limitation in docx 5.x. I'm also assuming that you want all the rows included by that rowSpan to stay on the same page (basically, you don't want the rowSpan cell to be split across pages).
Workaround Insert a new column at the beginning of the table to the left of the column where you have cells with rowSpan set. All of the cells should have these properties:
- No borders
- No margins
- No background color (or "white")
- No rowSpan
- A width with a size of "1". (apparently 0 doesn't do the expected thing in MS Word). I also pass "columnWidths" to the docx.Table object with a size of "1" for the first column.
This means the newly inserted first column in your table will be invisible and take up almost no space. Now, you can add an empty paragraph with "keepNext" set for all except the last cells in this column for merged cells in the 2nd column.
@dolanmiu I just discovered another example of the design problem with docx 5.x. So, it is not just limited to "keepNext". Basically, I was trying to override the cell margins for a whole row to be different from the default table margins. But I couldn't, because of vertically merged cells. The workaround is to leave the default table margins at 0 and just do cell-level margins. This time I'll provide some code and an example docx file.
This was possible in docx 4.x by setting the margins for the first cell on the 2nd row before merging them. The same can be done in MS Word. Just gotta split the merged cell apart, set the margins, and merge them together again.
const fs = require('fs');
const docx = require('docx');
const { Document, Packer, Paragraph, Table, TableCell, TableRow } = docx;
const doc = new Document();
const table = new Table({
margins: {
top: 720,
right: 720,
bottom: 720,
left: 720,
},
rows: [
new TableRow({
children: [
new TableCell({
rowSpan: 2,
children: [new Paragraph("I like big margins")],
}),
new TableCell({
children: [new Paragraph("Not gonna lie")],
}),
],
}),
new TableRow({
children: [
// Can't touch the merged cell to make it zero top/bottom margin
new TableCell({
children: [new Paragraph("No margins plz")],
margins: {
top: 0,
right: 0,
bottom: 0,
left: 0,
},
}),
],
}),
],
});
doc.addSection({
children: [table],
});
Packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync(`example.docx`, buffer);
});
Thanks for all this investigation
If you can, could you submit a PR to try fix this?
@dolanmiu We haven't even agreed on a solution yet. There are multiple ways to solve this including reverting to the approach used in docx.js 4.x. But, if I were to select, IMHO, a good solution that is compatible with the declarative, read-only objects design used in docx.js 5.x and would not be a breaking change, then I would go with this example code. It has 6 rows, 2 columns where the last 3 rows are exactly like the first 3. The first cell is vertically merged across all 3 rows. We attempt to prevent page breaks that would split the vertically merged cells. We also want the 2nd/5th rows to override table margins.
Pay particular attention to 2 new TableRow arguments: keepNext, margins. The behavior of these new arguments are defined below the code block.
const table = new Table({
margins: {
top: 720,
right: 720,
bottom: 720,
left: 720,
},
rows: [
new TableRow({
keepNext: true,
children: [
new TableCell({ rowSpan:3, children:[ new Paragraph("I like big margins") ] }),
new TableCell({ children:[ new Paragraph("Not gonna lie") ] }),
],
}),
new TableRow({
keepNext: true,
margins: { top:0, right:0, bottom:0, left:0 },
children: [
new TableCell({ children:[ new Paragraph("No margins plz") ] }),
],
}),
new TableRow({
keepNext: false,
children: [
new TableCell({ children:[ new Paragraph("Margin call!") ] }),
],
}),
... repeat ...
],
});
- keepNext This TableRow constructor argument is NOT an official Office Open XML property on a table row. It is a paragraph property in the XML. But just like the paragraph property does with the next paragraph, it will ensure that the next row in the table is on the same page as the current row, if possible. When serialized to XML, the first cell in the row will be guaranteed to have at least one paragraph (an empty one will be added if none exist). All paragraphs in the first cell in the row will or won't have a "keepNext" property depending on the provided value.
- margins This TableRow constructor argument is NOT an official Office Open XML property on a table row. It is, however, a cell property in the XML. When serialized to XML, these margins will be merged into all cells in the row. Cell margins, if any, override row margins.
Both of these properties allow me to modify cells that were vertically merged into a cell above them. Do you agree this is a good solution to my problem?
Yes, I think adding keepNext on a TableRow is good and easy to understand for the users
It is, however, a cell property in the XML. When serialized to XML, these margins will be merged into all cells in the row. Cell margins, if any, override row margins.
However, I don't think we should do this, if we can, we should not deviate from the official spec too much. It seems to me that adding margins to the TableRow is more of a convenience to save some lines rather than a necessity. And adding margin to a row, to me signifies that it adds a margin around the row (try to imagine a floating row).
P.S. Just checked if adding margin to a table row in HTML <tr></tr> does anything, and it does not.
It seems to me that adding margins to the
TableRowis more of a convenience to save some lines rather than a necessity.
The convenience isn't why I proposed it. It was meant to solve this problem: https://github.com/dolanmiu/docx/issues/668#issuecomment-749202434
I proposed it because it was a way to solve that problem in a way that was compatible with your declarative, read-only objects design that also didn't introduce breaking changes. Do you understand how this solves that problem? Or do I need to clarify? If you do understand the problem and why the solution solves it, but still don't like that solution, then I can propose another solution. But I'm not sure if I can do so without changing how things already work.
P.S. Unless there happens to be a way to modify the margins of cells merged into a cell above them that I don't know about?