# 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_button
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
@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

Here’s 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
```

## 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
