Search code examples
pythonwebsocketjinja2fastapi

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


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


Solution

  • This sounds like a use case for WebSockets. The example below is based on the one given in the documentation link above, and can handle multiple connections, broadcasting any 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 connection/client will also receive the new comment you just added. If you didn't want to broadcast the message, you could instead use await manager.send_personal_message(data, websocket).

    Working Example

    app.py

    from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
    from websockets.exceptions import ConnectionClosed
    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, ConnectionClosed):
            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>