Files
MyManagingTools/src/components/jsonviewer/Readme.md

11 KiB

JsonViewer Hooks System - Technical Documentation

Overview

The JsonViewer Hooks System provides a flexible, event-driven mechanism to customize the behavior and rendering of JSON nodes. Using a fluent builder pattern, developers can define conditions and actions that trigger during specific events in the JsonViewer lifecycle.

Core Concepts

Hook Architecture

A Hook consists of three components:

  • Event Type: When the hook should trigger (on_render, on_click, etc.)
  • Conditions: What criteria must be met for the hook to execute
  • Executor: The function that runs when conditions are met

HookContext

The HookContext object provides rich information about the current node being processed:

class HookContext:
    key: Any              # The key of the current node
    node: Any             # The node object itself
    helper: Any           # JsonViewer helper utilities
    jsonviewer: Any       # Reference to the parent JsonViewer instance
    json_path: str        # Full JSON path (e.g., "users.0.name")
    parent_node: Any      # Reference to the parent node
    metadata: dict        # Additional metadata storage

Utility Methods:

  • get_node_type(): Returns the string representation of the node type
  • get_value(): Gets the actual value from the node
  • is_leaf_node(): Checks if the node has no children

HookBuilder API

Creating a Hook

Use the HookBuilder class with method chaining to create hooks:

hook = (HookBuilder()
    .on_render()
    .when_long_text(100)
    .execute(my_custom_renderer))

Event Types

on_render()

Triggers during node rendering, allowing custom rendering logic.

def custom_text_renderer(context):
    value = context.get_value()
    return Span(f"Custom: {value}", cls="custom-text")

text_hook = (HookBuilder()
    .on_render()
    .when_type(str)
    .execute(custom_text_renderer))

on_click()

Triggers when a node is clicked.

def handle_click(context):
    print(f"Clicked on: {context.json_path}")
    return None  # No rendering change

click_hook = (HookBuilder()
    .on_click()
    .when_editable()
    .requires_modification()
    .execute(handle_click))

on_hover() / on_focus()

Triggers on hover or focus events respectively.

def show_tooltip(context):
    return Div(f"Path: {context.json_path}", cls="tooltip")

hover_hook = (HookBuilder()
    .on_hover()
    .when_type(str)
    .execute(show_tooltip))

Conditions

Conditions determine when a hook should execute. Multiple conditions can be chained, and all must be satisfied.

when_type(target_type)

Matches nodes with values of a specific type.

# Hook for string values only
string_hook = (HookBuilder()
    .on_render()
    .when_type(str)
    .execute(string_formatter))

# Hook for numeric values
number_hook = (HookBuilder()
    .on_render()
    .when_type((int, float))  # Accepts tuple of types
    .execute(number_formatter))

when_key(key_pattern)

Matches nodes based on their key.

# Exact key match
email_hook = (HookBuilder()
    .on_render()
    .when_key("email")
    .execute(email_formatter))

# Function-based key matching
def is_id_key(key):
    return str(key).endswith("_id")

id_hook = (HookBuilder()
    .on_render()
    .when_key(is_id_key)
    .execute(id_formatter))

when_value(target_value=None, predicate=None)

Matches nodes based on their actual value.

Exact value matching:

# Highlight error status
error_hook = (HookBuilder()
    .on_render()
    .when_value("ERROR")
    .execute(lambda ctx: Span(ctx.get_value(), cls="error-status")))

# Special handling for null values
null_hook = (HookBuilder()
    .on_render()
    .when_value(None)
    .execute(lambda ctx: Span("N/A", cls="null-value")))

Predicate-based matching:

# URLs as clickable links
url_hook = (HookBuilder()
    .on_render()
    .when_value(predicate=lambda x: isinstance(x, str) and x.startswith("http"))
    .execute(lambda ctx: A(ctx.get_value(), href=ctx.get_value(), target="_blank")))

# Large numbers formatting
large_number_hook = (HookBuilder()
    .on_render()
    .when_value(predicate=lambda x: isinstance(x, (int, float)) and x > 1000)
    .execute(lambda ctx: Span(f"{x:,}", cls="large-number")))

when_path(path_pattern)

Matches nodes based on their JSON path using regex.

# Match all user names
user_name_hook = (HookBuilder()
    .on_render()
    .when_path(r"users\.\d+\.name")
    .execute(user_name_formatter))

# Match any nested configuration
config_hook = (HookBuilder()
    .on_render()
    .when_path(r".*\.config\..*")
    .execute(config_formatter))

when_long_text(threshold=100)

Matches string values longer than the specified threshold.

def text_truncator(context):
    value = context.get_value()
    truncated = value[:100] + "..."
    return Div(
        Span(truncated, cls="truncated-text"),
        Button("Show more", cls="expand-btn"),
        cls="long-text-container"
    )

long_text_hook = (HookBuilder()
    .on_render()
    .when_long_text(100)
    .execute(text_truncator))

when_editable(editable_paths=None, editable_types=None)

Matches nodes that should be editable.

def inline_editor(context):
    value = context.get_value()
    return Input(
        value=str(value),
        type="text" if isinstance(value, str) else "number",
        cls="inline-editor",
        **{"data-path": context.json_path}
    )

