Skip to main content

A component based library for building htmx powered applications.

Project description

redmage

run-tests

Redmage is component based library for building htmx powered web applications.

It is built on top of the starlette web framework.

Example

Redmage 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 += 1

The Counter component 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 an hx-post, hx-target, and possibly more hx-* attributes.

Redmage abstracts this using the Target.post decorator method. To the developer it simply looks like the add_one method is executed when the button is clicked which updates the component state and causes the component to re-render.

Installation

Redmage is available on pypi.

pip install redmage

Redmage application

The 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.starlette

Application Options

You can pass the following keyword arguments to the Redmage constructor which work as proxies to the underlying Starlette app.

  • debug
  • middleware

First Component

Let's create a Component. In the example we just returned a div element. This works but we're going to want to create a proper html page with html, header, body tags 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 the Index component with the route "/". When you navigate to localhost:8000/ an instance of Index will be created and it's render method will be called to generate the html.

The Component class is abstract and has a single abstract base method, render, that must be implemented and return and instance of redmage.elements.Element.

Elements

Redmage internally uses python classes associated with each html tag (Div, Body, H1 etc.). They can all be imported from redmage.elements. Each of these classes subclasses redmage.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.

keyword htmx attribute type default documentation notes
trigger hx-trigger str or Trigger None See Trigger section below
swap hx-swap str HTMXSwap.OUTER_HTML redmage.types.HTMXSwap enum
swap_oob hx-swap-oob bool False
confirm hx-confirm bool False
boost hx-boost bool False
on hx-on str None
indicator N/A bool False
target hx-target, hx-<method> Target None See Target section below
click hx-target, hx-<method>, hx-trigger Target None See Trigger keywords section below
submit hx-target, hx-<method>, hx-trigger Target None See Trigger keywords section below
change hx-target, hx-<method>, hx-trigger Target None See Trigger keywords section below
mouse_over hx-target, hx-<method>, hx-trigger Target None See Trigger keywords section below
mouse_enter hx-target, hx-<method>, hx-trigger Target None See Trigger keywords section below
load hx-target, hx-<method>, hx-trigger Target None See Trigger keywords section below
intersect hx-target, hx-<method>, hx-trigger Target None See Trigger keywords section below
revealed hx-target, hx-<method>, hx-trigger Target None See Trigger keywords section below

Redmage doesn't have any support for a specific template engine, but it should be pretty easy to build a Component subclass to support one, such as Jinja2. See the todo_jinja2 example.

Nesting Components

Components can be easily nested by using them just like you would use any other html Element object.

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 an Index component like above so it will be ommited.

Targets

htmx targets

Simple Example

Thus 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 use redmage.Target class. It has decorator attributes associated with the following HTTP methods.

  • get
  • post
  • put
  • patch
  • delete

These decorators wrap methods of our component. We can pass the output of these methods to an Element's target keyword 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 = count

When the button is clicked an HTTP POST request is issued to our application. The set_count method is ran, updating the component state, and the component is re-rendered and swapped into the DOM.

By default, if the target method returns None then self is 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 arguments

All of the arguments of a render method, except self, 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 a positional 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.content

Redmage (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's documentation. Redmage will use these converters.

Redmage adds a couple of custom convertors that starlette does not provide. One is a boolean converter to convert bool type. The other is a custom string converter that is used for str. Since Redmage building URLs we need to convert the empty string to __empty__.

Component State

The 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 = count  

In 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.

Triggers

htmx triggers

Redmage provides a very thin abstraction over hx-trigger attributes. An element's trigger keyword argument can be used to tell Redamge which event type should trigger a component update. You can just pass a string value of the event name.

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 = count

Now 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.Trigger
  • redmage.triggers.TriggerModifier
  • redmage.triggers.DelayTriggerModifier
  • redmage.triggers.ThrottleTriggerModifier

TODO document redmage.triggers.* classes.

Below is an example of using Trigger classes 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 = count

Now the count will update after one second instead of immediately.

Trigger keywords

The Element class has a number of keyword arguments associated with events that we can pass Target objects 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 = count

The Element class currently provides keyword arguments for the following events:

  • click
  • submit
  • change
  • mouse_over
  • mouse_enter
  • load
  • intersect
  • revealed

Render Extensions

We can use render extensions to inject objects as positional arguments to each render method in our application.

TODO give an example of how to register a render extension and when you might use one.

Examples

TODO add cool examples.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

redmage-0.5.0.tar.gz (17.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

redmage-0.5.0-py3-none-any.whl (16.6 kB view details)

Uploaded Python 3

File details

Details for the file redmage-0.5.0.tar.gz.

File metadata

  • Download URL: redmage-0.5.0.tar.gz
  • Upload date:
  • Size: 17.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.7.1 CPython/3.10.12 Linux/5.15.0-92-generic

File hashes

Hashes for redmage-0.5.0.tar.gz
Algorithm Hash digest
SHA256 6fffaeb91552cbcc4499e9917bd87c3e3b12f1ef00ed6b11be26174e95e6cce0
MD5 c82e1de321d828dc7b08f6164bef04bf
BLAKE2b-256 3c30a0cc33d0549dae6976585199a37b35dc11133ec11b97116314d263282368

See more details on using hashes here.

File details

Details for the file redmage-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: redmage-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 16.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.7.1 CPython/3.10.12 Linux/5.15.0-92-generic

File hashes

Hashes for redmage-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 543a5ec8100dacbea89cfa6fa6c98dcaf167299454f622fc1959325bbf3e1e35
MD5 c4b5b1e71a388c5043d6ed8053c1b048
BLAKE2b-256 689de14d2f286b89ac7af71fec1b6dd26c211e3e15d6e6643ab12eb6fbc53327

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page