Extending Comments on the Blog
Something came over me this week and I decided to extend my little commenting system to allow for threaded replies.
Backend
I detailed adding comments using Flask in a previous post and this builds on that work. For replies, I created an association between the original comment and any comment submitted which is flagged as a reply. It looks like this in the database:
```python
Create an association table to link two comments together
comment_replies = db.Table( "comment_replies", db.metadata, db.Column("original_id", db.Integer, db.ForeignKey("comment.id"), primary_key=True), db.Column("reply_id", db.Integer, db.ForeignKey("comment.id"), primary_key=True), )
class Comment(db.Model): # Rest of the model properties...
# Store the linked comments in a list on any comment by ID
replies = db.relationship(
"Comment",
secondary="comment_replies",
primaryjoin=(comment_replies.c.original_id == id),
secondaryjoin=(comment_replies.c.reply_id == id),
lazy="dynamic",
)
```
This links the requested comment to any other comment in the database. These are stored in a list which can be filtered and accessed in the template. To avoid making a new POST route for comments, replies are noted with a reply_id= querystring. If the query exists, the new comment is associated with the parent's ID.
Frontend
The frontend was more complicated to do. I originally built the comment module as a custom element, which worked well for that first implementation. It turned into a much more complicated problem in this case because of they way I was rendering comments with a template expression:
A thought from ${comment.name}
${comment.message}
``js
render(slug) {
if (this.comments) {
this.comments
.map((comment) => {
this.insertAdjacentHTML(
"afterend",
```
Comemnts were requested when the element loaded and returned as JSON. The comment.replies key existed for everything, but some arrays were empty. It was also a pain to inlcude expressions in the template string. After a bunch of trial and error, I decided that I was trying to use the custom element just because and not becaues it really gave me a good solution.
I ended up switching back to htmx to handle the commenting system. The custom element already used a network call to load data, so I'm not adding a request. I also get the added benefit of letting the server process the database and return formatted HTML with all nesting already handled.
htmx offers a load event which provides out-of-the-box lazy loading. The blog post will load before comments are requested, so there is no waiting to begin reading the content.
Since every comment can also have comments, this was a good place for some recursion. This led me to create my first Jinja macro which let me define a comment template once which can recurse through the entire reply tree if it exists.
```jinja2 {% macro render_comment(comment) -%}
{{ comment.message }}
{% if comment.has_replies %} {% for reply in comment.replies %} {% if reply.approved %} {{ render_comment(reply) }} {% endif %} {% endfor %} {% endif %}{% endmacro %} ```
The comment template returned by Flask is two lines and uses the macro to render any comment recursively, attaching approved replies to the parent:
jinja2
{% from 'shared/macros.html' import render_comment %}
{% for comment in comments%}
{{ render_comment(comment) }}
{% endfor %}
I was initially frustrated that I couldn't get the custom element to work the way I wanted, but once I decied to switch, this came together much faster and is more robust, I think. It's not clever, but it's easy to follow. I don't have to worry about Javascript rendering along with the server rendering posts and it all just plays nicely.
On to the next project...
Previous
Basement Shows and Lost Music
A thought from Alan Levine
2024-03-17 15:44:57Someone should leave a comment that can be replied to, so I am here to start that. While conceptually understanding what you are talking about I feel the rust of my own technical chops, you are into a whole tech realm on another plain. But always appreciate the writing about the how’s and why’s for what’s running underneath.
A thought from Felix the dog
2024-03-17 17:34:47I can reply here! Nice!
A thought from Kevin Spencer
2024-03-17 21:07:17Very nice, look multi threaded.
A thought from Brian
2024-03-17 18:28:00Nope, didn't disappear! Thanks Alan - I feel like a lot of this interest came from my WordPress tinkering back in 2012 when I first started getting interested in making stuff, and I remember you and Jim (via Reclaim) helping me unmuck some WP white screen of deaths I caused in the process.