Skip to content

Htmy integraton component_selector pattern feels a bit too "magical" #54

@danielkjellid

Description

@danielkjellid

Hello! Thank you for your work on htmy and fasthx, both has been very pleasant to use, and the APIs are both intuitive and simple. Great work.

However, one of the main parts of the FastAPI -> fasthx -> htmy integration i'm falling in love with is its explicitness, and not too much magic happening, however, i think the component_selector pattern in the htmy.hx decorator is a bit hard to grasp (especially with the lacking documentation), and locks you in a bit too much, allow me to explain:

I have a simple login form, containing email and password. On form submit, i want to authenticate the user, and show form field errors if an error occurs. On successful authentication, i want to redirect the user to the home page. A pretty common sign in flow. This is achievable, but the end product feels like to does a lot of magic, and makes it a bit hard to read and understand. En example:

@dataclass
class LoginForm:
    errors: dict[str, str] = field(default_factory=dict)

    def htmy(self, context: Context) -> Component:
        return html.div(
            html.form(
                html.input_(
                    type="email",
                    name="email",
                    placeholder="Email",
                ),
                html.p(self.errors.get("email", "")),
                ...
                html.button(
                    "Login",
                    type="submit",
                ),
                id="login-form",
                method="POST",
                hx_post="/login/",
                hx_swap="innerHTML",
            ),
        )


@router.post("/login")
@htmy.hx(LoginForm)
async def login_with_credentials_api(
    email: Annotated[str, Form()],
    password: Annotated[str, Form()],
):
    ...

    if not is_email_valid:
        # Its very magical and implicit that this gets 
        # propagated to the "error" class var in the
        # LoginForm component. 
        # Especially if you have multiple class vars that 
        # needs to get populated.
        return {"email": "Invalid email"} 

    return RedirectResponse(
        url="/",
        status_code=status.HTTP_200_OK,
        headers={
            "HX-Push-Url": "/",
        },
    )

Could a solution be to make the component_selector argument optional, and rather explicitly instantiate the component inside the business logic?

Example:

@router.post("/login")
@htmy.hx()
async def login_with_credentials_api(
    email: Annotated[str, Form()],
    password: Annotated[str, Form()],
    request: Request,
):
    ...
    
    if not is_email_valid:
        context = CurrentRequest.to_context(request)
        return LoginForm(errors={"email": "Invalid email"}).htmy(context)

    return RedirectResponse(
        url="/",
        status_code=status.HTTP_200_OK,  # htmx does not read headers on a 302.
        headers={
            "HX-Push-Url": "/",
        },
    )

This makes it, in my opinion, far easier to understand what is going on. Maybe I'm just ignorant, and there is a better solution that already exists 😅

Again, thank you! Keep up the good work.

Metadata

Metadata

Assignees

No one assigned

    Labels

    questionFurther information is requested

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions