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