Implement reactive attributes for minimal DOM updates

Features:
- Track attributes that depend on signals (ternary expressions)
- Store element references in globals for reactive attributes
- Re-evaluate and update only changed attributes when signals change
- Similar to reactive conditionals but for attributes

Performance:
- Minimal DOM updates - only changed attributes are updated
- No full re-renders, no virtual DOM diffing
- Direct attribute updates via js_set_attribute

Example:
  class_='tab active' if tab_index == 0 else 'tab'

When tab_index changes, only the class attribute updates.

Implementation:
- Collection phase: scan for attributes with signal dependencies
- Render phase: store element references to globals
- Signal update: re-evaluate expression and update attribute

All tests passing (55 passed, 2 skipped)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Aaron SinclairandClaude committed 2025-11-15 11:23:09 -08:00
1 parent b0e38db22c
commit 1575571f3e
6 files changed
+761 -474

No files matched your search

+99
View File
@@ -0,0 +1,99 @@
"""Tests for reactive attributes that update when signals change."""
import pytest
from ritz.parser import parse_component
from ritz.llvm_codegen_signals import generate_llvm_ir
def test_parse_reactive_attribute():
"""Test parsing attribute with signal dependency."""
source = """
from ritz.syntax import *
@component
def Toggle():
active = signal(1)
with Button(class_='active' if active else 'inactive'):
yield "Click"
"""
ir = parse_component(source)
# Should have ternary expression in class attribute
with_stmt = [s for s in ir["body"] if s["type"] == "with"][0]
class_prop = with_stmt["element"]["props"]["class_"]
assert class_prop["type"] == "ifexp"
assert class_prop["test"]["id"] == "active"
def test_reactive_attribute_infrastructure():
"""Test that reactive attribute tracking is generated."""
source = """
from ritz.syntax import *
@component
def Toggle():
active = signal(1)
def toggle():
active = 0 if active else 1
with Button(class_='btn-active' if active else 'btn-inactive', onclick=toggle):
yield "Toggle"
"""
ir = parse_component(source)
llvm_ir = generate_llvm_ir(ir)
# Should have storage for the button element
assert "@reactive_attr_" in llvm_ir or "@elem_" in llvm_ir
# Should update attribute when signal changes in toggle()
# The toggle function should re-evaluate the class expression and update
assert "js_set_attribute" in llvm_ir
def test_multiple_reactive_attributes():
"""Test multiple elements with reactive attributes."""
source = """
from ritz.syntax import *
@component
def Tabs():
tab = signal(0)
def set_tab(value):
tab = value
with Ul():
with Li(class_='active' if tab == 0 else ''):
yield "Tab 0"
with Li(class_='active' if tab == 1 else ''):
yield "Tab 1"
"""
ir = parse_component(source)
llvm_ir = generate_llvm_ir(ir)
# Both Li elements should have reactive attributes
# When tab signal changes, both should update
assert llvm_ir.count("js_set_attribute") >= 2
def test_reactive_attribute_with_comparison():
"""Test reactive attribute with comparison in ternary."""
source = """
from ritz.syntax import *
@component
def Counter():
count = signal(5)
with Div(class_='high' if count > 10 else 'low'):
yield "Value"
"""
ir = parse_component(source)
llvm_ir = generate_llvm_ir(ir)
# Should emit comparison when evaluating class
assert "icmp" in llvm_ir
assert "select" in llvm_ir