HTMX, Hyperscript and Flask - Slideshow gallery

Photo by Zalfa Imani on Unsplash

HTMX, Hyperscript and Flask - Slideshow gallery

Continuing work on my project in Flask and Bulma, I needed a neat way to display a photo gallery. There are plenty of premade gallery setups that I could have used, but I wanted to make something using Bulma, Flask, HTMX, and Hyperscript as much as possible. This led to a lot of frustration trying to make Bulma do something it wasn't quite capable of doing.

I wanted to display pictures in a neat organized manner where it would be easy to see a whole bunch of them at once, in thumbnail form, and then to show them in full screen when they are clicked. Most places around the net have this solved by using some kind of modal. Bulma does have the modal capability, which I intended to use for this purpose. However, the way Bulma is designed to show images is to make them conform to some preset aspect ratios regardless of their actual proportions. After trying everything I could think of, I ended up adding some of my own CSS to show these full-screen images properly.

To make a thumbnail gallery, I created square thumbnails out of all the uploaded images. Then I displayed the thumbnails using these templates:

In user_pics.html:

<div id="pics" class="columns is-multiline">
  <!-- Show all the pics -->
  {% for pic in pics.items %}
  {% include 'photo.html' %}
  {% endfor %}
</div>

This creates a row of columns and the class .is-multiline makes it so that if any columns don't fit in the first row, they will continue in the next row. It's pretty neat.

Then in photo.html I defined the columns in such a way that each photo was in a column by itself. I've set the size of the thumbnail by using the column width modifier classes .is-one-quarter-desktop and .is-half-tablet.

<div class="column is-one-quarter-desktop is-half-tablet">
    <div class="card">
        <div class="card-image">
            <a  hx-get="{{ url_for('users.img_show', img_id=pic.id, username=user.username, page=page) }}"
                hx-trigger="click"
                hx-target="#modal"
                hx-swap="innerHTML">
            <figure class="image mx-0 px-0">
                <img src="{{ url_for('static', filename='imgs/' + pic.thumbnail) }}" alt="">
            </figure>
            </a>
        </div>
    </div>
</div>

I still haven't decided if I want to show some text with each picture or not, but that is why I used .card and .card-image classes. If I change my mind later I can easily add a .card-content <div/> to show a caption under each thumbnail.

Calling the modal

As you can see above, there is some HTMX in the link for each picture which is used to open the modal and fetch the slideshow gallery. To give a target to this call, I added the following at the end of user_pics.html:

<!-- Modal -->
<div id="modal"></div>

The HTMX calls the users.img_show function in routes.py for users Blueprint. This is what this function looks like:

@users.route("/user/<string:username>/img/<int:img_id>", methods=['GET'])
def img_show(img_id, username):
    page = request.args.get('page', 1, type=int)
    user = User.query.filter_by(username=username).first_or_404()
    pics = Picture.query.filter_by(author=user)\
        .order_by(Picture.date_posted.desc())\
        .paginate(page=page, per_page=PICS_PER_PAGE)
    if pics:
        return render_template('modal_img_show.html', pics=pics, img_id=img_id)
    else:
        return "No pic found."

I use the user and page information to load all of the images visible on the current page, then render and return modal_img_show.html template with the ID of the selected image. This is where the whole slideshow gallery resides.

<div id="modal_img_show" class="modal is-fullwidth is-active">
    <div class="modal-background cancel" _="on click toggle .is-active on #modal_img_show"></div>
    <div id="img_show" class="modal-content img_show_modal is-vcentered">

        {% if pics.total > 0 %}

        {% for pic in pics.items %}

        <img class="img_show {{ 'is-active' if pic.id==img_id }}" src="{{ url_for('static', filename='imgs/' + pic.file_name) }}">

        {% endfor %}

        <!-- Next and previous buttons -->
        <a class="prev" 
            _="on click 
                set currImg to previous <img.is-active/> then 
                toggle .is-active on currImg then
                set prevImg to previous <img.img_show/> from currImg with wrapping then 
                toggle .is-active on prevImg then
            end">&#10094;</a>
        <a class="next" 
            _="on click 
                set currImg to previous <img.is-active/> then 
                toggle .is-active on currImg then
                set nextImg to next <img.img_show/> from currImg with wrapping then 
                toggle .is-active on nextImg then
            end">&#10095;</a>

        {% endif %}

    </div>
    <button class="modal-close is-large" aria-label="close" _="on click toggle .is-active on #modal_img_show"></button>
</div>

Let's examine the interesting parts of this code.

Setting up the modal

Since this is a Bulma modal, the first line takes care of setting it up as such. It also sets it to .is-active so it can be visible as soon as it loads.

The second line defines the background of the modal, which is dark but slightly transparent. I've added some Hyperscript to it to close the modal if the background is clicked:

<div class="modal-background cancel" _="on click toggle .is-active on #modal_img_show"></div>

For this application, Hyperscript was very simple and intuitive. I added the same code to the close button at the end of the modal (which shows up in the top right corner).

<button class="modal-close is-large" aria-label="close" _="on click toggle .is-active on #modal_img_show"></button>

With the surrounding parts of the modal defined, it was time to focus on .modal-content where the fun stuff happens. Before doing anything inside it, I had to define some CSS to stretch the content across the full screen. Well, 95% of it.

.img_show_modal {
  height: 95%;
  overflow: hidden;
}

With the container showing up properly, I needed some way to show the images. I used W3 Schools' example of a slideshow gallery as a starting point. I didn't need the thumbnails shown under the current picture, so I omitted that part.

I also omitted most of the styling factors for the gallery, leaving just the <img/> tags. I iterated through the images and loaded them all up, but only the current image was set to .is-active.

{% for pic in pics.items %}

<img class="img_show {{ 'is-active' if pic.id==img_id }}" src="{{ url_for('static', filename='imgs/' + pic.file_name) }}">

{% endfor %}

Here I again defined some CSS to make the picture cover the available area without distorting its original aspect ratio and to define that .is-active means to display the image.

.img_show {
  display: none;
  -o-object-fit: contain;
     object-fit: contain;
  height: 100%;
  width: 100%;
}

.img_show.is-active {
  display: block;
}

The last part was attaching some Hyperscript to the next and previous buttons to move through the slides.

<!-- Next and previous buttons -->
<a class="prev" 
    _="on click 
        set currImg to previous <img.is-active/> then 
        toggle .is-active on currImg then
        set prevImg to previous <img.img_show/> from currImg with wrapping then 
        toggle .is-active on prevImg then
    end">&#10094;</a>
<a class="next" 
    _="on click 
        set currImg to previous <img.is-active/> then 
        toggle .is-active on currImg then
        set nextImg to next <img.img_show/> from currImg with wrapping then 
        toggle .is-active on nextImg then
    end">&#10095;</a>

This part was a little tricky. Most of the Hyperscript documentation is written with the assumption that the actions are being done around the object that was listening for an event. In this case, the button is listening, and then the actions need to happen to the active image and images around it.

Once I figured out that I could put the currently active image into a variable, I was able to find the images next to it. The steps were then intuitive:

  1. Find the currently active image with previous <img.is-active/> which works because the links to previous/next buttons are right under all the <img/> tags in the DOM, so the previous <img/> that has .is-active is the one we're looking for

  2. Put it into currImg variable

  3. Remove .is-active class from it to hide it

  4. Find the previous/next image to the current one with previous <img.img_show/> from currImg with wrapping or next <img.img_show/> from currImg with wrapping

  5. Put them into appropriately named variables prevImg or nextImg

  6. Add.is-active class to make them visible

And there you have it. It was a fun little project that let me explore a few more features of Hyperscript.