Added Hooks implementation
This commit is contained in:
443
src/components/jsonviewer/Readme.md
Normal file
443
src/components/jsonviewer/Readme.md
Normal 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()`
|
||||
Reference in New Issue
Block a user