Tags in Grids (htmx solution!)

95 views
Skip to first unread message

Vincent Chevrier

unread,
Mar 15, 2025, 10:34:14 PMMar 15
to py4web
Hi all,

sharing something I've been working on. This extends the existing Tags objects to allow removal and addition of tags in a grid interactively. It relies on htmx (and the styling is Bulma). I'm quite happy with the user experience and the coding experience. The "enable" call adds an endpoint that manages the htmx calls, and then you simply add it as a column. The only other thing is you need to add the script  htmx tag at the top of layout.html. I created this because I was unhappy with the Tags experience in the web ui, but found them very convenient in code.

this is the coding experience:

db.define_table('book',
    Field('author','string'),
    Field('title','string'))

if not db(db.book.id > 0).count() > 0:
    db.book.insert(author="Tolstoy", title="War and Peace")
    db.book.insert(author="Card", title="Ender's Game")

tags = HtmxTags(db.book)

# makes the tag htmx endpoint used by the tag column
tags.enable(uses=(db, session, auth))

@action('book', method=['POST', 'GET'])
@action('book/<path:path>', method=['POST', 'GET'])
@action.uses('generic.html', db, session, auth)
def book_grid(path=None):
    columns = [
        db.book.author,
        db.book.title,
        tags.tag_column()]

    grid = Grid(path, db.book,
            formstyle=FormStyleBulma,
            columns=columns,
            grid_class_style=GridClassStyleBulma)

    return dict(grid=grid)


this gives the following UI experience
htmxtags.gif
the extended class looks like this:
class HtmxTags(Tags):
    def tag_column(self) -> Column:
        """Component to be used in Grid

        Returns:
            Column: column object instance that returns the htmx
        """
        return Column(
            self.name,
            lambda row: self.htmx_form(row.id),
            required_fields=[self.table.id],
        )

    def htmx_form(self, record_id: int):
        """html component for tag POST and DELETE via htmx
        to be functional the tag_endpoint has to be exposed by a controller

        Args:
            record_id (int): record ID

        Returns:
            DIV: html
        """
        endpoint = f"/{request.app_name}"
        tag_table = self.tag_table
        tag_tablename = tag_table._tablename
        db = tag_table._db

        inline_id = f"{tag_tablename}-{record_id}"
        spans = []
        rows = db(tag_table.record_id == record_id).select()
        # return [row.tagpath.strip("/") for row in rows]

        for row in rows:
            # tag_row = self.tag_table[row.tag_id]
            tag_html_id = f"{tag_tablename}-{record_id}-{row.id}"
            hx_delete = f"{endpoint}/htmx/{tag_tablename}/{row.id}"
            button = BUTTON(
                _class="delete is-small",
                **{
                    "_hx-delete": hx_delete,
                    "_type": "button",
                    "_hx-on:click": f'document.getElementById("{tag_html_id}").remove()',
                },
            )
            span = SPAN(row.tagpath.strip("/"), button, _class="tag", _id=tag_html_id)
            spans.append(span)
        div_tags = DIV(*spans, _class="tags")

        input_div = DIV(
            P(
                INPUT(
                    **{
                        "_class": "input is-small",
                        "_name": "tag",
                        "_type": "text",
                        "_placeholder": "enter tag",
                    }
                ),
                _class="control",
            ),
            P(
                BUTTON(
                    SPAN(I(_class="fas fa-check"), _class="icon is-small"),
                    **{
                        "_class": "button is-small is-success",
                        "_hx-post": f"{endpoint}/htmx/{tag_tablename}/{record_id}",
                        "_hx-target": f"#{inline_id}",
                        "_hx-include": "closest div",
                        "_hx-trigger": "click",
                        "_hx-swap": "outerHTML",
                    },
                ),
                _class="control",
            ),
            _class="field has-addons",
        )
        return DIV(div_tags, input_div, _id=inline_id)

    def enable(self, uses: Tuple = ()) -> None:
        """Register the htmx endpoint needed for using the tag_column in a Bulma grid

        Args:
            uses (Tuple, optional): uses that are passed ot the action.uses
                decorator. Defaults to ().

        """
        route = f"htmx/{self.tag_table._tablename}/<path:path>"

        @action(route, method=["POST", "GET", "DELETE"])
        @action.uses(*uses)
        def _(path=None):
            # if the method is DELETE the ID is for the join table
            if request.method == "DELETE":
                tag_id = int(path)
                tag_row = self.tag_table[tag_id]
                record_id = tag_row.record_id
                tag_row.delete_record()
                return self.htmx_form(record_id)

            # if the method is POST the ID is for record table
            elif request.method == "POST":
                record_id = int(path)
                post_vars = request.POST or {}
                tag = post_vars.get("tag")
                if tag:
                    self.add(record_id, tag)
                    return self.htmx_form(record_id)





Massimo

unread,
Mar 16, 2025, 12:47:36 AMMar 16
to py4web
Nice. How about adding this example to the docs?

Jorge Salvat

unread,
Aug 16, 2025, 7:00:03 AMAug 16
to py4web
Hi Vincent,

your HtmxTags class looks really nice and usefull

Tried to implement your code but does not work. 
Which is the script needed at the top of layout.html ???
Where you define the 'default' column ???

Thanks for sharing

Vincent Chevrier

unread,
Aug 17, 2025, 9:10:45 PMAug 17
to Jorge Salvat, py4web
Hi Jorge,

HtmxTags is a subclass of Tags from pydal.tools.tags 
"default"  is defined in the init of Tags:
class Tags:
    def __init__(self, table, name="default", tag_table=None):

So I could've changed the column title like this
tags = HtmxTags(db.book, name="My Column Title")

The head of my layout.html where Bulma css and the HTMX are pulled:
<head>
  <base href="[[=URL('static')]]/">
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="shortcut icon" href="favicon.ico" />
    integrity="sha512-1PKOgIY59xJ8Co8+NE6FZ+LOAZKjy+KY8iq0G4B3CyeY6wYHN3yt9PW0XpSriVlkMXe40PTKnXrLnZ9+fkDaog=="
    crossorigin="anonymous" />
    integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
    crossorigin="anonymous"></script>
  [[block page_head]]<!-- individual pages can customize header here -->[[end]]
</head>

thanks
Vincent

--
You received this message because you are subscribed to the Google Groups "py4web" group.
To unsubscribe from this group and stop receiving emails from it, send an email to py4web+un...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/py4web/710419f6-5411-41f0-a12e-fda9b40c4710n%40googlegroups.com.
Reply all
Reply to author
Forward
0 new messages