Htmy integraton component_selector pattern feels a bit too "magical" #55
-
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. |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments
-
Hi, Thanks for taking the time to write such a detailed description. I'll start with a question: would it be okay for you to convert this issue to a discussion? Then, if an actual feature request comes out, we can create a new issue with just the requirements. Sorry, I just like to keep my issue trackers relatively tidy. You are totally right about the lacking documentation on the integration. Unfortunately I work on these libs in my spare time, which is usually fairly limited. This is one of the reasons why I tend to recommend people (who seem to work on business projects) to get in contact for consulting. That way I have the time to put the basic app patterns in their codebase and also explain them. In some cases even work on the libraries a bit. I hope eventually the projects will find some contributors and the docs will improve. Sorry, that was a long intro 🙂 Back to your question. The magic is that there is no magic. For the
Now, in your case, the component selector itself is an The entire concept of Some notes regarding this part of your snippet: context = CurrentRequest.to_context(request)
return LoginForm(errors={"email": "Invalid email"}).htmy(context) You can not use context like this in the route (this can only be used in But then you may ask: could a route just return a component to be rendered? You can do that already like this: @router.post("/login")
@htmy.hx(lambda result: result)
async def login_with_credentials_api(
email: Annotated[str, Form()],
password: Annotated[str, Form()],
request: Request,
):
...
if not is_email_valid:
return LoginForm(errors={"email": "Invalid email"})
return RedirectResponse(
url="/",
status_code=status.HTTP_200_OK, # htmx does not read headers on a 302.
headers={
"HX-Push-Url": "/",
},
) This is not a pattern I'd generally recommend, mainly because if the submitted data is wrong, you should instead raise an exception and use the I hope I managed to help a bit. Sorry if my response is a but messy, it's quite late 🙂 Feel free to ask follow up questions. Cheers, |
Beta Was this translation helpful? Give feedback.
-
Thanks for your quick and thorough reply. Feel free to turn this into a discussion (or close it) if you want, but i feel like you answered my question and addressed my concerns, so I'm not sure how useful it is. Either is fine by me :) I understand the design a bit better now, and will look more into the error_component_selector api. This is also undocumented, but i think your comment is a good starting point for exploration.
I know its in very early development and understandably you're doing this on your free time, so time an effort is limited. That is completely understandable. I hope both this and htmy gets enough traction so you're able to invest more time into it. Keep up the good work! |
Beta Was this translation helpful? Give feedback.
-
Thanks! Could you please mark the question as answered? And feel free to ask more questions! Actually, I think a positive discussion is also a contribution and I'm happy to keep this one, it can be helpful for others. Honestly, answering a question takes much less time (at least for me) then writing the docs, mainly because documentation requires extra thought, more detailed and correct technical explanation, example code, and so on. |
Beta Was this translation helpful? Give feedback.
Hi,
Thanks for taking the time to write such a detailed description. I'll start with a question: would it be okay for you to convert this issue to a discussion? Then, if an actual feature request comes out, we can create a new issue with just the requirements. Sorry, I just like to keep my issue trackers relatively tidy.
You are totally right about the lacking documentation on the integration. Unfortunately I work on these libs in my spare time, which is usually fairly limited. This is one of the reasons why I tend to recommend people (who seem to work on business projects) to get in contact for consulting. That way I have the time to put the basic app patterns in their codebase and also …