Skip to content

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:

  1. Component Creation - Components are created and wrapped with modifier support
  2. Modifier Building - Modifiers are accumulated using the fluent API
  3. Component Rendering - Components render to DOM node structures
  4. DOM Creation - DOM nodes are converted to actual HTML elements
  5. 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:

typescript
// In Text.ts
export function Text(content: string | (() => string)) {
  const component = new EnhancedText({ content })
  return withModifiers(component)  // Wraps with modifier support
}

The withModifiers() function:

  1. Calls createModifiableComponent() to enhance the component
  2. Adds a modifier property with the fluent API
  3. Returns a ModifiableComponent with modifier capabilities

createModifiableComponent() Process ​

typescript
// 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:

typescript
const styledText = Text('Hello')
  .modifier
  .fontSize(24)           // Creates FontSize modifier
  .backgroundColor('red') // Creates Background modifier  
  .padding(10)           // Creates Padding modifier
  .build()               // Finalizes the component

Each method in the chain:

  1. Creates a Modifier object with type, properties, and priority
  2. Adds it to the builder's internal modifiers array
  3. Returns the builder for chaining

The build() Method ​

The critical .build() method finalizes the modifier chain:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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) ​

typescript
// 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) ​

typescript
// 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:

typescript
// 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:

typescript
// 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:

  1. Check if component has modifiers array with expected modifiers
  2. Verify DOM nodes have modifiers property attached
  3. Ensure modifier application doesn't throw errors

Issue: Components Not Rendering ​

Symptom: Only some components display

Debugging Steps:

  1. Check layout component children rendering
  2. Verify render functions return proper DOM node arrays
  3. Check for spread operator issues in h() function calls

Issue: Events Not Working ​

Symptom: Buttons don't respond to clicks

Debugging Steps:

  1. Verify event props are attached to DOM nodes (onClick, onInput)
  2. Check DOM renderer event binding in applyProps
  3. Ensure event handlers are functions, not undefined

Performance Considerations ​

Modifier Caching ​

Modifiers should be cached when possible:

typescript
// 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:

typescript
applyModifiersToNode(node, modifiers, context, { batch: true })

Memory Management ​

Clean up reactive effects in component cleanup:

typescript
// In component constructor
this.cleanup.push(() => effect.dispose())

Debugging Tools ​

Console Debugging ​

Add temporary logging to trace modifier flow:

typescript
// 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 ​

  1. Elements tab: Inspect final DOM structure and applied styles
  2. Console: Check for modifier application errors
  3. Sources: Set breakpoints in modifier apply methods

Best Practices ​

1. Always Use .build() ​

typescript
// 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 ​

typescript
// Good
.modifier.fontSize(24).backgroundColor('#blue')

// Avoid when possible
.modifier.css({ fontSize: '24px', backgroundColor: '#blue' })

3. Handle Reactive Content Correctly ​

typescript
// Good - reactive text
const dynamicText = Text(() => `Count: ${count()}`)

// Bad - static snapshot
const dynamicText = Text(`Count: ${count()}`)

4. Clean Up Resources ​

typescript
// 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.

Released under the MPL-2.0 License.