editable_hook = (HookBuilder()
    .on_click()
    .when_editable(
        editable_paths=["user.name", "user.email"],
        editable_types=[str, int, float]
    )
    .requires_modification()
    .execute(inline_editor))

when_custom(condition)

Use custom condition objects for complex logic.

class BusinessLogicCondition(Condition):
    def evaluate(self, context):
        # Complex business logic here
        return (context.key == "status" and 
                context.get_value() in ["pending", "processing"])

custom_hook = (HookBuilder()
    .on_render()
    .when_custom(BusinessLogicCondition())
    .execute(status_renderer))

Combining Conditions

Multiple Conditions (AND Logic)

Chain multiple conditions - all must be satisfied:

complex_hook = (HookBuilder()
    .on_render()
    .when_type(str)
    .when_key("description")
    .when_long_text(50)
    .execute(description_formatter))

Composite Conditions

Use when_all() and when_any() for explicit logic:

# AND logic
strict_hook = (HookBuilder()
    .on_render()
    .when_all([
        WhenType(str),
        WhenLongText(100),
        WhenKey("content")
    ])
    .execute(content_formatter))

# OR logic
flexible_hook = (HookBuilder()
    .on_render()
    .when_any([
        WhenKey("title"),
        WhenKey("name"),
        WhenKey("label")
    ])
    .execute(title_formatter))

State Modification

Use requires_modification() to indicate that the hook will modify the application state:

def save_edit(context):
    new_value = get_new_value_from_ui()  # Implementation specific
    # Update the actual data
    context.jsonviewer.update_value(context.json_path, new_value)
    return success_indicator()

edit_hook = (HookBuilder()
    .on_click()
    .when_editable()
    .requires_modification()
    .execute(save_edit))

Complete Examples

Example 1: Enhanced Text Display

def enhanced_text_renderer(context):
    value = context.get_value()
    
    # Truncate long text
    if len(value) > 100:
        display_value = value[:100] + "..."
        tooltip = value  # Full text as tooltip
    else:
        display_value = value
        tooltip = None
    
    return Span(
        display_value,
        cls="enhanced-text",
        title=tooltip,
        **{"data-full-text": value}
    )

text_hook = (HookBuilder()
    .on_render()
    .when_type(str)
    .when_value(predicate=lambda x: len(x) > 20)
    .execute(enhanced_text_renderer))

Example 2: Interactive Email Fields

def email_renderer(context):
    email = context.get_value()
    return Div(
        A(f"mailto:{email}", href=f"mailto:{email}", cls="email-link"),
        Button("Copy", cls="copy-btn", **{"data-clipboard": email}),
        cls="email-container"
    )

email_hook = (HookBuilder()
    .on_render()
    .when_key("email")
    .when_value(predicate=lambda x: "@" in str(x))
    .execute(email_renderer))

Example 3: Status Badge System

def status_badge(context):
    status = context.get_value().lower()
    
    badge_classes = {
        "active": "badge-success",
        "pending": "badge-warning", 
        "error": "badge-danger",
        "inactive": "badge-secondary"
    }
    
    css_class = badge_classes.get(status, "badge-default")
    
    return Span(
        status.title(),
        cls=f"status-badge {css_class}"
    )

status_hook = (HookBuilder()
    .on_render()
    .when_key("status")
    .when_value(predicate=lambda x: str(x).lower() in ["active", "pending", "error", "inactive"])
    .execute(status_badge))

Integration with JsonViewer

Adding Hooks to JsonViewer

# Create your hooks
hooks = [
    text_hook,
    email_hook, 
    status_hook
]

# Initialize JsonViewer with hooks
viewer = JsonViewer(
    session=session,
    _id="my-viewer",
    data=my_json_data,
    hooks=hooks
)

Factory Functions

Create reusable hook factories for common patterns:

def create_url_link_hook():
    """Factory for URL link rendering"""
    def url_renderer(context):
        url = context.get_value()
        return A(url, href=url, target="_blank", cls="url-link")
    
    return (HookBuilder()
            .on_render()
            .when_value(predicate=lambda x: isinstance(x, str) and x.startswith(("http://", "https://")))
            .execute(url_renderer))

def create_currency_formatter_hook(currency_symbol="$"):
    """Factory for currency formatting"""
    def currency_renderer(context):
        amount = context.get_value()
        return Span(f"{currency_symbol}{amount:,.2f}", cls="currency-amount")
    
    return (HookBuilder()
            .on_render()
            .when_type((int, float))
            .when_key(lambda k: "price" in str(k).lower() or "amount" in str(k).lower())
            .execute(currency_renderer))

# Usage
hooks = [
    create_url_link_hook(),
    create_currency_formatter_hook("€"),
]

Best Practices

  1. Specific Conditions: Use the most specific conditions possible to avoid unintended matches
  2. Performance: Avoid complex predicates in when_value() for large datasets
  3. Error Handling: Include error handling in your executor functions
  4. Reusability: Create factory functions for common hook patterns
  5. Testing: Test hooks with various data structures to ensure they work as expected

Performance Considerations

  • Hooks are evaluated in the order they are added to the JsonViewer
  • Only the first matching hook for each event type will execute per node
  • Use simple conditions when possible to minimize evaluation time
  • Consider the size of your JSON data when using regex in when_path()