How to get the updated list of items in Jinja2 template using FastAPI?

Question:

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!!

Asked By: aj91

||

Answers:

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>
Answered By: Chris
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.