HTMX with Flask - Follow button

HTMX with Flask - Follow button

I haven't seen many examples of how to use HTMX with Flask, so I'm documenting little snippets from my own code for anyone who wishes to make use of this wonderful combination of technologies. I'm also using Bulma CSS framework to make things prettier while being easy to use.

In this example, I have a User model with the follow functionality implemented like in this tutorial :

followers = db.Table('followers',
    db.Column('follower_id', db.Integer, db.ForeignKey('users.id')),
    db.Column('followed_id', db.Integer, db.ForeignKey('users.id')),
    db.Column('ctime', db.DateTime, nullable=False, default=datetime.utcnow)
)

class User(db.Model, UserMixin):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(20), unique=True, nullable=False)

    # other fields and functions

    followed = db.relationship(
        'User', secondary=followers,
        primaryjoin=(followers.c.follower_id == id),
        secondaryjoin=(followers.c.followed_id == id),
        backref=db.backref('followers', lazy='dynamic'), lazy='dynamic')

    def follow(self, user):
        if not self.is_following(user):
            self.followed.append(user)
            db.session.commit()

    def unfollow(self, user):
        if self.is_following(user):
            self.followed.remove(user)
            db.session.commit()

    def is_following(self, user):
        return self.followed.filter(
            followers.c.followed_id == user.id).count() > 0

I have a user profile header where I wanted to put a follow button, so it's accessible from any user related page. At first I had the logic like this:

{% if user != current_user %}
    {% if current_user.is_following(user) %}
        <button class="button is-small is-primary is-light is-rounded mt-2">
           Unfollow
        </button>
    {% endif %}
    {% if not current_user.is_following(user) %}
        <button class="button is-small is-primary is-rounded mt-2">
            Follow
        </button>
    {% endif %}
{% endif %}

This would hide the button if the user was looking at their own profile and display the correct button on the page depending on whether the current user was already following the user whose page they were viewing.

Now to use HTMX, I needed three things:

  1. To include HTMX in the head of my page
  2. A URL to call which would update the database when the button is clicked
  3. A piece of HTML to swap in place of the current button to change its appearance

Including HTMX can be done by adding the following tag in the page head:

<script src="https://unpkg.com/htmx.org@1.5.0" integrity="sha384-oGA+prIp5Vchu6we2YkI51UtVzN9Jpx2Z7PnR1I78PnZlN8LkrCT4lqqqmDkyrvI" crossorigin="anonymous"></script>

To have a URL for the button to call, I needed a new route in my user routes:

users/routes.py

@users.route("/user/<string:username>/follow", methods=['PUT'])
def toggle_follow_user(username):
    user = User.query.filter_by(username=username).first_or_404()
    if current_user.is_following(user):
        current_user.unfollow(user)
        return render_template('follow-button.html', user=user)
    else:
        current_user.follow(user)
        return render_template('unfollow-button.html', user=user)

I decided to make two separate templates for this button, so I can just load the one that I need.

This function was doing what it needs to do in the back end, and returning the proper HTML to swap the button on the page.

I also replaced the logic on the user profile header to use these new button templates:

{% if user != current_user %}
    {% if current_user.is_following(user) %}
        {% include 'unfollow-button.html' %}
    {% endif %}
    {% if not current_user.is_following(user) %}
        {% include 'follow-button.html' %}
    {% endif %}
{% endif %}

And now all that was left to do was to sprinkle the HTMX parameters in the buttons.

I added the user's username to the ID of the button, then used url_for() to create a call to the new toggle_follow_user route in hx-put. The hx-target of the call would be the button itself, so it contains the same value as the button id. And since we're replacing the whole button with a new one, hx-swap is set to outerHTML.

This is what these new button templates look like:

follow-button.html

<button id="{{user.username}}-follow"
    class="button is-small is-primary is-rounded mt-2"
    hx-put="{{ url_for('users.toggle_follow_user', username=user.username) }}"
    hx-target="#{{user.username}}-follow"
    hx-swap="outerHTML"
    >Follow</button>

unfollow-button.html

<button id="{{user.username}}-follow"
    class="button is-small is-primary is-light is-rounded mt-2"
    hx-put="{{ url_for('users.toggle_follow_user', username=user.username)}}"
    hx-target="#{{user.username}}-follow"
    hx-swap="outerHTML"
    >Unfollow</button>

I realize that the differences between the two buttons are minor, just text and an extra CSS class, which I was thinking of changing into variables and passing them into just one template, but in the end I decided that this is easier to read and would make it simpler to modify later on if I decided to change buttons to look different.

I hope you found this helpful.

Cover photo by Jehyun Sung on Unsplash