Repository: Evertras/bubble-table
Pull request #176 (2024-05-18 09:37:23)

Add row style func
https://github.com/Evertras/bubble-table/issues/173

Adds a way to style a row based on the row index/data, to allow for zebra striping and more dynamic styles based on row data.


Patch:
diff --git a/README.md b/README.md
index 8d2f03b..a9e3467 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,9 @@ determining overrides.  The default base style is a basic right-alignment.
 [See the main feature example](./examples/features) to see styles and
 how they override each other.
 
+Styles can also be applied via a style function which can be used to apply
+zebra striping, data-specific formatting, etc.
+
 Can be focused to highlight a row and navigate with up/down (and j/k).  These
 keys can be customized with a KeyMap.
 
diff --git a/examples/updates/main.go b/examples/updates/main.go
index 207a9ac..608b5da 100644
--- a/examples/updates/main.go
+++ b/examples/updates/main.go
@@ -31,9 +31,30 @@ type Model struct {
 	data []*SomeData
 }
 
+func rowStyleFunc(input table.RowStyleFuncInput) lipgloss.Style {
+	calculatedStyle := lipgloss.NewStyle()
+
+	switch input.Row.Data[columnKeyStatus] {
+	case "Critical":
+		calculatedStyle = styleCritical.Copy()
+	case "Stable":
+		calculatedStyle = styleStable.Copy()
+	case "Good":
+		calculatedStyle = styleGood.Copy()
+	}
+
+	if input.Index%2 == 0 {
+		calculatedStyle = calculatedStyle.Background(lipgloss.Color("#222"))
+	} else {
+		calculatedStyle = calculatedStyle.Background(lipgloss.Color("#444"))
+	}
+
+	return calculatedStyle
+}
+
 func NewModel() Model {
 	return Model{
-		table:       table.New(generateColumns(0)),
+		table:       table.New(generateColumns(0)).WithRowStyleFunc(rowStyleFunc),
 		updateDelay: time.Second,
 	}
 }
@@ -152,18 +173,6 @@ func generateRowsFromData(data []*SomeData) []table.Row {
 			columnKeyStatus: entry.Status,
 		})
 
-		// Highlight different statuses
-		switch entry.Status {
-		case "Critical":
-			row = row.WithStyle(styleCritical)
-
-		case "Stable":
-			row = row.WithStyle(styleStable)
-
-		case "Good":
-			row = row.WithStyle(styleGood)
-		}
-
 		rows = append(rows, row)
 	}
 
diff --git a/table/model.go b/table/model.go
index b1934f8..d5e8937 100644
--- a/table/model.go
+++ b/table/model.go
@@ -40,6 +40,7 @@ type Model struct {
 	baseStyle      lipgloss.Style
 	highlightStyle lipgloss.Style
 	headerStyle    lipgloss.Style
+	rowStyleFunc   func(RowStyleFuncInput) lipgloss.Style
 	border         Border
 	selectedText   string
 	unselectedText string
diff --git a/table/options.go b/table/options.go
index 0bf43ad..eed61d9 100644
--- a/table/options.go
+++ b/table/options.go
@@ -5,6 +5,30 @@ import (
 	"github.com/charmbracelet/lipgloss"
 )
 
+// RowStyleFuncInput is the input to the style function that can
+// be applied to each row.  This is useful for things like zebra
+// striping or other data-based styles.
+//
+// Note that we use a struct here to allow for future expansion
+// while keeping backwards compatibility.
+type RowStyleFuncInput struct {
+	// Index is the index of the row, starting at 0.
+	Index int
+
+	// Row is the full row data.
+	Row Row
+}
+
+// WithRowStyleFunc sets a function that can be used to apply a style to each row
+// based on the row data.  This is useful for things like zebra striping or other
+// data-based styles.  It can be safely set to nil to remove it later.
+// This style is applied after the base style and before individual row styles.
+func (m Model) WithRowStyleFunc(f func(RowStyleFuncInput) lipgloss.Style) Model {
+	m.rowStyleFunc = f
+
+	return m
+}
+
 // WithHighlightedRow sets the highlighted row to the given index.
 func (m Model) WithHighlightedRow(index int) Model {
 	m.rowCursorIndex = index
diff --git a/table/row.go b/table/row.go
index ab3dd61..f2cf5d3 100644
--- a/table/row.go
+++ b/table/row.go
@@ -115,6 +115,15 @@ func (m Model) renderRow(rowIndex int, last bool) string {
 
 	rowStyle := row.Style.Copy()
 
+	if m.rowStyleFunc != nil {
+		styleResult := m.rowStyleFunc(RowStyleFuncInput{
+			Index: rowIndex,
+			Row:   row,
+		})
+
+		rowStyle = rowStyle.Inherit(styleResult)
+	}
+
 	if m.focused && highlighted {
 		rowStyle = rowStyle.Inherit(m.highlightStyle)
 	}


Test patch:
diff --git a/table/view_test.go b/table/view_test.go
index 07014b1..fe07155 100644
--- a/table/view_test.go
+++ b/table/view_test.go
@@ -798,6 +798,57 @@ func TestSimple3x3StyleOverridesAsBaseColumnRowCell(t *testing.T) {
 	assert.Equal(t, expectedTable, rendered)
 }
 
+func TestStyleFuncAppliesAfterBaseStyleAndColStylesAndBeforeRowStyle(t *testing.T) {
+	styleFunc := func(input RowStyleFuncInput) lipgloss.Style {
+		if input.Index%2 == 0 {
+			return lipgloss.NewStyle().Align(lipgloss.Left)
+		}
+
+		return lipgloss.NewStyle()
+	}
+
+	model := New([]Column{
+		NewColumn("1", "1", 6),
+		// This column style should be overridden by the style func
+		NewColumn("2", "2", 6).WithStyle(lipgloss.NewStyle().Align(lipgloss.Right)),
+		NewColumn("3", "3", 6),
+	}).
+		WithBaseStyle(lipgloss.NewStyle().Align(lipgloss.Center)).
+		WithRowStyleFunc(styleFunc)
+
+	rows := []Row{}
+
+	for rowIndex := 1; rowIndex <= 5; rowIndex++ {
+		rowData := RowData{}
+
+		for columnIndex := 1; columnIndex <= 3; columnIndex++ {
+			id := fmt.Sprintf("%d", columnIndex)
+
+			rowData[id] = fmt.Sprintf("%d,%d", columnIndex, rowIndex)
+		}
+
+		rows = append(rows, NewRow(rowData))
+	}
+
+	rows[0] = rows[0].WithStyle(lipgloss.NewStyle().Align(lipgloss.Right))
+
+	model = model.WithRows(rows)
+
+	const expectedTable = `┏━━━━━━┳━━━━━━┳━━━━━━┓
+┃  1   ┃     2┃  3   ┃
+┣━━━━━━╋━━━━━━╋━━━━━━┫
+┃   1,1┃   2,1┃   3,1┃
+┃ 1,2  ┃   2,2┃ 3,2  ┃
+┃1,3   ┃2,3   ┃3,3   ┃
+┃ 1,4  ┃   2,4┃ 3,4  ┃
+┃1,5   ┃2,5   ┃3,5   ┃
+┗━━━━━━┻━━━━━━┻━━━━━━┛`
+
+	rendered := model.View()
+
+	assert.Equal(t, expectedTable, rendered)
+}
+
 // This is a long test due to typing and multiple big table strings, that's okay
 //
 //nolint:funlen