Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/v2/features/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
* [Parallelism Mode](v2/features/parallelism.md?id=parallelism)
* [Protection](v2/features/protection.md?id=protection)
* [QR Code](v2/features/qrcode.md?id=qrcode)
* [Repeat Header](v2/features/repeatheader.md?id=repeat-header)
* [Signature](v2/features/signature.md?id=signature)
* [Text](v2/features/text.md?id=text)
* [Unit Testing](v2/features/unittests.md?id=unit-testing)
136 changes: 136 additions & 0 deletions docs/v2/features/repeatheader.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Repeat Header on Page Breaks

`WithRepeatOnPageBreak` marks individual rows to automatically re-appear at the top of new pages when content overflows. This is essential for multi-page tables, reports, and data exports where headers must remain visible across page boundaries.

## Overview

When a document's content exceeds a single page, maroto automatically creates new pages. Without repeat headers, table headers disappear after the first page, making it difficult to understand subsequent data. The `WithRepeatOnPageBreak()` method ensures marked rows (typically headers) are automatically re-injected at the beginning of each new page.

## Use Cases

- **Table Headers** - Column labels in multi-page tables
- **Report Sections** - Section titles that span multiple pages
- **Data Lists** - Headers for contact directories, inventories, or catalogs
- **Forms** - Field labels in long surveys or questionnaires
- **Shipping Labels** - Item headers in bill of lading documents

## Key Differences from RegisterHeader

| Aspect | `RegisterHeader()` | `WithRepeatOnPageBreak()` |
|--------|-------------------|-------------------------|
| **Scope** | Global (every page) | Local (on overflow only) |
| **Purpose** | Document-wide header/footer | Table/section header |
| **Setup** | Once at document start | Per-row marking |
| **Repeats** | Automatically on all pages | Only when content breaks |

## Usage Notes

- Marked rows are copied when a page break occurs, not moved
- Performance impact is minimal (typically <1% overhead for reasonable row counts)
- Works seamlessly with `RegisterHeader()` and `RegisterFooter()`
- Compatible with the `list` component for building table-like structures
- Default behavior is non-repeating; must be explicitly enabled

## GoDoc

