Package: redmage
redmageRedmage is component based library for buildinghtmxpowered web applications.It is built on top of thestarletteweb framework.ExampleRedmage is meant to reduce the complexity of designing htmx powered applications by abstracting the need to explicitly register routes and configure the hx-* attributes for each interaction on your app.Consider the example below.from redmage import Component, Redmage, Target
from redmage.elements import Button, Div, H1, Script


app = Redmage()


class Counter(Component, routes=("/",)):
    count: int

    def __init__(self):
        self.count = 0

    async def render(self):
        return Div(
            H1(f"Clicked {self.count} times."),
            Button(
                "Add 1",
                click=self.add_one(),
            ),
            Script(src="https://unpkg.com/htmx.org@1.9.2"),
        )

    @Target.post
    def add_one(self):
        self.count += 1TheCountercomponent will add one to the count every time the button is clicked. If you're familiar with htmx, you might notice that this would usually require registering a new route in our backend, maybe something like/add_one. And in the html, we would have to explicitly add anhx-post,hx-target, and possibly morehx-*attributes.Redmage abstracts this using theTarget.postdecorator method. To the developer it simply looks like theadd_onemethod is executed when the button is clicked which updates the component state and causes the component to re-render.InstallationRedmage is available on pypi.pip install redmageRedmage applicationThe first thing you need to do is create an instance of the Redmage class.from redmage import Redmage


app = Redmage()At this point our app won't do anything because we haven't registered any routes by sublassing Component. But you can start it up using your favorite ASGI server like uvicorn.uvicorn <module>:<filename>:app.starletteApplication OptionsYou can pass the following keyword arguments to the Redmage constructor which  work as proxies to the underlying Starlette app.debugmiddlewareFirst ComponentLet's create a Component. In the example we just returned adivelement. This works but we're going to want to create a proper html page withhtml,header,bodytags etc.from redmage import Component, Redmage
from redmage.elements import Body, Doc, H1, Head, Html, Script, Title


app = Redmage()


class Index(Component, routes=("/",)):

    async def render(self):
        return Doc(
            Html(
                Head(
                    Title("Example"),
                ),
                Body(
                    H1("Hello Redmage"),
                    Script(src="https://unpkg.com/htmx.org@1.9.2"),
                ),
            )
        )This tells our app to register theIndexcomponent with the route "/". When you navigate tolocalhost:8000/an instance ofIndexwill be created and it's render method will be called to generate the html.TheComponentclass is abstract and has a single abstract base method,render, that must be implemented and return and instance ofredmage.elements.Element.ElementsRedmage internally uses python classes associated with each html tag (Div,Body,H1etc.). They can all be imported fromredmage.elements. Each of these classes subclassesredmage.elements.Element. Pass the elements inner html as positional arguments and add attributes with keyword arguments.from redmage.elements import Div


div = Div(
    H1("Title"),
    P("paragraph"),
    my_attribute="a cool attribute value"
    _class="my-class"
)


print(div)


# output
# <div my-attribute="a cool attribute value" class="my-class">
#    <h1>Title</h1>
#    <p>paragraph</p>
# </div>Notice that underscores in keywords are converted to hyphens and leading underscores are stripped so you can avoid conflict with Python keywords like class.Additionally, a number of htmx specific keywords are supported.keywordhtmx attributetypedefaultdocumentationnotestriggerhx-triggerstr or TriggerNoneSee Trigger section belowswaphx-swapstrHTMXSwap.OUTER_HTMLredmage.types.HTMXSwap enumswap_oobhx-swap-oobboolFalseconfirmhx-confirmboolFalseboosthx-boostboolFalseonhx-onstrNoneindicatorN/AboolFalsetargethx-target, hx-<method>TargetNoneSee Target section belowclickhx-target, hx-<method>, hx-triggerTargetNoneSee Trigger keywords section belowsubmithx-target, hx-<method>, hx-triggerTargetNoneSee Trigger keywords section belowchangehx-target, hx-<method>, hx-triggerTargetNoneSee Trigger keywords section belowmouse_overhx-target, hx-<method>, hx-triggerTargetNoneSee Trigger keywords section belowmouse_enterhx-target, hx-<method>, hx-triggerTargetNoneSee Trigger keywords section belowloadhx-target, hx-<method>, hx-triggerTargetNoneSee Trigger keywords section belowintersecthx-target, hx-<method>, hx-triggerTargetNoneSee Trigger keywords section belowrevealedhx-target, hx-<method>, hx-triggerTargetNoneSee Trigger keywords section belowRedmage doesn't have any support for a specific template engine, but it should be pretty easy to build aComponentsubclass to support one, such as Jinja2. See the todo_jinja2 example.Nesting ComponentsComponents can be easily nested by using them just like you would use any other htmlElementobject.from redmage import Redmage
from redmage.elements import Body, Doc, H1, Head, Html, Script, Title


app = Redmage()


class Index(Component, routes=("/",)):

    async def render(self):
        return Doc(
            Html(
                Head(
                    Title("Todo App"),
                ),
                Body(
                    ChildComponent(),
                    Script(src="https://unpkg.com/htmx.org@1.9.2"),
                ),
            )
        )


