Modifier Pipeline and Rendering Workflow ​
This guide provides a deep dive into how TachUI's modifier system works internally, from component creation to DOM rendering. Understanding this pipeline is crucial for debugging styling issues and developing advanced components.
Overview ​
The TachUI modifier pipeline consists of several stages that transform components with modifiers into styled DOM elements:
- Component Creation - Components are created and wrapped with modifier support
- Modifier Building - Modifiers are accumulated using the fluent API
- Component Rendering - Components render to DOM node structures
- DOM Creation - DOM nodes are converted to actual HTML elements
- Modifier Application - Modifiers are applied to the final DOM elements
Stage 1: Component Creation with Modifier Support ​
withModifiers() Function ​
When a component is created (e.g., Text(), Button()), it goes through the withModifiers() function:
// In Text.ts
export function Text(content: string | (() => string)) {
const component = new EnhancedText({ content })
return withModifiers(component) // Wraps with modifier support
}The withModifiers() function:
- Calls
createModifiableComponent()to enhance the component - Adds a
modifierproperty with the fluent API - Returns a
ModifiableComponentwith modifier capabilities
createModifiableComponent() Process ​
// In registry.ts
export function createModifiableComponent(component: ComponentInstance) {
const modifiableComponent = {
...component,
modifiers: [], // Empty initially
modifierBuilder: createModifierBuilder(component),
render: enhancedRenderFunction // Enhanced to attach modifiers to DOM nodes
}
return modifiableComponent
}Key Point: The component starts with an empty modifiers array.
Stage 2: Modifier Building with Fluent API ​
Modifier Builder Chain ​
When you use the fluent API:
const styledText = Text('Hello')
.modifier
.fontSize(24) // Creates FontSize modifier
.backgroundColor('red') // Creates Background modifier
.padding(10) // Creates Padding modifier
.build() // Finalizes the componentEach method in the chain:
- Creates a
Modifierobject with type, properties, and priority - Adds it to the builder's internal
modifiersarray - Returns the builder for chaining
The build() Method ​
The critical .build() method finalizes the modifier chain:
// In builder.ts
build(): T {
if ('modifiers' in this.component) {
// Update existing modifiable component
const existingModifiable = this.component as ModifiableComponent
existingModifiable.modifiers = [...existingModifiable.modifiers, ...this.modifiers]
return this.component as T
} else {
// Create new modifiable component with modifiers
return { ...this.component, modifiers: [...this.modifiers] } as T
}
}Key Point: .build() transfers modifiers from the builder to the component's modifiers array.
Stage 3: Component Rendering to DOM Nodes ​
Enhanced Render Function ​
When a modifiable component is rendered, its enhanced render function:
// In registry.ts - createModifiableComponent
modifiableComponent.render = () => {
const renderResult = originalRender()
const nodes = Array.isArray(renderResult) ? renderResult : [renderResult]
// Attach modifiers to each DOM node
return nodes.map((node: any) => ({
...node,
modifiers: modifiableComponent.modifiers, // Transfer modifiers to DOM nodes
componentId: component.id
}))
}Key Point: Modifiers are attached to DOM nodes as metadata for later application.
Layout Component Rendering ​
Layout components (VStack, HStack) render their children:
// In wrapper.ts - LayoutComponent.render()
const vstackRenderedChildren = this.children.map((child) => {
const childResult = child.render() // Child's enhanced render function
return Array.isArray(childResult) ? childResult : [childResult]
})
const vstackFlattened = vstackRenderedChildren.flat()
return [{
type: 'element',
tag: 'div',
props: { style: { display: 'flex', flexDirection: 'column' } },
children: vstackFlattened // Children with modifiers attached
}]Key Point: Child components' modifiers are preserved through the layout rendering process.
Stage 4: DOM Creation ​
DOM Renderer Process ​
The DOM renderer converts DOM nodes to actual HTML elements:
// In renderer.ts
renderSingle(node: DOMNode) {
const element = this.createElement(node) // Create HTML element
// Apply modifiers if present
if (element instanceof Element && 'modifiers' in node && node.modifiers.length > 0) {
this.applyModifiersToElement(element, node.modifiers, node)
}
return element
}Element Creation with Props ​
Basic element creation handles standard HTML props:
// In renderer.ts
createElement(node: DOMNode) {
const element = document.createElement(node.tag)
if (node.props) {
this.applyProps(element, node.props) // Standard HTML attributes
}
return element
}Stage 5: Modifier Application to DOM Elements ​
Two Application Points ​
Modifiers are applied at two points in the pipeline:
1. DOM Renderer (Per Element) ​
// In renderer.ts
private applyModifiersToElement(element: Element, modifiers: any[], node: any) {
applyModifiersToNode(node, modifiers, {
element: element,
componentId: node.componentId,
phase: 'creation'
})
}2. DOM Bridge (Per Component) ​
// In dom-bridge.ts - mountComponentTree
if (isModifiableComponent(component)) {
for (const element of elements) {
applyModifiersToElement(element, component) // Apply component-level modifiers
}
}Modifier Application Engine ​
The core modifier application:
// In registry.ts
export function applyModifiersToNode(node: DOMNode, modifiers: Modifier[], context: ModifierContext) {
const sortedModifiers = [...modifiers].sort((a, b) => a.priority - b.priority)
for (const modifier of sortedModifiers) {
modifier.apply(node, context) // Each modifier applies its styles
}
}Individual Modifier Application ​
Each modifier type implements the apply method:
// Example: FontSize modifier
apply(node: DOMNode, context: ModifierContext) {
if (context.element && context.element instanceof HTMLElement) {
context.element.style.fontSize = `${this.properties.size}px`
}
}Common Pipeline Issues and Solutions ​
Issue: Modifiers Not Applied ​
Symptom: Components render but have no styling
Debugging Steps:
- Check if component has
modifiersarray with expected modifiers - Verify DOM nodes have
modifiersproperty attached - Ensure modifier application doesn't throw errors
Issue: Components Not Rendering ​
Symptom: Only some components display
Debugging Steps:
- Check layout component children rendering
- Verify render functions return proper DOM node arrays
- Check for spread operator issues in
h()function calls
Issue: Events Not Working ​
Symptom: Buttons don't respond to clicks
Debugging Steps:
- Verify event props are attached to DOM nodes (
onClick,onInput) - Check DOM renderer event binding in
applyProps - Ensure event handlers are functions, not undefined
Performance Considerations ​
Modifier Caching ​
Modifiers should be cached when possible:
// Good: Cache modifier results
const computedStyle = useMemo(() => computeStyle(props), [props])
// Bad: Recompute on every render
const computedStyle = computeStyle(props)Batch DOM Updates ​
Use the modifier system's batch application:
applyModifiersToNode(node, modifiers, context, { batch: true })Memory Management ​
Clean up reactive effects in component cleanup:
// In component constructor
this.cleanup.push(() => effect.dispose())Debugging Tools ​
Console Debugging ​
Add temporary logging to trace modifier flow:
// Component creation
console.log('Component created with modifiers:', component.modifiers)
// Render phase
console.log('Rendering with DOM nodes:', nodes.map(n => n.modifiers))
// DOM application
console.log('Applying modifiers to element:', element.tagName, modifiers)Browser DevTools ​
- Elements tab: Inspect final DOM structure and applied styles
- Console: Check for modifier application errors
- Sources: Set breakpoints in modifier apply methods
Best Practices ​
1. Always Use .build() ​
// Good
const component = Text('Hello').modifier.fontSize(24).build()
// Bad - modifiers won't be applied
const component = Text('Hello').modifier.fontSize(24)2. Prefer Specific Modifiers ​
// Good
.modifier.fontSize(24).backgroundColor('#blue')
// Avoid when possible
.modifier.css({ fontSize: '24px', backgroundColor: '#blue' })3. Handle Reactive Content Correctly ​
// Good - reactive text
const dynamicText = Text(() => `Count: ${count()}`)
// Bad - static snapshot
const dynamicText = Text(`Count: ${count()}`)4. Clean Up Resources ​
// In component classes
constructor() {
const effect = createEffect(() => { /* ... */ })
this.cleanup.push(() => effect.dispose())
}Conclusion ​
Understanding the modifier pipeline helps you:
- Debug styling issues effectively
- Create performant custom components
- Optimize modifier application
- Build complex UI layouts with confidence
The key insight is that modifiers flow from component creation → builder accumulation → DOM node attachment → final element application, with proper cleanup and memory management throughout the process.