* [row : WithRepeatOnPageBreak](https://pkg.go.dev/github.com/johnfercher/maroto/v2/pkg/components/row#Row.WithRepeatOnPageBreak)
* [row : IsRepeatOnPageBreak](https://pkg.go.dev/github.com/johnfercher/maroto/v2/pkg/components/row#Row.IsRepeatOnPageBreak)

## Code Example

### Basic Table with Repeating Header

```go
package main

import (
"github.com/johnfercher/maroto/v2"
"github.com/johnfercher/maroto/v2/pkg/components/col"
"github.com/johnfercher/maroto/v2/pkg/components/row"
"github.com/johnfercher/maroto/v2/pkg/components/text"
"github.com/johnfercher/maroto/v2/pkg/config"
)

func main() {
m := maroto.New()

// Create header row that repeats on page breaks
headerRow := row.New(8).
Add(text.NewCol(3, "Item ID")).
Add(text.NewCol(6, "Description")).
Add(text.NewCol(3, "Amount")).
WithRepeatOnPageBreak()

m.AddRows(headerRow)

// Add many data rows (will span multiple pages)
for i := 1; i <= 100; i++ {
m.AddRow(6,
text.NewCol(3, fmt.Sprintf("ID-%d", i)),
text.NewCol(6, fmt.Sprintf("Item %d", i)),
text.NewCol(3, fmt.Sprintf("$%.2f", 10.50*float64(i))),
)
Comment thread
azamat7g marked this conversation as resolved.
}

doc, err := m.Generate()
if err != nil {
panic(err)
}

doc.Save("invoice.pdf")
}
```

### With List Component

```go
type Product struct {
ID string
Name string
Price float64
}

func (p *Product) GetHeader() core.Row {
return row.New(8).
Add(col.New(3).Add(text.NewCol("Item ID"))).
Add(col.New(6).Add(text.NewCol("Product Name"))).
Add(col.New(3).Add(text.NewCol("Price"))).
WithRepeatOnPageBreak() // Header repeats on page breaks
}

func (p *Product) GetContent(i int) core.Row {
return row.New(6).
Add(col.New(3).Add(text.NewCol(p.ID))).
Add(col.New(6).Add(text.NewCol(p.Name))).
Add(col.New(3).Add(text.NewCol(fmt.Sprintf("$%.2f", p.Price))))
}

func main() {
m := maroto.New()

products := []Product{
// 100+ products...
}

rows, err := list.Build(products)
if err != nil {
panic(err)
}

m.AddRows(rows...)
doc, err := m.Generate()
if err != nil {
panic(err)
}

doc.Save("catalog.pdf")
}
```


## Related Features

- [Register Header](v2/features/header?id=header) - Global document header
- [Register Footer](v2/features/footer?id=footer) - Global document footer
- [List Component](v2/features/list?id=list) - Table/list builder
- [Cell Style](v2/features/cellstyle?id=cell-style) - Styling headers and data
33 changes: 33 additions & 0 deletions maroto.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,12 +221,23 @@ func (m *Maroto) addRow(r core.Row) {
return
}

repeatRows := m.collectRepeatRows()

// As row will extrapolate page, we will add empty space
// on the page to force a new page
m.fillPageToAddNew()

m.addHeader()

m.addRepeatRows(repeatRows)

// Re-check: repeat rows + header may have consumed enough space that r still doesn't fit.
if m.currentHeight+rowHeight > maxHeight-m.footerHeight {
// Force one more page break; skip re-collecting repeat rows to avoid accumulation.
m.fillPageToAddNew()
m.addHeader()
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

// AddRows row on the new page
m.currentHeight += rowHeight
m.rows = append(m.rows, r)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand Down Expand Up @@ -373,6 +384,28 @@ func (m *Maroto) getRowsHeight(rows ...core.Row) float64 {
return height
}

func (m *Maroto) collectRepeatRows() []core.Row {
headerSet := make(map[core.Row]bool)
for _, headerRow := range m.header {
headerSet[headerRow] = true
}

var out []core.Row
for _, r := range m.rows {
if r.IsRepeatOnPageBreak() && !headerSet[r] {
out = append(out, r)
}
}
return out
}

func (m *Maroto) addRepeatRows(rows []core.Row) {
for _, r := range rows {
m.currentHeight += r.GetHeight(m.provider, &m.cell)
m.rows = append(m.rows, r)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func getConfig(configs ...*entity.Config) *entity.Config {
if len(configs) > 0 {
return configs[0]
Expand Down
21 changes: 21 additions & 0 deletions maroto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,27 @@ func TestMaroto_AddRow(t *testing.T) {
// Assert
test.New(t).Assert(sut.GetStructure()).Equals("maroto_add_row_3.json")
})
t.Run("when repeat rows exist and page overflows, should re-add them on new page", func(t *testing.T) {
t.Parallel()
// Arrange
cfg := config.NewBuilder().
WithDimensions(20, 20).
WithBottomMargin(0).
WithTopMargin(0).
WithLeftMargin(0).
WithRightMargin(0).
Build()
sut := maroto.New(cfg)

// Act
sut.AddRow(5, col.New(12)).WithRepeatOnPageBreak()
for i := 0; i < 5; i++ {
sut.AddRow(5, col.New(12))
}

// Assert
test.New(t).Assert(sut.GetStructure()).Equals("maroto_add_row_repeat.json")
})
}

func TestMaroto_AddRows(t *testing.T) {
Expand Down
92 changes: 92 additions & 0 deletions mocks/Row.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions pkg/components/row/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,27 @@ func ExampleRow_WithStyle() {
// Do things and generate
_, _ = m.Generate()
}

// ExampleRow_WithRepeatOnPageBreak demonstrates how to mark a Row to repeat on page breaks.
// This is useful for table headers that should appear at the top of each page.
func ExampleRow_WithRepeatOnPageBreak() {
// Create a header row that repeats on page breaks
headerRow := row.New(8).
Add(text.NewCol(6, "Column 1")).
Add(text.NewCol(6, "Column 2")).
WithRepeatOnPageBreak()

m := maroto.New()
m.AddRows(headerRow)

// Add many data rows that will span multiple pages
for i := 0; i < 100; i++ {
m.AddRow(5,
text.NewCol(6, "Data 1"),
text.NewCol(6, "Data 2"),
)
}

// The header row will automatically repeat on page 2, 3, etc.
_, _ = m.Generate()
}
22 changes: 17 additions & 5 deletions pkg/components/row/row.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import (
)

type Row struct {
height float64
autoHeight bool
cols []core.Col
style *props.Cell
config *entity.Config
height float64
autoHeight bool
cols []core.Col
style *props.Cell
config *entity.Config
repeatOnPageBreak bool
}

// New is responsible to create a core.Row.
Expand Down Expand Up @@ -115,6 +116,17 @@ func (r *Row) WithStyle(style *props.Cell) core.Row {
return r
}

// WithRepeatOnPageBreak marks this row to be re-injected at the top of every new page it overflows into.
func (r *Row) WithRepeatOnPageBreak() core.Row {
r.repeatOnPageBreak = true
return r
}

// IsRepeatOnPageBreak returns whether this row should be repeated on page breaks.
func (r *Row) IsRepeatOnPageBreak() bool {
return r.repeatOnPageBreak
}

// resetHeight resets the line height to 0
func (r *Row) resetHeight() {
r.height = 0
Expand Down
Loading