# 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: ```python 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: ```python hook = (HookBuilder() .on_render() .when_long_text(100) .execute(my_custom_renderer)) ``` ### Event Types #### `on_render()` Triggers during node rendering, allowing custom rendering logic. ```python 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. ```python 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. ```python 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. ```python # 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. ```python # 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:** ```python # 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:** ```python # 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. ```python # 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. ```python 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. ```python 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. ```python 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: ```python 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: ```python # 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: ```python 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 ```python 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 ```python 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 ```python 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 ```python # 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: ```python 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()`