Files
MyFastHtml/README.md
2025-11-26 20:53:12 +01:00

960 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# MyFastHtml
A utility library designed to simplify the development of FastHtml applications by providing:
- Predefined pages for common functionalities (e.g., authentication, user management).
- A command management system to facilitate client-server interactions.
- Helpers to create interactive controls more easily.
- A system for control state persistence.
---
## Features
- **Dynamic HTML with HTMX**: Simplify dynamic interaction using attributes like `hx-post` and custom routes like
`/commands`.
- **Command management**: Write server-side logic in Python while abstracting the complexities of HTMX.
- **Binding management**: Mechanism to bind two html element together.
- **Control helpers**: Easily create reusable components like buttons.
- **Login Pages**: Include common pages for login, user management, and customizable dashboards.
> _**Note:** Support for state persistence is currently under construction._
---
## Installation
Ensure you have Python >= 3.12 installed, then install the library with `pip`:
```bash
pip install myfasthtml
```
---
## Quick Start
### FastHtml Application
To create a simple FastHtml application, you can use the `create_app` function:
```python
from fasthtml import serve
from fasthtml.components import *
from myfasthtml.myfastapp import create_app
app, rt = create_app(protect_routes=False)
@rt("/")
def get_homepage():
return Div("Hello, FastHtml!")
if __name__ == "__main__":
serve(port=5002)
```
### Use Commands
```python
from fasthtml import serve
from myfasthtml.controls.helpers import mk
from myfasthtml.core.commands import Command
from myfasthtml.myfastapp import create_app
# Define a simple command action
def say_hello():
return "Hello, FastHtml!"
# Create the command
hello_command = Command("say_hello", "Responds with a greeting", say_hello)
# Create the app
app, rt = create_app(protect_routes=False)
@rt("/")
def get_homepage():
return mk.button("Click Me!", command=hello_command)
if __name__ == "__main__":
serve(port=5002)
```
- When the button is clicked, the `say_hello` command will be executed, and the server will return the response.
- HTMX automatically handles the client-server interaction behind the scenes.
---
### Bind components
```python
from dataclasses import dataclass
from myfasthtml.controls.helpers import mk
@dataclass
class Data:
value: str = "Hello World"
checked: bool = False
# Binds an Input with a label
mk.mk(Input(name="input_name"), binding=Binding(data, attr="value").htmx(trigger="input changed")),
mk.mk(Label("Text"), binding=Binding(data, attr="value")),
# Binds a checkbox with a labl
mk.mk(Input(name="checked_name", type="checkbox"), binding=Binding(data, attr="checked")),
mk.mk(Label("Text"), binding=Binding(data, attr="checked")),
```
## Planned Features (Roadmap)
### Predefined Pages
The library will include predefined pages for:
- **Authentication**: Login, signup, password reset.
- **User Management**: User profile and administration pages.
- **Dashboard Templates**: Fully customizable dashboard components.
- **Error Pages**: Detailed and styled error messages (e.g., 404, 500).
### State Persistence
Controls will have their state automatically synchronized between the client and the server. This feature is currently
under construction.
---
## Advanced Features
### Command Management System
Commands allow you to simplify frontend/backend interaction. Instead of writing HTMX attributes manually, you can define
Python methods and handle them as commands.
#### Example
Heres how `Command` simplifies dynamic interaction:
```python
from myfasthtml.core.commands import Command
# Define a command
def custom_action(data):
return f"Received: {data}"
my_command = Command("custom", "Handles custom logic", custom_action)
# Get the HTMX parameters automatically
htmx_attrs = my_command.get_htmx_params()
print(htmx_attrs)
# Output:
# {
# "hx-post": "/commands",
# "hx-vals": '{"c_id": "unique-command-id"}'
# }
```
Use the `get_htmx_params()` method to directly integrate commands into HTML components.
---
## Testing
### TestableElements
#### TestableTextarea
**Use case:** Multi-line text input
**Methods:**
- `send(value)` - Set the textarea value
- `append(text)` - Append text to current value
- `clear()` - Clear the textarea
**Example:**
```python
def test_textarea_binding(user, rt):
@rt("/")
def index():
data = Data("Initial text")
textarea = Textarea(name="message")
label = Label()
mk.manage_binding(textarea, Binding(data))
mk.manage_binding(label, Binding(data))
return textarea, label
user.open("/")
textarea = user.find_element("textarea")
textarea.send("New message")
user.should_see("New message")
textarea.append("\nMore text")
user.should_see("New message\nMore text")
textarea.clear()
user.should_see("")
```
#### TestableSelect
**Use case:** Dropdown selection
**Properties:**
- `is_multiple` - Check if multiple selection is enabled
- `options` - List of available options
**Methods:**
- `select(value)` - Select option by value
- `select_by_text(text)` - Select option by visible text
- `deselect(value)` - Deselect option (multiple select only)
**Example (Single Select):**
```python
def test_select_binding(user, rt):
@rt("/")
def index():
data = Data("option1")
select = Select(
Option("First", value="option1"),
Option("Second", value="option2"),
Option("Third", value="option3"),
name="choice"
)
label = Label()
mk.manage_binding(select, Binding(data))
mk.manage_binding(label, Binding(data))
return select, label
user.open("/")
select_elt = user.find_element("select")
select_elt.select("option2")
user.should_see("option2")
select_elt.select_by_text("Third")
user.should_see("option3")
```
**Example (Multiple Select):**
```python
def test_multiple_select_binding(user, rt):
@rt("/")
def index():
data = ListData(["option1"])
select = Select(
Option("First", value="option1"),
Option("Second", value="option2"),
Option("Third", value="option3"),
name="choices",
multiple=True
)
label = Label()
mk.manage_binding(select, Binding(data))
mk.manage_binding(label, Binding(data))
return select, label
user.open("/")
select_elt = user.find_element("select")
select_elt.select("option2")
user.should_see("['option1', 'option2']")
select_elt.deselect("option1")
user.should_see("['option2']")
```
#### TestableRange
**Use case:** Slider input
**Properties:**
- `min_value` - Minimum value
- `max_value` - Maximum value
- `step` - Step increment
**Methods:**
- `set(value)` - Set slider to specific value (auto-clamped)
- `increase()` - Increase by one step
- `decrease()` - Decrease by one step
**Example:**
```python
def test_range_binding(user, rt):
@rt("/")
def index():
data = NumericData(50)
range_input = Input(
type="range",
name="volume",
min="0",
max="100",
step="10",
value="50"
)
label = Label()
mk.manage_binding(range_input, Binding(data))
mk.manage_binding(label, Binding(data))
return range_input, label
user.open("/")
slider = user.find_element("input[type='range']")
slider.set(75)
user.should_see("75")
slider.increase()
user.should_see("85")
slider.decrease()
user.should_see("75")
```
#### TestableRadio
**Use case:** Radio button (mutually exclusive options)
**Properties:**
- `radio_value` - The value attribute of this radio
- `is_checked` - Check if this radio is selected
**Methods:**
- `select()` - Select this radio button
**Example:**
```python
def test_radio_binding(user, rt):
@rt("/")
def index():
data = Data("option1")
radio1 = Input(type="radio", name="choice", value="option1", checked=True)
radio2 = Input(type="radio", name="choice", value="option2")
radio3 = Input(type="radio", name="choice", value="option3")
label = Label()
mk.manage_binding(radio1, Binding(data))
mk.manage_binding(radio2, Binding(data))
mk.manage_binding(radio3, Binding(data))
mk.manage_binding(label, Binding(data))
return radio1, radio2, radio3, label
user.open("/")
radio2 = user.find_element("input[value='option2']")
radio2.select()
user.should_see("option2")
radio3 = user.find_element("input[value='option3']")
radio3.select()
user.should_see("option3")
```
#### TestableButton
**Use case:** Clickable button with HTMX
**Properties:**
- `text` - Visible text of the button
**Methods:**
- `click()` - Click the button (triggers HTMX if configured)
**Example:**
```python
def test_button_binding(user, rt):
@rt("/")
def index():
data = Data("initial")
button = Button(
"Click me",
hx_post="/update",
hx_vals='{"action": "clicked"}'
)
label = Label()
mk.manage_binding(button, Binding(data))
mk.manage_binding(label, Binding(data))
return button, label
@rt("/update")
def update(action: str):
data = Data("updated")
label = Label()
mk.manage_binding(label, Binding(data))
return label
user.open("/")
button = user.find_element("button")
button.click()
user.should_see("updated")
```
#### TestableDatalist
**Use case:** Input with autocomplete suggestions (combobox)
**Properties:**
- `suggestions` - List of available suggestions
**Methods:**
- `send(value)` - Set input value (any value, not restricted to suggestions)
- `select_suggestion(value)` - Select a value from suggestions
**Example:**
```python
def test_datalist_binding(user, rt):
@rt("/")
def index():
data = Data("")
datalist = Datalist(
Option(value="apple"),
Option(value="banana"),
Option(value="cherry"),
id="fruits"
)
input_elt = Input(name="fruit", list="fruits")
label = Label()
mk.manage_binding(input_elt, Binding(data))
mk.manage_binding(label, Binding(data))
return input_elt, datalist, label
user.open("/")
input_with_list = user.find_element("input[list='fruits']")
# Free text input
input_with_list.send("mango")
user.should_see("mango")
# Select from suggestions
input_with_list.select_suggestion("banana")
user.should_see("banana")
```
## CSS Selectors for Finding Elements
When using `user.find_element()`, use these selectors:
| Component | Selector Example |
|----------------|--------------------------------------------------------|
| Input (text) | `"input[name='field_name']"` or `"input[type='text']"` |
| Checkbox | `"input[type='checkbox']"` |
| Radio | `"input[type='radio']"` or `"input[value='option1']"` |
| Range | `"input[type='range']"` |
| Textarea | `"textarea"` or `"textarea[name='field_name']"` |
| Select | `"select"` or `"select[name='field_name']"` |
| Button | `"button"` or `"button.primary"` |
| Datalist Input | `"input[list='datalist_id']"` |
## Binding
### Overview
This package contains everything needed to implement a complete binding system for FastHTML components.
### Fully Supported Components Summary
| Component | Testable Class | Binding Support |
|-------------------|------------------|-----------------|
| Input (text) | TestableInput | ✅ |
| Checkbox | TestableCheckbox | ✅ |
| Textarea | TestableTextarea | ✅ |
| Select (single) | TestableSelect | ✅ |
| Select (multiple) | TestableSelect | ✅ |
| Range (slider) | TestableRange | ✅ |
| Radio buttons | TestableRadio | ✅ |
| Button | TestableButton | ✅ |
| Input + Datalist | TestableDatalist | ✅ |
### Supported Components
#### 1. Input (Text)
```python
# Methods
input.send(value)
# Binding modes
- ValueChange(default)
- Text
updates
trigger
data
changes
```
#### 2. Checkbox
```python
# Methods
checkbox.check()
checkbox.uncheck()
checkbox.toggle()
# Binding modes
- AttributePresence
- Boolean
data
binding
```
#### 3. Textarea
```python
# Methods
textarea.send(value)
textarea.append(text)
textarea.clear()
# Binding modes
- ValueChange
- Multi - line
text
support
```
#### 4. Select (Single)
```python
# Methods
select.select(value)
select.select_by_text(text)
# Properties
select.options # List of available options
select.is_multiple # False for single select
# Binding modes
- ValueChange
- String
value
binding
```
#### 5. Select (Multiple)
```python
# Methods
select.select(value)
select.deselect(value)
select.select_by_text(text)
# Properties
select.options
select.is_multiple # True for multiple select
# Binding modes
- ValueChange
- List
data
binding
```
#### 6. Range (Slider)
```python
# Methods
range.set(value) # Auto-clamps to min/max
range.increase()
range.decrease()
# Properties
range.min_value
range.max_value
range.step
# Binding modes
- ValueChange
- Numeric
data
binding
```
#### 7. Radio Buttons
```python
# Methods
radio.select()
# Properties
radio.radio_value # Value attribute
radio.is_checked
# Binding modes
- ValueChange
- String
value
binding
- Mutually
exclusive
group
behavior
```
#### 8. Button
```python
# Methods
button.click()
# Properties
button.text # Visible button text
# Binding modes
- Triggers
HTMX
requests
- Can
update
bindings
via
server
response
```
#### 9. Input + Datalist (Combobox)
```python
# Methods
datalist.send(value) # Any value
datalist.select_suggestion(value) # From suggestions
# Properties
datalist.suggestions # Available options
# Binding modes
- ValueChange
- Hybrid: free
text + suggestions
```
### Architecture Overview
#### Three-Phase Binding Lifecycle
```python
# Phase 1: Create (inactive)
binding = Binding(data, "value")
# Phase 2: Configure + Activate
binding.bind_ft(element, name="input", attr="value")
# Phase 3: Deactivate (cleanup)
binding.deactivate()
```
#### Data Flow
```
User Input → HTMX Component → HTMX Request → Binding.update()
setattr(data, attr, value)
Observable triggers
Binding.notify()
Update all bound UI elements
```
### Quick Reference
#### Creating a Binding
```python
# Simple binding
binding = Binding(data, "value").bind_ft(
Input(name="input"),
name="input",
attr="value"
)
# With detection and update modes
binding = Binding(data, "checked").bind_ft(
Input(type="checkbox", name="check"),
name="check",
attr="checked",
detection_mode=DetectionMode.AttributePresence,
update_mode=UpdateMode.AttributePresence
)
# With data converter
binding = Binding(data, "value").bind_ft(
Input(type="checkbox", name="check"),
name="check",
attr="checked",
data_converter=BooleanConverter()
)
```
#### Testing a Component
```python
def test_component_binding(user, rt):
@rt("/")
def index():
data = Data("initial")
component = Component(name="field")
label = Label()
mk.manage_binding(component, Binding(data))
mk.manage_binding(label, Binding(data))
return component, label
user.open("/")
user.should_see("initial")
testable = user.find_element("selector")
testable.method("new value")
user.should_see("new value")
```
#### Managing Binding Lifecycle
```python
# Create
binding = Binding(data, "value")
# Activate (via bind_ft)
binding.bind_ft(element, name="field")
# Deactivate
binding.deactivate()
# Reactivate with new element
binding.bind_ft(new_element, name="field")
```
### Pattern 1: Bidirectional Binding
All components support bidirectional binding:
- UI changes update the data object
- Data object changes update the UI (via Label or other bound components)
```python
input_elt = Input(name="field")
label_elt = Label()
mk.manage_binding(input_elt, Binding(data))
mk.manage_binding(label_elt, Binding(data))
# Change via UI
testable_input.send("new value")
# Label automatically updates to show "new value"
```
### Pattern 2: Multiple Components, Same Data
Multiple different components can bind to the same data:
```python
input_elt = Input(name="input")
textarea_elt = Textarea(name="textarea")
label_elt = Label()
# All bind to the same data object
mk.manage_binding(input_elt, Binding(data))
mk.manage_binding(textarea_elt, Binding(data))
mk.manage_binding(label_elt, Binding(data))
# Changing any component updates all others
```
### Pattern 3: Component Without Name
Components without a name attribute won't trigger updates but won't crash:
```python
input_elt = Input() # No name attribute
label_elt = Label()
mk.manage_binding(label_elt, Binding(data))
# Input won't trigger updates, but label will still display data
```
## Authentication
session
```
{'access_token': 'xxx',
'refresh_token': 'yyy',
'user_info': {
'email': 'admin@myauth.com',
'username': 'admin',
'roles': ['admin'],
'user_settings': {},
'id': 'uuid',
'created_at': '2025-11-10T15:52:59.006213',
'updated_at': '2025-11-10T15:52:59.006213'
}
}
```
## Contributing
We welcome contributions! To get started:
1. Fork the repository.
2. Create a feature branch.
3. Submit a pull request with clear descriptions of your changes.
For detailed guidelines, see the [Contributing Section](./CONTRIBUTING.md) (coming soon).
---
## License
This project is licensed under the terms of the MIT License. See the `LICENSE` file for details.
---
## Technical Overview
### Project Structure
```
MyFastHtml
├── src
│ ├── myfasthtml/ # Main library code
│ │ ├── core/commands.py # Command definitions
│ │ ├── controls/button.py # Control helpers
│ │ └── pages/LoginPage.py # Predefined Login page
│ └── ...
├── tests # Unit and integration tests
├── LICENSE # License file (MIT)
├── README.md # Project documentation
└── pyproject.toml # Build configuration
```
### Notable Classes and Methods
#### 1. `Command`
Represents a backend action with server communication.
- **Attributes:**
- `id`: Unique identifier for the command.
- `name`: Command name (e.g., `say_hello`).
- `description`: Description of the command.
- **Method:** `get_htmx_params()` generates HTMX attributes.
#### 2. `mk_button`
Simplifies the creation of interactive buttons linked to commands.
- **Arguments:**
- `element` (str): The label for the button.
- `command` (Command): Command associated with the button.
- `kwargs`: Additional button attributes.
#### 3. `LoginPage`
Predefined login page that provides a UI template ready for integration.
- **Constructor Parameters:**
- `settings_manager`: Configuration/settings object.
- `error_message`: Optional error message to display.
- `success_message`: Optional success message to display.
---
## Entry Points
- `/commands`: Handles HTMX requests from the command attributes.
---
## Exceptions
No custom exceptions defined yet. (Placeholder for future use.)
## Troubleshooting
### Issue: "No element found matching selector"
**Cause:** Incorrect CSS selector or element not in DOM
**Solution:** Check the HTML output and adjust selector
```python
# Debug: Print the HTML
print(user.get_content())
# Try different selectors
user.find_element("textarea")
user.find_element("textarea[name='message']")
```
### Issue: TestableControl has no attribute 'send'
**Cause:** Wrong testable class returned by factory
**Solution:** Verify factory method is updated correctly
### Issue: AttributeError on TestableTextarea
**Cause:** Class not properly inheriting from TestableControl
**Solution:** Check class hierarchy and imports
### Issue: Select options not found
**Cause:** `_update_fields()` not parsing select correctly
**Solution:** Verify TestableElement properly parses select/option tags
## Relase History
* 0.1.0 : First release
* 0.2.0 : Updated to myauth 0.2.0
* 0.3.0 : Added Bindings support