pys-onClick function firing without click

This seems to be a not-uncommon question in JS circles, but I can’t seem to formulate a solution within PyScript. Below is a rudimentary setup for what will become a “rock paper scissors” game. The idea is to have the user click a button, “rock”, “paper”, or “scissors”, all three of which will fire a Python function that contains the game logic. For now, the function merely displays the user’s move and the computer’s move (determined at random). Each button passes an argument to the function–‘rock’, ‘paper’, or ‘scissors’-- and for now, the function should simply write back into the HTML the user and computer choices. The problem is, when written this way, all three buttons are firing the function on page load. While only the last call displays to the page, the console shows that all three are firing in order, each one overwriting the last. I need only one of the buttons to fire onClick. I haven’t had any problems triggering functions that take no arguments onclick and only onclick. It’s when I try to pass arguments that I get a problem. The fundamental question is how do I pass an argument to a pys-onClick (or py-onClick, depending on the version of PyScript) function without the function firing on page load? What am I missing?

Incidentally, an equivalent function written in JS performs as expected. See last code block below. The difference is, while the JS function recognizes “this.value” passed in from the buttons, Python does not.

<!-- HTML -->
...
<form onsubmit="return false">
    <button class="button" id="rock" type="submit" pys-onClick="play('rock')">Rock</button>
    <button class="button" id="paper" type="submit" pys-onClick="play('paper')">Paper</button>   
    <button class="button" id="scissors" type="submit" pys-onClick="play('scissors')">Scissors</button>
</form>
...
<!-- Placeholders for function output -->
<div id="user_move"></div>
<div id="computer_move"></div>
...
<py-script>
import random

MOVES = ["rock", "paper", "scissors"]
...
def play(move):
    computer_move = random.choice(MOVES)
    Element("user_move").write(move)
    Element("computer_move").write(computer_move)

</py-script>

If it helps, here’s the entire script. Open the console and you’ll see that all three buttons are firing the function.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
        <!-- Latest compiled and minified CSS -->
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
        <!-- Latest compiled JavaScript -->
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
        <!-- PyScript import -->
        <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
    </head>

    <body>
        <div class="container-fluid">
            <div class="row">
                <div class="col-sm-2"></div>
                <div class="col-sm-10">
                <form onsubmit="return false">  
                    <button class="submit" id="rock" type="submit" pys-onClick="play('rock')">Rock</button>
                    <button class="submit" id="paper" type="submit" pys-onClick="play('paper')">Paper</button>
                    <button class="submit" id="scissors" type="submit" pys-onClick="play('scissors')">Scissors</button>
                </form>   
                </div>
            </div>
            <div class="row">
                <div id="user_move"></div>
                <div id="computer_move"></div>
            </div>
        
        </div>
        
<py-script>
import time
import random

MOVES = ["rock", "paper", "scissors"]
WIN_MSG = ["You win!", "You dropped the bomb on me! You win!", "Grrrr... You win. Jerk."]
LOSE_MSG = ["Ha ha! You lose!", "Don't bring that weak game in here! You lose!", "You might be good. But I'm better! Non-winner"]
TIE_MSG = ["Great minds think alike. We tied.", "I see the way you think. We tied.", "No joy! Tie."]

def play(move):
    computer_move = random.choice(MOVES)
    Element("user_move").write(move)
    Element("computer_move").write(computer_move)

</py-script>

    </body>
</html>

Same idea, written with JS:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
        <!-- Latest compiled and minified CSS -->
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
        <!-- Latest compiled JavaScript -->
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
     
    </head>

    <body>
        <div class="container-fluid">
            <div class="row">
                <div class="col-sm-2"></div>
                <div class="col-sm-10">
                <form onsubmit="return false">  
                    <button class="submit" id="rock" value="rock" type="submit" onClick="play(this.value)">Rock</button>
                    <button class="submit" id="paper" value="paper" type="submit" onClick="play(this.value)">Paper</button>
                    <button class="submit" id="scissors" value="scissors" type="submit" onClick="play(this.value)">Scissors</button>
                </form>   
                </div>
            </div>
            <div class="row">
                <div id="user_move"></div>
                <div id="computer_move"></div>
            </div>
        </div>
        
        <script>

        function play(move) {
            document.getElementById('user_move').innerHTML = move
        }

        </script>

    </body>
</html>
1 Like

This issue lies in a difference between how the onClick and pys-onClick attributes are resolved.

When PyScript loads, it looks for all the tags withpys-* attributes on the page… When it finds one, it runs the following code:

Where event is the pys event translated to the matching JS event (“click”), and handlercode is the verbatim contents of the pys-onClick attribute.

In your case:

<button class="submit" id="rock" type="submit" pys-onClick="play('rock')">Rock</button>

Causes PyScript to run:

Element("rock").element.addEventListener("click",  create_proxy(play('rock'))

Which executes the function play, whereas you want to pass a reference to play with the argument rock. Hence why you’re seeing all three functions execute at load time, since this code will run each time the play(*) is called.

You can either continue to use the javascript version, or use functools.partial:
pys-onClick="partial(play,'rock')

Note that the event handling codes pass a pointer to the object that triggers the event itself (causing TypeError: play() takes 1 positional argument but 2 were given). You may want to make the signature of the function def play(move, *args, **kwargs): or similar.

2 Likes

Thanks so much for the explanation. I think I understand (and the suggestions definitely work).

2 Likes

After reflecting on this some more, I think the pys-on* events should probably mirror the behavior of the Javascript on- events. That would certainly be more intuitive.

I’ve opened an issue and pull request about this (and credited you Mark!)

1 Like

I saw that. You’re very kind. I lack the background and knowledge to understand the intricacies of PyScript’s development process, but I will be following this with great interest. Thanks for making the PR.

2 Likes

Just ran my original script on the unstable release. Works just as expected. Very nice work, Jeff. Thanks!