1

I am building a comment system on my blog and I am rendering existing comments like this:

{% for comment in comments %}

                    <div id="task-comments" class="pt-4">
                        <!--     comment-->
                        <div
                            class="bg-white rounded-lg p-3  flex flex-col justify-center items-center md:items-start shadow-lg mb-4">
                            <div class="flex flex-row justify-center mr-2">
                                <img alt="avatar" width="48" height="48"
                                    class="rounded-full w-10 h-10 mr-4 shadow-lg mb-4"
                                    src="https://cdn1.iconfinder.com/data/icons/technology-devices-2/100/Profile-512.png">
                                <h3 class="text-purple-600 font-semibold text-lg text-center md:text-left ">{{
                                    comment.author['name']|e }}</h3>
                            </div>


                            <p style="width: 90%" class="text-gray-600 text-lg text-center md:text-left ">{{
                                comment.content|e }} </p>

                        </div>
                        <!--  comment end-->
                        <!--     comment-->

                        <!--  comment end-->
                    </div>
                    {% endfor %}

The problem here is that when I post a comment (using a FastAPI route), I don't know how to get the updated list of comments. I understand that Jinja may not be the best tool for this and have considered using Alpine JS x-for loop, but would love to know if there was a way to do this in Jinja natively.

Thanks!!

Chris
  • 18,724
  • 6
  • 46
  • 80
aj91
  • 23
  • 4
  • What is the actual problem? Comments do not save when passed to url, or jinja doesn't show them after a page refresh? – sudden_appearance Feb 04 '22 at 18:55
  • Jinja shows them after a page refresh but I want to know how to either trigger that refresh manually via my FastAPI route or have an elegant way to add the posted comment as an element in the dom without refreshing – aj91 Feb 04 '22 at 19:27
  • This is the point of javascript and ajax (or plain xhr) request to server with `success` callback function, which will either trigger page refresh or rebuild dom on a given response data – sudden_appearance Feb 04 '22 at 19:32
  • Right now this is what I'm returning ' return { "status" : "SUCCESS", "data" : req_info } ' Should I change this to trigger a refresh? – aj91 Feb 04 '22 at 19:56
  • I am talking not about backend. Your frontend part (Jinja templates which are sent to client) must call api endpoint to create comment. If you are answering with data, then you can update your dom with javascript without page refresh. Or you can just trigger a refresh a page (again using javascript) when you get a success code – sudden_appearance Feb 04 '22 at 20:01

1 Answers1

2

This sounds like a use case for WebSockets. The below is based on the example given in the documentation above, and can handle multiple connections, broadcasting the newly added comment to all the connected clients. Thus, if you open http://127.0.0.1:8000/ in multiple tabs in your browser, and add a new comment using one of these connections, every other will also receive the new comment. If you don't want to broadcast the message, then you could instead use await manager.send_personal_message(data, websocket).

app.py

from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
from fastapi.templating import Jinja2Templates
import uvicorn

class ConnectionManager:
    def __init__(self):
        self.active_connections: List[WebSocket] = []

    async def connect(self, websocket: WebSocket):
        await websocket.accept()
        self.active_connections.append(websocket)

    def disconnect(self, websocket: WebSocket):
        self.active_connections.remove(websocket)

    async def send_personal_message(self, message: str, websocket: WebSocket):
        await websocket.send_json(message)

    async def broadcast(self, message: str):
        for connection in self.active_connections:
            await connection.send_json(message)

class Comment:
    def __init__(self, author, content): 
        self.author = author 
        self.content = content

app = FastAPI()
templates = Jinja2Templates(directory="templates")
manager = ConnectionManager()
comments = [] 
comments.append( Comment("author 1 ", "content 1") )
comments.append( Comment("author 2 ", "content 2") )
comments.append( Comment("author 3 ", "content 3") )

@app.get("/")
def main(request: Request):
    return templates.TemplateResponse("index.html", {"request": request, "comments": comments})

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await manager.connect(websocket)
    try:
        while True:
            data = await websocket.receive_json()
            comments.append(Comment(data['author'], data['content']))
            await manager.broadcast(data)
    except WebSocketDisconnect:
        manager.disconnect(websocket)
        
if __name__ == '__main__':
    uvicorn.run(app, host='127.0.0.1', port=8000)

templates/index.html

<!DOCTYPE html>
<html>
    <head>
        <title>Title</title>
    </head>
    <body>
        <h1>Add new comment</h1>
        <form action="" onsubmit="addComment(event)">
            <input type="text" id="author" autocomplete="off"/>
            <input type="text" id="content" autocomplete="off"/>
            <button>Add comment</button>
        </form>
        <h2>Comments</h2>
        <ul id='comments'>
            {% for comment in comments %}
                <li>
                  <h3> {{comment.author}} </h3>
                  <p> {{comment.content}} </p>
               </li>
            {% endfor %}
        </ul>
        <script>
            var ws = new WebSocket("ws://localhost:8000/ws");
            ws.onmessage = function(event) {
                var comments = document.getElementById('comments')
                var comment = document.createElement('li')
                var jsonObj  = JSON.parse(event.data);
                var authorNode = document.createElement('h3');
                authorNode.innerHTML = jsonObj.author;
                var contentNode = document.createElement('p');
                contentNode.innerHTML = jsonObj.content;
                comment.appendChild(authorNode);
                comment.appendChild(contentNode);
                comments.appendChild(comment)
            };
            function addComment(event) {
                var author = document.getElementById("author")
                var content = document.getElementById("content")
                ws.send(JSON.stringify({"author": author.value, "content": content.value}))
                author.value = ''
                content.value = ''
                event.preventDefault()
            }
        </script>
    </body>
</html>
Chris
  • 18,724
  • 6
  • 46
  • 80
  • Wow I never knew about websockets!!! This is so cool and it works! Any security concerns I should be aware about? Also, will the code work when I push to heroku @chris? – aj91 Feb 07 '22 at 16:12
  • Have a look at [this](https://devcenter.heroku.com/articles/websocket-security). – Chris Feb 07 '22 at 16:31
  • one more quick question - how do I control the styling of the 'document.createElement' bit? The comment appears unformatted in the corner of the page, but I want it to appear in a div I've created. Any advice? – aj91 Feb 08 '22 at 14:39