Grid Layouts with GridBuilder¶
Build complex node layouts efficiently using the GridBuilder API, which reduces layout code by 60-70% compared to manual grid construction.
Overview¶
GridBuilder provides a fluent API for creating CSS grid layouts within nodes. Instead of manually constructing NodeGrid, GridCell, and GridCoordinates objects, you can use a chainable builder pattern with preset templates for common layouts.
Key features: - 🎨 Preset layouts: Ready-to-use templates (three_column, simple_node) - 🔧 Fluent API: Chainable methods for readable code - 📐 Row/column helpers: Convenient methods for horizontal and vertical layouts - ⚡ Reduced boilerplate: 60-70% less code than manual construction - 🎯 Type-safe: Full Pydantic validation
Quick Start¶
Using Presets¶
The fastest way to create common layouts:
from pynodewidget import JsonSchemaNodeWidget, GridBuilder, PRESETS
from pydantic import BaseModel, Field
class Params(BaseModel):
threshold: float = Field(default=0.5, ge=0, le=1)
enabled: bool = True
class DashboardNode(JsonSchemaNodeWidget):
label = "Dashboard"
parameters = Params
icon = "📊"
# Three-column preset
grid = (
GridBuilder()
.preset(PRESETS.three_column)
.slot("header", HeaderComponent(id="header", label="Settings"))
.slot("center", TextField(id="notes", label="Notes", multiline=True))
.build()
)
Custom Grid Layouts¶
Build custom layouts cell-by-cell:
class CustomNode(JsonSchemaNodeWidget):
label = "Custom Layout"
parameters = Params
grid = (
GridBuilder()
.rows(["60px", "1fr", "40px"])
.cols(["200px", "1fr"])
.gap("0.5rem")
.cell(row=1, col=1, col_span=2, components=[
HeaderComponent(id="header", label="Title")
])
.cell(row=2, col=1, components=[
TextField(id="sidebar", label="Sidebar")
])
.cell(row=2, col=2, components=[
TextField(id="content", label="Content", multiline=True)
])
.cell(row=3, col=1, col_span=2, components=[
ButtonHandle(id="submit", label="Submit", action="execute")
])
.build()
)
Preset Layouts¶
GridBuilder includes two common layout presets:
1. Three-Column¶
Three-column layout with optional header and footer.
from pynodewidget import GridBuilder, PRESETS
# Basic three-column layout
grid = (
GridBuilder()
.preset(PRESETS.three_column)
.slot("left", LabeledHandle(id="input", handle_type="input"))
.slot("center", TextField(id="content", label="Content"))
.slot("right", LabeledHandle(id="output", handle_type="output"))
.build()
)
# With optional header and footer
grid = (
GridBuilder()
.preset(PRESETS.three_column)
.slot("header", HeaderComponent(id="header", label="Node Title"))
.slot("left", LabeledHandle(id="input", handle_type="input"))
.slot("center", TextField(id="content", label="Content"))
.slot("right", LabeledHandle(id="output", handle_type="output"))
.slot("footer", BoolField(id="enabled", value=True))
.build()
)
Structure (basic):
┌──────┬──────┬──────┐
│ │ │ │
│ Left │Center│Right │ 1fr
│ │ │ │
└──────┴──────┴──────┘
auto 1fr auto
Structure (with header/footer):
┌─────────────────────┐
│ Header │ auto
├──────┬──────┬───────┤
│ │ │ │
│ Left │Center│ Right │ 1fr
│ │ │ │
├──────┴──────┴───────┤
│ Footer │ auto
└─────────────────────┘
auto 1fr auto
Slot names: "left", "center", "right", "header" (optional), "footer" (optional)
2. Simple Node¶
Minimal node layout with header and centered input/output handles.
grid = (
GridBuilder()
.preset(PRESETS.simple_node)
.slot("header", HeaderComponent(id="header", label="Transform"))
.slot("input", ButtonHandle(id="in", handle_type="input"))
.slot("center", TextField(id="value", label="Value"))
.slot("output", ButtonHandle(id="out", handle_type="output"))
.build()
)
Structure:
┌─────────────────────┐
│ Header │ auto
├──────┬──────┬───────┤
│ In │ Value│ Out │ 1fr
└──────┴──────┴───────┘
auto 1fr auto
Slot names: "header", "input", "center", "output"
Row and Column Helpers¶
Convenience methods for linear layouts:
row() - Horizontal Layout¶
Create a single-row layout with multiple columns:
from pynodewidget import GridBuilder, TextField, NumberField
grid = (
GridBuilder()
.row(
TextField(id="name", label="Name"),
NumberField(id="age", label="Age"),
TextField(id="email", label="Email")
)
.build()
)
Equivalent to:
grid = (
GridBuilder()
.rows(["1fr"])
.cols(["1fr", "1fr", "1fr"])
.cell(row=1, col=1, components=[TextField(id="name", label="Name")])
.cell(row=1, col=2, components=[NumberField(id="age", label="Age")])
.cell(row=1, col=3, components=[TextField(id="email", label="Email")])
.build()
)
col() - Vertical Layout¶
Create a single-column layout with multiple rows:
grid = (
GridBuilder()
.col(
HeaderComponent(id="header", label="Form"),
TextField(id="name", label="Name"),
NumberField(id="count", label="Count"),
ButtonHandle(id="submit", label="Submit", action="execute")
)
.build()
)
Equivalent to:
grid = (
GridBuilder()
.rows(["auto", "auto", "auto", "auto"])
.cols(["1fr"])
.cell(row=1, col=1, components=[HeaderComponent(...)])
.cell(row=2, col=1, components=[TextField(...)])
.cell(row=3, col=1, components=[NumberField(...)])
.cell(row=4, col=1, components=[ButtonHandle(...)])
.build()
)
Combining row() with Customization¶
grid = (
GridBuilder()
.row(
TextField(id="input1", label="Input 1"),
TextField(id="input2", label="Input 2")
)
.gap("1rem") # Add spacing
.build()
)
Custom Grid Building¶
Basic Grid Configuration¶
Set up grid dimensions and spacing:
grid = (
GridBuilder()
.rows(["60px", "1fr", "40px"]) # 3 rows: fixed, flexible, fixed
.cols(["1fr", "2fr"]) # 2 cols: 1:2 ratio
.gap("0.5rem") # Spacing between cells
.build()
)
Row/column sizing options:
- "auto": Size to content
- "1fr", "2fr", etc.: Fractional units (flexible)
- "100px", "50%": Fixed sizes
- "minmax(100px, 1fr)": Min/max constraints
Adding Cells¶
Place components in grid cells:
grid = (
GridBuilder()
.rows(["60px", "1fr"])
.cols(["1fr"])
.cell(
row=1, # Grid row (1-indexed)
col=1, # Grid column (1-indexed)
components=[ # List of components
HeaderComponent(id="header", label="Title", icon="📋")
]
)
.cell(
row=2,
col=1,
components=[
TextField(id="notes", label="Notes", multiline=True)
]
)
.build()
)
Cell Spanning¶
Cells can span multiple rows or columns:
grid = (
GridBuilder()
.rows(["60px", "1fr"])
.cols(["1fr", "1fr", "1fr"])
.cell(
row=1,
col=1,
col_span=3, # Span all 3 columns
components=[HeaderComponent(id="header", label="Full Width Header")]
)
.cell(row=2, col=1, components=[TextField(id="col1", label="Column 1")])
.cell(row=2, col=2, components=[TextField(id="col2", label="Column 2")])
.cell(row=2, col=3, components=[TextField(id="col3", label="Column 3")])
.build()
)
Spanning options:
- col_span: Number of columns to span
- row_span: Number of rows to span
Cell Layout Options¶
Customize alignment and padding within cells:
grid = (
GridBuilder()
.rows(["auto"])
.cols(["1fr"])
.cell(
row=1,
col=1,
components=[ButtonHandle(id="btn", label="Click", action="execute")],
# Cell-level layout
h_align="center", # Horizontal: "start", "center", "end", "stretch"
v_align="center", # Vertical: "start", "center", "end", "stretch"
padding="1rem" # Internal padding
)
.build()
)
Complete Examples¶
Dashboard with Statistics¶
from pynodewidget import (
JsonSchemaNodeWidget, GridBuilder,
HeaderComponent, TextField, NumberField, BoolField, ButtonHandle
)
from pydantic import BaseModel, Field
class DashboardParams(BaseModel):
title: str = Field(default="Dashboard")
metric1: float = Field(default=0.0)
metric2: float = Field(default=0.0)
enabled: bool = True
class DashboardNode(JsonSchemaNodeWidget):
label = "Dashboard"
parameters = DashboardParams
icon = "📊"
color = "blue"
grid = (
GridBuilder()
.rows(["60px", "auto", "1fr", "40px"])
.cols(["1fr", "1fr"])
.gap("0.5rem")
# Header spanning full width
.cell(
row=1, col=1, col_span=2,
components=[HeaderComponent(
id="header",
label="Analytics Dashboard",
icon="📈",
bgColor="#1e40af"
)]
)
# Metrics row
.cell(row=2, col=1, components=[
NumberField(id="metric1", label="Metric 1", value=0.0)
])
.cell(row=2, col=2, components=[
NumberField(id="metric2", label="Metric 2", value=0.0)
])
# Content spanning full width
.cell(
row=3, col=1, col_span=2,
components=[TextField(
id="notes",
label="Analysis Notes",
multiline=True,
placeholder="Enter your analysis..."
)]
)
# Footer with controls
.cell(row=4, col=1, components=[
BoolField(id="enabled", label="Enable Updates")
])
.cell(row=4, col=2, components=[
ButtonHandle(id="refresh", label="Refresh", action="refresh")
], h_align="end")
.build()
)
Form with Sections¶
class FormParams(BaseModel):
name: str = Field(default="")
email: str = Field(default="")
age: int = Field(default=18, ge=0, le=120)
newsletter: bool = False
class FormNode(JsonSchemaNodeWidget):
label = "Registration Form"
parameters = FormParams
icon = "📝"
grid = (
GridBuilder()
.col(
HeaderComponent(id="h1", label="Personal Information", bgColor="#059669"),
TextField(id="name", label="Full Name"),
TextField(id="email", label="Email"),
NumberField(id="age", label="Age"),
HeaderComponent(id="h2", label="Preferences", bgColor="#0891b2"),
BoolField(id="newsletter", label="Subscribe to newsletter"),
ButtonHandle(id="submit", label="Submit", action="register")
)
.gap("0.5rem")
.build()
)
Split View Editor¶
class EditorParams(BaseModel):
source: str = Field(default="")
compiled: str = Field(default="")
auto_compile: bool = True
class EditorNode(JsonSchemaNodeWidget):
label = "Code Editor"
parameters = EditorParams
icon = "💻"
grid = (
GridBuilder()
.rows(["60px", "1fr", "40px"])
.cols(["1fr", "1fr"])
.gap("0.5rem")
# Header
.cell(
row=1, col=1, col_span=2,
components=[HeaderComponent(
id="header",
label="Code Compiler",
icon="⚙️"
)]
)
# Source code
.cell(row=2, col=1, components=[
TextField(
id="source",
label="Source Code",
multiline=True,
placeholder="Enter code..."
)
])
# Compiled output
.cell(row=2, col=2, components=[
TextField(
id="compiled",
label="Compiled Output",
multiline=True,
disabled=True
)
])
# Footer controls
.cell(row=3, col=1, components=[
BoolField(id="auto_compile", label="Auto-compile")
])
.cell(row=3, col=2, components=[
ButtonHandle(id="compile", label="Compile Now", action="compile")
], h_align="end")
.build()
)
Sidebar Navigation¶
class NavParams(BaseModel):
current_page: str = Field(default="home")
content: str = Field(default="")
class NavNode(JsonSchemaNodeWidget):
label = "Page Layout"
parameters = NavParams
icon = "🗂️"
grid = (
GridBuilder()
.preset(PRESETS.sidebar)
.slot("sidebar", SelectField(
id="current_page",
label="Navigation",
options=["home", "dashboard", "settings", "help"]
))
.slot("content", TextField(
id="content",
label="Page Content",
multiline=True,
placeholder="Content goes here..."
))
.build()
)
Asymmetric Dashboard¶
class AsymmetricParams(BaseModel):
featured: str = Field(default="")
widget1: str = Field(default="")
widget2: str = Field(default="")
widget3: str = Field(default="")
class AsymmetricNode(JsonSchemaNodeWidget):
label = "Asymmetric Layout"
parameters = AsymmetricParams
icon = "📐"
grid = (
GridBuilder()
.rows(["60px", "200px", "100px"])
.cols(["1fr", "1fr", "1fr"])
.gap("0.5rem")
# Header
.cell(row=1, col=1, col_span=3, components=[
HeaderComponent(id="header", label="Dashboard")
])
# Featured content (spans 2 columns)
.cell(row=2, col=1, col_span=2, components=[
TextField(id="featured", label="Featured", multiline=True)
])
# Side widget
.cell(row=2, col=3, components=[
TextField(id="widget1", label="Quick Stats")
])
# Bottom row (3 equal widgets)
.cell(row=3, col=1, components=[
TextField(id="widget2", label="Widget 2")
])
.cell(row=3, col=2, components=[
TextField(id="widget3", label="Widget 3")
])
.cell(row=3, col=3, components=[
ButtonHandle(id="action", label="Action", action="execute")
])
.build()
)
Comparison: Manual vs GridBuilder¶
Manual Construction (Old Way)¶
from pynodewidget.models import NodeGrid, GridCell, GridCoordinates, CellLayout
grid = NodeGrid(
rows=["60px", "1fr"],
columns=["1fr"],
gap="0.5rem",
cells=[
GridCell(
id="cell-header",
coordinates=GridCoordinates(row=1, col=1),
layout=CellLayout(),
components=[
HeaderComponent(id="header", label="Title")
]
),
GridCell(
id="cell-body",
coordinates=GridCoordinates(row=2, col=1),
layout=CellLayout(),
components=[
TextField(id="content", label="Content")
]
)
]
)
GridBuilder (New Way)¶
from pynodewidget import GridBuilder, PRESETS
grid = (
GridBuilder()
.preset(PRESETS.three_column)
.slot("header", HeaderComponent(id="header", label="Title"))
.slot("center", TextField(id="content", label="Content"))
.build()
)
Benefits: - ✅ 70% less code - ✅ Chainable, readable API - ✅ No manual ID generation for cells - ✅ Type-safe with Pydantic validation - ✅ Preset templates for common layouts
Best Practices¶
1. Start with Presets¶
Use presets for standard layouts before building custom grids:
# ✅ Good: Use preset when it fits
grid = GridBuilder().preset(PRESETS.three_column)...
# ❌ Avoid: Rebuilding common layouts manually
grid = GridBuilder().rows(["auto", "1fr", "auto"]).cols(["auto", "1fr", "auto"])...
2. Use row() and col() for Linear Layouts¶
# ✅ Good: Use row() for horizontal layout
grid = GridBuilder().row(field1, field2, field3).build()
# ❌ Avoid: Manual cell placement for simple rows
grid = GridBuilder().rows(["1fr"]).cols(["1fr", "1fr", "1fr"])
.cell(row=1, col=1, components=[field1])
.cell(row=1, col=2, components=[field2])...
3. Chain Methods for Readability¶
# ✅ Good: Chain methods
grid = (
GridBuilder()
.rows(["60px", "1fr"])
.cols(["200px", "1fr"])
.gap("0.5rem")
.cell(...)
.build()
)
# ❌ Avoid: Breaking chain unnecessarily
builder = GridBuilder()
builder = builder.rows(["60px", "1fr"])
builder = builder.cols(["200px", "1fr"])
grid = builder.build()
4. Use Descriptive Component IDs¶
# ✅ Good: Clear IDs
.slot("header", HeaderComponent(id="main-header", label="Title"))
.slot("body", TextField(id="notes-content", label="Notes"))
# ❌ Avoid: Generic IDs
.slot("header", HeaderComponent(id="comp1", label="Title"))
.slot("body", TextField(id="comp2", label="Notes"))
5. Leverage Spanning for Headers/Footers¶
# ✅ Good: Span full width for headers
.cell(row=1, col=1, col_span=3, components=[HeaderComponent(...)])
# ❌ Avoid: Multiple header cells
.cell(row=1, col=1, components=[HeaderComponent(...)])
.cell(row=1, col=2, components=[HeaderComponent(...)])
.cell(row=1, col=3, components=[HeaderComponent(...)])
6. Set Appropriate Row Sizes¶
# ✅ Good: Fixed size for headers/footers, flexible for content
.rows(["60px", "1fr", "40px"])
# ❌ Avoid: All flexible rows when you need fixed sizes
.rows(["1fr", "1fr", "1fr"])
7. Add Gap for Visual Separation¶
# ✅ Good: Consistent spacing
GridBuilder().rows([...]).cols([...]).gap("0.5rem")
# ❌ Avoid: No gap (components touch)
GridBuilder().rows([...]).cols([...]) # gap defaults to "0"
Troubleshooting¶
Grid Not Displaying¶
Problem: Grid appears empty or components don't show.
Solutions:
- Ensure you call .build() at the end
- Check that component IDs are unique
- Verify row/col indices are 1-based (not 0-based)
# ✅ Correct
.cell(row=1, col=1, components=[...])
# ❌ Wrong (0-based indexing)
.cell(row=0, col=0, components=[...])
Spanning Not Working¶
Problem: Cell doesn't span as expected.
Solutions: - Verify you have enough rows/columns for the span - Check that col_span and row_span are integers - Ensure no overlapping cells
# ✅ Correct: 3 columns available for col_span=3
.cols(["1fr", "1fr", "1fr"])
.cell(row=1, col=1, col_span=3, components=[...])
# ❌ Wrong: Only 2 columns, can't span 3
.cols(["1fr", "1fr"])
.cell(row=1, col=1, col_span=3, components=[...])
Components Overlap¶
Problem: Components render on top of each other.
Solutions:
- Add gap between cells: .gap("0.5rem")
- Check for overlapping cell coordinates
- Verify spanning doesn't create conflicts
Preset Slot Not Found¶
Problem: ValueError: Unknown slot name 'xyz'
Solutions:
- Check preset documentation for correct slot names
- Use PRESETS.three_column, PRESETS.simple_node
- Verify spelling of slot names
# ✅ Correct
.preset(PRESETS.three_column)
.slot("left", ...)
.slot("center", ...)
# ❌ Wrong slot name
.preset(PRESETS.three_column)
.slot("middle", ...) # Should be "center"
API Reference¶
GridBuilder Methods¶
.preset(preset: PresetConfig) -> GridBuilder¶
Apply a preset configuration.
Parameters:
- preset: One of PRESETS.three_column, PRESETS.simple_node
Returns: Self (for chaining)
.slot(name: str, *components) -> GridBuilder¶
Assign components to a preset slot.
Parameters:
- name: Slot name from preset (e.g., "header", "body", "left")
- *components: Component instances to place in the slot
Returns: Self (for chaining)
Raises: ValueError if slot name not found in preset
.rows(sizes: list[str]) -> GridBuilder¶
Set grid row sizes.
Parameters:
- sizes: List of CSS grid row sizes (e.g., ["60px", "1fr", "auto"])
Returns: Self (for chaining)
.cols(sizes: list[str]) -> GridBuilder¶
Set grid column sizes.
Parameters:
- sizes: List of CSS grid column sizes (e.g., ["200px", "1fr"])
Returns: Self (for chaining)
.gap(gap: str) -> GridBuilder¶
Set spacing between grid cells.
Parameters:
- gap: CSS gap value (e.g., "0.5rem", "10px")
Returns: Self (for chaining)
.cell(row: int, col: int, components: list, ...) -> GridBuilder¶
Add a cell to the grid.
Parameters:
- row: Row number (1-indexed)
- col: Column number (1-indexed)
- components: List of component instances
- row_span: (Optional) Number of rows to span (default: 1)
- col_span: (Optional) Number of columns to span (default: 1)
- h_align: (Optional) Horizontal alignment: "start", "center", "end", "stretch"
- v_align: (Optional) Vertical alignment: "start", "center", "end", "stretch"
- padding: (Optional) Cell padding (CSS value)
Returns: Self (for chaining)
.row(*components) -> GridBuilder¶
Create single-row layout with components as columns.
Parameters:
- *components: Component instances to place in columns
Returns: Self (for chaining)
.col(*components) -> GridBuilder¶
Create single-column layout with components as rows.
Parameters:
- *components: Component instances to place in rows
Returns: Self (for chaining)
.build() -> NodeGrid¶
Build and return the final grid configuration.
Returns: NodeGrid instance ready for use in node classes
Next Steps¶
- Styling Nodes: Customize node appearance
- Custom Nodes: Build complete custom nodes
- Handles Configuration: Configure connection points
- Developer Architecture: Understand internal structure