As I mentioned in my previous post, my blogging app written in Flask allows users to post comments under each post. The post page contains a little comment form which consists of only one text box and a submit button. I have originally implemented it to redirect the user to the same page on submit, so the new comment would show in the comment section under the post and the form would get cleared. It all worked well, but I thought it would be faster and feel better if I spruced it up with a little HTMX.
Starting point
This is what my form looked like originally:
<article id="write-comment" class="media">
<div class="media-content">
<form method="POST" action="">
{{ form.hidden_tag() }}
<div class="field">
{{ form.content.label(class="label") }}
{{ form.content(class="textarea", rows="4", placeholder="Add a comment...") }}
</div>
<nav class="level is-mobile ">
<div class="level-left"></div>
<div class="level-right">
<div class="level-item">
{{ form.submit(class="button is-primary") }}
</div>
</div>
</nav>
</form>
</div>
</article>
If you ignore all the Bulma classes, it is a very basic form. When submitted it would hit this route:
@posts.route("/post/<int:post_id>", methods=['GET', 'POST'])
def post(post_id):
post = Post.query.get_or_404(post_id)
form = CommentForm()
if form.validate_on_submit():
comment = Post(title=f"Re: {post.title}", content=form.content.data, author=current_user, op=post)
db.session.add(comment)
db.session.commit()
return redirect(url_for('posts.post', post_id=post.id))
return render_template('post.html', title=post.title, post=post, user=post.author, form=form)
This was the same route that handled GET method for the initial load and would be used again after redirecting to the same post after the new comment was processed.
Adding HTMX
I already had HTMX included in the head of the page by adding this line:
<script src="https://unpkg.com/htmx.org@1.5.0" integrity="sha384-oGA+prIp5Vchu6we2YkI51UtVzN9Jpx2Z7PnR1I78PnZlN8LkrCT4lqqqmDkyrvI" crossorigin="anonymous"></script>
Next thing I needed was to add HTMX to the form itself. All it took was to change the form's opening tag to this:
<form hx-post="{{ url_for('posts.add_comment', post_id=post.id) }}" hx-swap="afterbegin" hx-target="#comments">
And then separate the POST and GET methods into two different routes with the same URL:
@posts.route("/post/<int:post_id>", methods=['GET'])
def post(post_id):
post = Post.query.get_or_404(post_id)
form = CommentForm()
return render_template('post.html', title=post.title, post=post, user=post.author, form=form)
@posts.route("/post/<int:post_id>", methods=['POST'])
def add_comment(post_id):
post = Post.query.get_or_404(post_id)
form = CommentForm()
if form.validate_on_submit():
comment = Post(title=f"Re: {post.title}", content=form.content.data, author=current_user, op=post)
db.session.add(comment)
db.session.commit()
return render_template('comment.html', comment=comment)
return "Error happened."
This new add_comment() function used the same template fragment comment.html that the post template uses for listing previously posted comments to the page.
At this point I decided to test and see what it does, and it worked! All it took was one line of HTMX and a bit of mostly copy/paste refactoring of the routes for my page to be more than 10 times quicker.
Resetting the form
After getting over my giddiness caused by the ease of this implementation, I noticed an annoying problem. After submitting the form, my comment stayed in the textarea field. I needed to reset this, but I had no idea how to do it, so I went to the HTMX Discord channel and asked the pros who answered me almost immediately. Wonderful people.
They told me to use the htmx:afterRequest
event to call form.reset()
and gave me some possible ways to implement this. Since I've already used jquery in other places on the blog, I've decided to go with this little script which I've put in comment_form_clear.js file.
$("#write-comment").on("htmx:afterRequest", function(e) {
$("#write-comment form")[0].reset();
})
I have included this under my comment form like this:
<script src="{{ url_for('static', filename='js/comment_form_clear.js') }}" type="application/javascript"></script>
Unexpected hiccup
Then I got stuck for a little while trying to figure out how to allow inline style created by HTMX in my Content-Security-Policy header. I'm using flask-talisman to secure the app, and it was preventing the scripts and styles from being used. I ended up adding the hash code for the style reported in the browser console in my CSP definition:
csp = {
'default-src': [
'\'self\'',
...
],
'style-src': ['\'self\'',
'https://bulma.io',
...
"'sha256-the_ridiculously_long_hash_code_found_in_error_message_in_browser_console'"
],
'script-src': ['\'self\'',
...
]
}
Notice that the hash has to have double quotes followed by single quotes, so single quotes can be included in the actual header. That took a while to troubleshoot.
After this, everything was working very quickly. The comments appeared instantaneously under the post and the form was fresh and ready to be used again.
Unfinished business
This implementation works well if everything goes well, but I will need to spend some time on error handling in case things don't go as planned. I also want to add some flair to the whole thing maybe with a loading animation or fade in effect.
Photo by Volodymyr Hryshchenko on Unsplash