Pushing Data to Web Pages¶
The WebSocket API is an advanced technology that makes it possible to open a two-way interactive communication session between the user's browser and a server. With this API, you can send messages to a server and receive event-driven responses without having to poll the server for a reply.
JustPy uses this technology in order allow the server to "push" data to web pages. Here is an example of a program that implements a clock. Every second, the server pushes the updated time to all open web pages.
import justpy as jp import time import asyncio wp = jp.WebPage(delete_flag=False) clock_div = jp.Span(text='Loading...', classes='text-5xl m-1 p-1 bg-gray-300 font-mono', a=wp) async def clock_counter(): while True: clock_div.text = time.strftime("%a, %d %b %Y, %H:%M:%S", time.localtime()) jp.run_task(wp.update()) await asyncio.sleep(1) async def clock_init(): jp.run_task(clock_counter()) async def clock_test(): return wp jp.justpy(clock_test, startup=clock_init)
The program first creates a WebPage instance called
clock_div which is added to
wp. Then we define a function called
clock_counter. This function implements an infinite loop that changes the text of
clock_div to the current time, updates
wp and sleeps (in a non-blocking way) for one second. Every second, this loop is repeated.
wp is updated using the method
In order to understand what is happening, we need to make several distinctions.
wpis an instance of the Python class
wpcan be the
WebPageinstance returned by several different requests, and in fact in our case,
wpis always returned. All users get sent the same
wp. This makes sense since the time is the same for everybody locally and we want to update it for everyone.
- However, each user has their own browser tab open. Also, the same user may have several browser tabs and windows open.
- There is therefore a one to many relation between
wpand browser tabs and windows.
wpmay be rendered in many different tabs and windows (this is true for any instance of the class
- Each time an instance of
WebPageis rendered in a browser tab, JustPy creates a websocket connection (channel) between the tab and the sever. Therefore, an instance of
WebPage, such as
wp, may have many different websockets associated with it, one for every open tab that has
wprendered in it. JustPy keeps track of all open websockets
update()method when applied to the page, retrieves all open websockets associated with the specific WebPage instance and uses them to change the specific browser tab they connect with.
wp.update()updates all browser tabs that were rendered using
In JustPy, all the above is done by the framework without any need for developer intervention. What you need to remember is simply this: If requests from any number of users were returned the same instance of
WebPage, any update to this instance will change the content for all these users.
In our case, calling
wp.update() will update all users since all requests result in wp being rendered (the function clock_test always returns
It is important to remember that the update method is a coroutine and must run in the asyncio loop that the server runs on and therefore user functions that await this method must also be coroutines and defined using async. Once
justpy has been called, the loop the server is running on can be accessed via
JustPy.loop in case you need to do something more complex. Also
asyncio.get_event_loop() will work if you haven’t initiated other loops.
If the previous paragraph reads like gibberish to you, I apologize. Explaining Python's asyncio is beyond the scope of this tutorial. If you don't need to develop applications that scale, don't worry about it. Just use the examples as templates.
Let's review the program in detail. First, we create
clock_div as global variables.
Then we define the coroutine
clock_counter. It runs an infinite loop. The first line of the loop sets the text attribute of
clock_div to be the current time and date (formatted to look a little nicer). The second line uses the JustPy helper utility
run_task to run the coroutine update (it in fact schedules the coroutine to run as soon as possible). The third line causes
clock_counter to suspend running for 1 second in a non-blocking way (all other coroutines can run while
Next, we define the coroutine
clock_init (it could be just a regular function since it does not await any coroutine in this specific case). We will see in a second how this function will be used. All it does is run
clock_counter once the server loop is up and running.
The last coroutine we define is
clock_test. This is the function that handles all web requests. It just returns
wp (it doesn’t need to be a coroutine in this case, it would work as a regular function since it does not await any coroutines).
The last line is the call to
justpy. The keyword parameter
startup allows us to designate a function to run once the server loop has been initiated. We cannot designate
clock_counter as the startup function because the server is locked until the startup function terminates and
clock_counter never terminates.
When a JustPy event handler finishes running and returns
None, JustPy calls the
update method of the WebPage instance in which the event occurred. That is the reason that content in browser tabs gets updated after events automatically. If you don't want the page to update, return
True or anything except for
Run the following program:
import justpy as jp wp = jp.QuasarPage(delete_flag=False) editor = jp.QEditor(a=wp, kitchen_sink=True, height='94vh') def joint_edit(): return wp jp.justpy(joint_edit)
This program allows joint editing of a document. Open several browser tabs or different browsers on your local machine and start editing the document. Any change you make in one browser tab, is reflected immediately in all others.
Since the program renders
wp, the same WebPage instance to all pages, they all share the same QEditor instance. QEditor supports the input event and therefore the keys pressed are sent to the server which in turn sets the value of the QEditor instance (
editor in our case) and then updates
wp using the update method. Since
wp is rendered in all browser tabs, they are all updated by JustPy.
Simple Message Board¶
JustPy allows updating just specific elements on the page and not the whole page. Here is an implementation of a simple message board in which just the Div with the messages is shared and updated. Load pages in different browser tabs and see the functionality (a message sent from one tab will show up in all the others).
import justpy as jp from datetime import datetime input_classes = 'm-2 bg-gray-200 font-mono appearance-none border-2 border-gray-200 rounded py-2 px-4 text-gray-700 focus:outline-none focus:bg-white focus:border-orange-500' button_classes = 'm-2 p-2 text-orange-700 bg-white hover:bg-orange-200 hover:text-orange-500 border focus:border-orange-500 focus:outline-none' message_classes = 'ml-4 p-2 text-lg bg-orange-500 text-white overflow-auto font-mono rounded-lg' shared_div = jp.Div(classes='m-2 h-1/2 border overflow-auto', delete_flag=False) header = jp.Div(text='Simple Message Board', classes='bg-orange-100 border-l-4 border-orange-500 text-orange-700 p-4 text-3xl', delete_flag=False) button_icon = jp.Icon(icon='paper-plane', classes='text-2xl', delete_flag=False) button_text = jp.Div(text='Send', classes='text-sm', delete_flag=False) message_icon = jp.Icon(icon='comments', classes='text-2xl text-green-600', delete_flag=False) def message_initialize(): # Called once on startup d = jp.Div(a=shared_div, classes='flex m-2 border') time_stamp = jp.P(text=datetime.now().strftime('%Y-%m-%d %H:%M:%S'), classes='text-xs ml-2 flex-shrink-0') p = jp.Pre(text='Welcome to the simple message board!', classes=message_classes) d.add(message_icon, time_stamp, p) async def send_message(self, msg): if self.message.value: d = jp.Div(classes='flex m-2 border') time_stamp = jp.P(text=datetime.now().strftime('%Y-%m-%d %H:%M:%S'), classes='text-xs ml-2 flex-shrink-0') p = jp.Pre(text=self.message.value, classes=message_classes) d.add(message_icon, time_stamp, p) shared_div.add_component(d, 0) self.message.value = '' # Clear message box after message is sent await shared_div.update() def message_demo(): wp = jp.WebPage() outer_div = jp.Div(classes='flex flex-col h-screen', a=wp) outer_div.add(header) d = jp.Div(classes='flex', a=outer_div) message = jp.Textarea(placeholder='Enter message here', a=d, classes=input_classes, debounce=500) send_button = jp.Button(a=d, click=send_message, classes=button_classes) send_button.add(button_icon, button_text) outer_div.add(shared_div) shared_div.add_page(wp) send_button.message = message return wp jp.justpy(message_demo, startup=message_initialize)
At the top of the program we define combinations of Tailwind classes to make our UI a little nicer. This technique, of defining classes in a global manner can be used to create templates for JustPy applications. Next, we create global components that will be shared by all web pages. We introduce the
Icon component which is used to display icons from the free Fontawesome collection.
Take a look at
message_demo, the third function we define. All requests will be handled by this function. Each time it is called, it creates a new
wp. It adds a Div to it (
outer_div). To this Div we add the predefined
header Div and another Div which holds the message box and the button used to send messages. The button itself has two child components, the plane icon and the Div with the 'Send' text. We then add the shared message div to
The next line,
shared_div.add_page(wp), is important. We need to add
wp to the dictionary of pages
shared_div is on. When we call the
update method of
shared_div, it will use this dictionary to update the appropriate browser tabs.
JustPy does not keep track automatically of which page an element is on. This would have introduced a lot of overhead since often, as in our case also, an element is indirectly added to a page by being added to an element that has been or will be added to a page.
Let's take a closer look at
send_message, the event handler that gets called when
send_button is clicked. If the message box is not empty, the function creates a Div to which it adds an icon, a time stamp and the text of the message. It then adds the Div as the first element in
shared_div. It clears the message box and then awaits the
update method of
shared_div will be updated in all the WebPages it is on. All the other elements on the page will not be updated.
send_message is a coroutine defined using the
async keyword. This is the case because we need to await
update from within
The Final Countdown¶
It is often required to call update several times within one event handler. For example, you may want to display a loading method while information is being retrieved from a database. Here is an exaggerated example:
import justpy as jp import asyncio button_classes = 'm-2 p-2 text-orange-700 bg-white hover:bg-orange-200 hover:text-orange-500 border focus:border-orange-500 focus:outline-none' async def count_down(self, msg): self.show = False if hasattr(msg.page, 'd'): msg.page.remove(msg.page.d) bomb_icon = jp.Icon(icon='bomb', classes='inline-block text-5xl ml-1 mt-1', a=msg.page) d = jp.Div(classes='text-center m-4 p-4 text-6xl text-red-600 bg-blue-500 faster', a=msg.page, animation=self.count_animation) msg.page.d = d for i in range(self.start_num, 0 , -1): d.text = str(i) await msg.page.update() await asyncio.sleep(1) d.text = 'Boom!' d.animation = 'zoomIn' d.set_classes('text-red-500 bg-white') bomb_icon.set_class('text-red-700') self.show = True def count_test(request): start_num = int(request.query_params.get('num', 10)) animation = request.query_params.get('animation', 'flip') wp = jp.WebPage() count_button = jp.Button(text='Start Countdown', classes=button_classes, a=wp, click=count_down) count_button.start_num = start_num count_button.count_animation = animation return wp jp.justpy(count_test)
When you press the countdown button, a countdown begins and the page is updated every second.
Every JustPy component has a
show attribute. If it is set to
False, the component is not rendered. The first line in the
count_down event handler sets the button's
show attribute to
False and it is not rendered during the countdown (in this way the user cannot accidentally initiate another countdown while one is going on). Then we remove from the page the
Div with the current countdown (we check if the
Div is stored on the page to handle the first countdown ). We create the
Div and add it to the page and also store a reference to it under the
d attribute of the page so we can later have easy access to it for removal.
Then we loop from the countdown start number down to zero. In each loop iteration we set the text of
d, update the page, and sleep 1 second using the asyncio library so as not to block countdowns on other pages. You can verify this by loading the page in different tabs or browsers.
After the countdown loop has ended, we set the text of
d to 'Boom!', change some of
d's classes and make the button visible again. Remember, that if an event handler returns
None as in our case (the default in Python when no return statement is encountered), the framework updates the page. Therefore when count_down terminates the page is updated again and that is why we see the button and 'Boom!'.
JustPy supports animation using the animate.css library. Just set the animation attribute of any component to a valid animation name. The default animation we use above is 'flip'. Try
http://127.0.0.1:8000/?animation=bounceIn for example. You can test different animations using query parameters. You can set the animation speed by adding one of the classes slow, slower, fast, faster to the classes of the component. In this case we used 'faster' so that the animation takes less than 1 second.