Added Hooks implementation

This commit is contained in:
2025-08-28 23:24:28 +02:00
parent eb8d6a99a2
commit 292a477298
5 changed files with 1152 additions and 74 deletions

View File

@@ -0,0 +1,443 @@
# 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()`