Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Idea] allow repeated attribute extension! #6

Closed
bessey opened this issue Mar 23, 2024 · 8 comments
Closed

[Idea] allow repeated attribute extension! #6

bessey opened this issue Mar 23, 2024 · 8 comments

Comments

@bessey
Copy link

bessey commented Mar 23, 2024

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.

@pelme
Copy link
Owner

pelme commented Mar 24, 2024

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?

@bessey
Copy link
Author

bessey commented Mar 25, 2024

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("&times;")]]

    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!"],
]

@bessey
Copy link
Author

bessey commented Mar 25, 2024

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

@pelme
Copy link
Owner

pelme commented Mar 26, 2024

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!

@pelme
Copy link
Owner

pelme commented Mar 26, 2024

(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.)

@bessey
Copy link
Author

bessey commented Mar 26, 2024

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.

@pelme
Copy link
Owner

pelme commented Mar 27, 2024

Thanks, that would be very nice! :)

@pelme
Copy link
Owner

pelme commented Jul 17, 2024

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!

@pelme pelme closed this as completed Jul 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants