Search code examples
pyscript

Initializing and using a Javascript class from PyScript


I would like to initialize a Javascript class and then call its methods all from PyScript.

My current approach is to use a singleton Javascript factory (MyClassFactory) to create and track instances of MyClass using a user provided id. This way the user can use the id to reference the desired input of MyClass without having a reference to the actual class instance. However, this alone is insufficient since the factory is itself a class. So, I wrap the factory with the useMyClass function that takes an id and the method of MyClass to call, which can then be invoked from PyScript.

It seems to work, but feels a bit Rube Goldbergish.

Is there a better way to do this?

My current approach.

<head>
  <meta charset="UTF-8">
  <script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>
<body>
<script>
  class MyClass {
    constructor() {
      this.hi_count = 0;
    }
    sayHi() {
      if (this.hi_count === 0) {
        document.write('Hi<br>')
      } else {
        document.write('Hi Again<br>')
      }
      this.hi_count++
    }
  }
  class MyClassFactory {
    constructor() {
      if (MyClassFactory.instance) {
        return MyClassFactory.instance;
      }
      this.myClasses = new Map();
      MyClassFactory.instance = this;
    }

    createMyClass(id) {
      if(this.myClasses.has(id)) {
        return this.myClasses.get(id);
      } else {
        const myClass = new MyClass();
        this.myClasses.set(id, myClass);
        return myClass;
      }
    }
  }
  function useMyClass(id, method) {
    const myClassFactory = new MyClassFactory();
    const myClass = myClassFactory.createMyClass(id);
    myClass[method]();
  }

</script>
<py-script>
  import js
  js.useMyClass(1,"sayHi")  # Creates new instance and calls sayHi method
  js.useMyClass(2,"sayHi")  # Creates new instance and calls sayHi method
  js.useMyClass(1,"sayHi"). # Uses existing instance with id 1 and calls sayHi method
</py-script>
</body>
</html>

Solution

  • There are a couple of things you can do to simplify your life here:

    • You can use the .new() method on JavaScript proxy objects to construct them within Python. This is the workaround for Python not having JavaScript's new keyword for constructors. See the Pyodide Proxy docs for more situations like this, where a keyword gets proxied by another keyword/syntax.
    • import js imports from the current global scope. You can use a class delcaration to create a reference to your class in the JS global scope, which you can them import into PyScript/Pyodide.

    With both of those things combined, here's an alternative version of your code:

    <head>
        <meta charset="UTF-8">
        <!-- I've pinned the release so this continues to be a valid example into the future-->
        <script defer src="https://pyscript.net/releases/2023.03.1/pyscript.js"></script>
    </head>
    <body>
        <script>
        var MyClass = class MyClass {
            constructor() {
                this.hi_count = 0;
            }
            sayHi() {
                if (this.hi_count === 0) {
                    document.write('Hi<br>')
                } else {
                    document.write('Hi Again<br>')
                }
                this.hi_count++
            }
        }
    
        </script>
        <py-script>
            from js import MyClass
    
            one = MyClass.new()
            two = MyClass.new()
    
            one.sayHi()
            two.sayHi()
            one.sayHi()
        </py-script>
        </body>
    </html>
    

    Which outputs:

    Hi
    Hi
    Hi Again