So for example I have a for loop inside my pyodide script that is inside my .html document. Is there a ways to change textContent of a div directly from pyodide for loop.
In the example below, only the last value of the for loop (in my case 99) is then sent to "myDiv". Is it even possible to change the textContent directly from pyodide script?
<head>
<script src="https://cdn.jsdelivr.net/pyodide/v0.22.1/full/pyodide.js"></script>
</head>
<body>
<div id="myDiv">Text that needs to change</div>
<script>
async function main() {
let pyodide = await loadPyodide();
return pyodide;
}
let pyodideReadyPromise = main();
async function pythonChange() {
let pyodide = await pyodideReadyPromise;
pyodide.runPython(`
from js import document
print("started")
for i in range(100):
print(i)
document.getElementById("myDiv").textContent = i
print("finished")
`)
}
pythonChange();
</script>
</body>
The issue isn't that the textContent isn't being changed; the issue is that there's no opportunity for the screen to update to actually display the change. The solution is to use a coroutine.
To observe that the textContent is indeed changing, we can add a small Mutation Observer to the very beginning of your script tag. This will log to the browser dev console any changes to the observed DOM node:
function callback(mutationList, observer){
mutationList.forEach(mutation => console.log(record.target))
}
const MO = new MutationObserver(callback)
MO.observe(document.getElementById("myDiv"), {attributes: true })
With this added code, the console fills with started
, 0
, 1
, ..., 98
, 99
, finished
. So the textContent is, in fact, changing, which is good.
You might think that the code is simply proceeding too fast for you eyes to see the numbers change, but that's not what's happening either. Let's slow things down by modifying your for loop:
for i in range(100):
print(i)
document.getElementById("myDiv").textContent = i
for j in range(1_000_000):
_ = 1
Now your loop has to "do a little useless work" before it advances to the next number. (You may need to change 1_000_000
to a larger or smaller number, depending on your system.) If you open the dev console, you'll see the numbers 0 to 99 appearing at a more measured pace. But the text on the page doesn't update until the Python code has finished. So what gives?
The issue is that while updates to the DOM are synchronous (i.e. no further code will be executed until the DOM update is complete), updates to the screen are asynchronous. What's more, the entire call to runPython()
is synchronous, so no updates to the screen will occur until the runPython
terminates. Essentially, the call to runPython
is a blocking call, and nothing else can happen on the page - screen updates and repainting, other JavaScript calls, etc - until runPython
returns.
This blog post gives a good high-level explanation of the interaction between synchronous code and visible changes on screen.
So, if the screen can't update until our synchronous code call terminates, what can we do? Make our code asynchronous! By turning our code into a coroutine which occasionally yields back to the browser's event loop to do some work (i.e. update the screen), we can see the updates visibly as they happen.
Pyodide has a nifty utility for this in the form of the runPythonAsync function, which allows you to write async code without resorting to wrapping your code into a coroutine. Here's a description of this feature and its purpose from when it was used in PyScript.
Here's a final code sample, which would replace the entire call to pyodide.runPython
in your original example. I've left the slowdown code in place so that the results are visible, but there's no need for it to be there in production.
pyodide.runPythonAsync(`
from js import document
from asyncio import sleep
print("started")
for i in range(100):
print(i)
document.getElementById("myDiv").textContent = i
await sleep(0.01)
for j in range(1_000_000):
_ = 1
print("finished")
`)