class ChildComponent(Component):

    async def render(self):
        return H1("Child Component")In the following examples I'm going to assume that the components we write are rendered in anIndexcomponent like above so it will be ommited.Targetshtmx targetsSimple ExampleThus far, our components have been static. With Redmage we have the ability to react to events on the frontend and update the state of our component using htmx. To do, this we useredmage.Targetclass. It has decorator attributes associated with the following HTTP methods.getpostputpatchdeleteThese decorators wrap methods of our component. We can pass the output of these methods to an Element'stargetkeyword argument. Check out the example below.from redmage import Target


class ClickComponent(Component):

    def __init__(self):
        self.count = 0

    async def render(self):
        return Button(
            self.count,
            target=self.set_count(self.count + 1)
        )

    @Target.post
    def set_count(self, count: int):
        self.count = countWhen the button is clicked an HTTP POST request is issued to our application. Theset_countmethod is ran, updating the component state, and the component is re-rendered and swapped into the DOM.By default, if the target method returnsNonethenselfis rendered. We could also explicitly return self, another component, or a tuple of components (this can be useful in conjunction with out of bounds swaps).Target method argumentsAll of the arguments of a render method, exceptself, require type hints so that Redmage can build a route. Positional or keyword (and keyword only) arguments are added to the route as path arguments or query parameters respectively.If the request has a body, the first argument must be apositional only argument. It's type must be a class that de-serializes the body by passing the fields as keyword arguments to it's constructor, like a dataclass.@dataclass
class UpdateMessageForm:
    content: str


class Message(Component):

    def __init__(self, content):
        self.content = content

    async def render(self):
        return Div(
            P(f"{self.content=}"),
            Form(
                Input(
                    type="text",
                    id="content",
                    name="content",
                ),
                Button("Update message", type="submit"),
                target=self.update_message(),
            ),
        )

    @Target.post
    def update_message(self, form: UpdateMessageForm, /):
        self.content = form.contentRedmage (and the underlying starlette app) must know how to convert your types to strings so that they can be encoded in URLs and converted back to your object types.Use the starlette app to register your custom converters according to it'sdocumentation. Redmage will use these converters.Redmage adds a couple of custom convertors that starlette does not provide. One is a boolean converter to convertbooltype. The other is a custom string converter that is used forstr. Since Redmage building URLs we need to convert the empty string to__empty__.Component StateThe component's state will also be encoded in the url so it can be recreated when the request is issued. Only attributes that have a class annotation will be included. The same converters described above will be used to serialize/de-serialize the component's attributes.@dataclass
class UpdateMessageForm:
    content: str


class MessageAndCounter(Component):
    content: str
    count: int

    def __init__(self, content, count):
        self.content = content
        self.count = count

    async def render(self):
        return Div(
            P(f"{self.content=}"),
            Form(
                Input(
                    type="text",
                    id="content",
                    name="content",
                ),
                Button("Update message", type="submit"),
                target=self.update_message(),
            ),
            P(f"{self.count=}"),
            Button("Add 1", click=self.update_count(self.count + 1)),
        )

    @Target.post
    def update_message(self, form: UpdateMessageForm, /):
        self.content = form.content

    @Target.post
    def update_count(self, count: int):
        self.count = countIn this example, if we didn't add the class annotations, when the message was updated the count would not be set and vice versa, breaking our component.Triggershtmx triggersRedmage provides a very thin abstraction overhx-triggerattributes. An element'striggerkeyword argument can be used to tell Redamge which event type should trigger a component update. You can just pass a string value of theeventname.class HoverCount(Component):
    def __init__(self):
        self.count = 0

    async def render(self):
        return Div(
            self.count,
            target=self.set_count(self.count + 1),
            trigger="mouseover",
        )

    @Target.post
    def set_count(self, count: int):
        self.count = countNow when you mouse over the number the count increases by one. Check the htmx documentation because it provides a number of modifiers that can be used to customize the trigger behavior.Redmage has built-in classes that can be used to build triggers as well.redmage.triggers.Triggerredmage.triggers.TriggerModifierredmage.triggers.DelayTriggerModifierredmage.triggers.ThrottleTriggerModifierTODO document redmage.triggers.* classes.Below is an example of usingTriggerclasses to add a delay to a trigger.from redmage.triggers import DelayTriggerModifier, Trigger
from redmage.types import HTMXTrigger


class HoverCount(Component):
    def __init__(self):
        self.count = 0

    async def render(self):
        trigger = Trigger(HTMXTrigger.MOUSEOVER, DelayTriggerModifier(1000))

        return Div(
            self.count, target=self.set_count(self.count + 1), trigger=trigger
        )

    @Target.post
    def set_count(self, count: int):
        self.count = countNow the count will update after one second instead of immediately.Trigger keywordsTheElementclass has a number of keyword arguments associated with events that we can passTargetobjects too. This can simplify our code by not having to add both target and trigger keyword arguments. Below is an example.class HoverCount(Component):
    def __init__(self):
        self.count = 0

    async def render(self):
        return Div(
            self.count,
            mouse_over=self.set_count(self.count + 1),
        )

    @Target.post
    def set_count(self, count: int):
        self.count = countTheElementclass currently provides keyword arguments for the following events:clicksubmitchangemouse_overmouse_enterloadintersectrevealedRender ExtensionsWe can use render extensions to inject objects as positional arguments to eachrendermethod in our application.TODO give an example of how to register a render extension and when you might use one.ExamplesTODO add cool examples.