[Idea] allow repeated attribute extension!
bessey opened this issue · 8 comments
Hey guys, saw your pip package in an HN thread and it really scratched an itch for me. I've been playing around with it and Tailwind to see if I can build a Python native UI framework.
This script:
import htpy as h
button = h.button(".btn")
submit_input = h.input(".btn", type="submit")
checkbox_input = h.input(".checkbox", type="checkbox")
print(
h.div[
h.p["Hello, World!"],
button(".bottom")["Click me!"],
checkbox_input(name="test"),
submit_input(value="Submit"),
]
)
Outputs: (I'm adding newlines for readability)
<div>
<p>Hello, World!</p>
<button class="bottom">Click me!</button>
<input name="test"><input value="Submit">
</div>
Where as I would have expected successive Element()
calls to accumulate attributes, and intelligently merge accumulated classes, e.g.
<div>
<p>Hello, World!</p>
<button class="btn bottom">Click me!</button>
<input class="checkbox" type="checkbox" name="test">
<input class="btn" type="submit" value="Submit">
</div>
For what its worth, with just one line change I was able to get the attribute accumulation by prepending **self._attrs,
here.
Thoughts? I appreciate this can be worked around with the UI Component pattern you mention, but applying that at such a low level makes composition quite painful. Presumably you would end up almost never being able to use the clever element()[]
syntax since its all wrapped in method calls.
Interesting! Attributes would be updated/overridden and classes would be merged with a space then. Are there other attributes except for class that would need special treatment (I can't think of any of the top of my head)? Can you share more details on the UI framework that you are trying to build?
I can't think of any either but to be fair I'm dusting off my frontend skills after quite a few years of backend focus right now.
As for the framework, I might have oversold it. Basically I'm a long time Rails guy toying with this new era of type hinted Python web tooling (FastAPI, SQLModel, and now Htpy & Ludic on the views end of things). I am pretty sold on the productivity of Htmx and Tailwind, but I believe it needs the right templating system.
The problem I see is that in order to be productive while adopting "locality of behavior" (ref1 ref2) practices these tools embrace, you really need a templating system that makes rendering components as low friction as rendering primitive DOM elements. To me that's the thing React nailed, and I was trying to get to that level with your lovely system.
On reflection though, I don't think just my earlier suggestion is really going to cut it. Another idea I had was finding a way to make your __getitem__
trick work for components. I'm no Python pro, but could this be abstracted away with decorators, something like this?
@component # This modifies the call signature of the function to bootstrap_modal(*, attrs)[children]
def bootstrap_modal(*, children: Node) -> Element:
return div(".modal", tabindex="-1", role="dialog")[
div(".modal-dialog", role="document")[div(".modal-content")[children]]
]
@component
def bootstrap_header(*, closeable: bool, children: Node) -> Element:
rest = []
if closeable:
rest = button(
".close",
type="button",
data_dismiss="modal",
aria_label="Close",
)[span(aria_hidden="true")[Markup("×")]]
return div(".modal-header")[div(".modal-title")[children, *rest]]
@component
def bootstrap_body(*, children: Node) -> Element:
return div(".modal-body")[children]
@component
def bootstrap_footer(*, children: Node) -> Element:
return div(".modal-footer")[children]
bootstrap_modal[
bootstrap_header(closeable=true)["Hello, World!"],
bootstrap_body["Lorem ipsum"],
bootstrap_footer["Etc!"],
]
I smashed together a working POC of this decorator function. No type hinting (yet). I'm at the limits of my knowledge of Python already to be honest 😅
https://gist.github.com/bessey/36bb432288fc268a7ea59131509f8132
I gave it a shot and implemented a type safe with_children
. It adds "children" as the first argument:
from __future__ import annotations
import typing as t
from collections.abc import Callable
from dataclasses import dataclass
import htpy as h
P = t.ParamSpec("P")
R = t.TypeVar("R")
C = t.TypeVar("C")
@dataclass
class _ChildrenWrapper(t.Generic[C, R]):
_component_func: t.Any
_args: t.Any
_kwargs: t.Any
def __getitem__(self, children: C) -> R:
return self._component_func(children, *self._args, **self._kwargs) # type: ignore
def with_children(
component_func: Callable[t.Concatenate[C, P], R],
) -> Callable[P, _ChildrenWrapper[C, R]]:
def func(*args: P.args, **kwargs: P.kwargs) -> _ChildrenWrapper[C, R]:
return _ChildrenWrapper(component_func, args, kwargs)
return func
@with_children
def bs_button(children: str, style: t.Literal["success", "danger"]) -> h.Element:
return h.button(class_=["btn", f"btn-{style}"])[children]
@with_children
def article_section(children: h.Node, title: str) -> h.Node:
return [h.h1[title], children]
print(bs_button("danger")["Delete my account"])
print(h.div[article_section("htpy")[h.p["Write HTML in Python!"]]])
if t.TYPE_CHECKING:
reveal_type(bs_button("danger")["Delete my account"])
reveal_type(article_section("htpy")[h.p["Write HTML in Python!"]])
Result:
$ python examples/component_decorator.py
<button class="btn btn-danger">Delete my account</button>
<div><h1>htpy</h1><p>Write HTML in Python!</p></div>
$ mypy examples/component_decorator.py
examples/component_decorator.py:47: note: Revealed type is "htpy.Element"
examples/component_decorator.py:48: note: Revealed type is "Union[None, htpy._HasHtml, typing.Iterable[Union[None, builtins.str, htpy.BaseElement, htpy._HasHtml, typing.Iterable[...], def () -> ...]], def () -> Union[None, builtins.str, htpy.BaseElement, htpy._HasHtml, typing.Iterable[...], def () -> ...]]"
Success: no issues found in 1 source file
It also gives the correct return type so that you can also return lists/fragments.
Generally I dont think htpy should be too opinionated about different ways to structure components libraries. It should just focus on generating the HTML. Python is powerful enough to implement any kind of component structure. Writing docs and figuring out useful and best practices and practical patterns is key to use htpy in a good way. We are wrapping "children" in our own code in some components but we have been happy enough with just passing children as a key word argument. :)
I think this snippet could be added to the "common patterns" docs!
(Btw, very much agree about LoB. We are using Django(with types)+htpy+htmx+Alpine.js and find it very productive combination. Have been doing AngularJS/React+APIs for years. We ran into limitations of django templates with this new approach, that's how htpy was started.)
Wow, I didn't expect it to be possible to build that decorator without reaching into htpy
at all, that's really cool. With that in mind I can see why you wouldn't feel a need to include it in the library, makes sense.
Happy to make that docs PR if you'd like.
Thanks, that would be very nice! :)
Closing this since there is no clear way forward. Feel free to discuss ideas on how to structure htpy components / code in the Discussions and/or open a new issue with more concrete ideas!