widgetti / ipyreact

React for ipywidgets that just works. No webpack, no npm, no hassle
BSD 3-Clause "New" or "Revised" License
104 stars 8 forks source link

Is "value" a special builtin prop? #4

Closed paddymul closed 4 months ago

paddymul commented 1 year ago

I tried to add an "other_value" prop to the example, expecting to get a trait for "other_value"

class OtherCountWidget(ipyreact.ReactWidget):
    _esm = """
    import confetti from "canvas-confetti";
    import * as React from "react";

    export default function({value, on_value, debug, other_count}) {
        return <div><button onClick={() => confetti() && on_value(value + 1)}>
            {value || 0} times confetti
        </button>
        <span>{other_count} other count</span>
        </div>
    };"""
ocw = OtherCountWidget()
ocw

ocw.traits()['other_count'] throws an error.

Is the idea that all component state should be passed from python in value?

Is the recommended practice to break out from value to individual props for a subcomponent... so that the default passed component just becomes a wrapper?

paddymul commented 1 year ago

Ahh, you still need to add it as a python trait to the class, the value trait is builtin.

This works for bi-directional binding

class OtherCountWidget(ipyreact.ReactWidget):
    other_count = Any(None, allow_none=True).tag(sync=True)
    _esm = """
    import confetti from "canvas-confetti";
    import * as React from "react";

    export default function({value, on_value, debug, other_count}) {
        return <div><button onClick={() => confetti() && on_value(value + 1)}>
            {value || 0} times confetti
        </button>
        <span>{other_count} other count</span>
        </div>
    };"""
ocw = OtherCountWidget()
ocw
ocw.other_count = 4
paddymul commented 1 year ago

This is obvious to me now, but wasn't when initially reading the documentation, I'll try to code up a simple example as a PR. The example will answer some of my other questions.

maartenbreddels commented 1 year ago

The example will answer some of my other questions.

Great :)

Yeah, value is added because it's used so much https://github.com/widgetti/ipyreact/blob/0a97edb5e4a1d7c93de8ba17eb5ec82b05a8a483/ipyreact/widget.py#L31

I think we should subclass from ValueWidget in ipywidgets, that makes it possible to use it with interact !

paddymul commented 1 year ago

I added a working example here https://github.com/widgetti/ipyreact/pull/7 . How should this be referenced in the docs?

maartenbreddels commented 1 year ago

Yeah, maybe in the https://github.com/widgetti/ipyreact#examples section we can refer to that directory?

kolibril13 commented 1 year ago

I am working on a ipyreact walkthrough notebook at https://github.com/widgetti/ipyreact/pull/11 and I want to add a section that explains how to increment a variable.

From this conversation and the README.md file I found that this example works:

import ipyreact

# πŸ’«πŸ’«πŸ’«  This counter works πŸ’«πŸ’«πŸ’«
class MyFirstWidget(ipyreact.ReactWidget):
    _esm = """
    import * as React from "react";

    export default function MyButton({value, on_value}) {
        return <button onClick={() => on_value(value + 1)}>
            {value || 0} times clicked
        </button>
    };"""
MyFirstWidget()

but then, when I add another traitlet other_count and replace all value variables with other_count variables, clicking the button does not increment the other_count value. Can some of you explain to me why this is the case? Is it because the on_value function is something related only to the value parameter? I'd be very interested in how this below example could be tweaked in a way so that it also allows incrementing the value of other_count:

import ipyreact
from traitlets import  Any

class MyFirstWidget(ipyreact.ReactWidget):
    other_count = Any(None, allow_none=True).tag(sync=True)
    _esm = """
    import * as React from "react";

    export default function MyButton({other_count, on_value}) {
        return <button onClick={() => on_value(other_count + 1)}>
            {other_count || 0} times clicked
        </button>
    };"""
MyFirstWidget()
maartenbreddels commented 1 year ago

Yes, see https://github.com/widgetti/ipyreact#facts, for every trait <name> there is a <name>, on_<name> pair on the react/frontend side.

https://github.com/widgetti/ipyreact/blob/a9e40b1100a6066265e310c83bfc667be4417b5d/ipyreact/widget.py#L31 value is a trait we already provide by default.

maartenbreddels commented 1 year ago

which means

return <button onClick={() => on_value(other_count + 1)}>

should be

return <button onClick={() => on_other_count(other_count + 1)}>
kolibril13 commented 1 year ago

Ohh nice! This works! πŸ•ΊπŸΌ

import ipyreact
from traitlets import Int

class MyFirstWidget(ipyreact.ReactWidget):
    my_count = Int(None, allow_none=True).tag(sync=True)
    _esm = """
    import * as React from "react";

    export default function MyButton({my_count, on_my_count}) {
        return <button onClick={() => on_my_count(my_count + 1)}>
            {my_count || 0} times clicked
        </button>
    };"""

MyFirstWidget()
kolibril13 commented 1 year ago

one more question. When I have this example here:

import ipyreact
from traitlets import Int

class MyFirstWidget(ipyreact.ReactWidget):
    my_count = Int(0).tag(sync=True)
    _esm = """
    import * as React from "react";

    export default function MyButton({my_count, on_my_count}) {
        return <button onClick={() => on_my_count(my_count + 1)}>
            {my_count} times clicked
        </button>
    };"""

m = MyFirstWidget()
m

Is there a way I can relocate the on_my_count logic to the python side?

This is how I would imagine something like that to look like:

import ipyreact
from traitlets import Int,observe

class MyCounterWidget(ipyreact.ReactWidget):
    my_count = Int(0).tag(sync=True)

    def my_python_function(self,count):
        self.my_count = count + 1

    _esm = """
    import * as React from "react";

    export default function MyButton({my_count}) {
        return <button onClick={() => my_python_function(my_count) }>
            {my_count} times clicked
        </button>
    };"""
m = MyCounterWidget()
m
maartenbreddels commented 1 year ago

See https://github.com/widgetti/ipyreact/pull/8 what do you think of this pr?

kolibril13 commented 1 year ago

amazing! That works out of the box! One question regarding best practice: I tested #8 and both examples below are working, one using

    def on_my_python_function(self):
        self.my_count = self.my_count + 1

and the other using

    def on_python_function(self,count):
        self.my_count = count + 1

Does one approach have an advantage about the other one? here is the full code:

import ipyreact
from traitlets import Int, Unicode

class Widget1(ipyreact.ReactWidget):
    my_count = Int(0).tag(sync=True)
    label = Unicode("Click me").tag(sync=True)

    def on_my_python_function(self):
        self.my_count = self.my_count + 1
        self.label = f"Clicked {self.my_count}"

    _esm = """
        import * as React from "react";

        export default function({on_my_python_function, label}) {
            return(
            <div>
                <button onClick={() => on_my_python_function()}>
                    {label}
                </button>
            </div>
            )
        };
    """

w1 = Widget1()
w1
import ipyreact
from traitlets import Int, Unicode

class Widget2(ipyreact.ReactWidget):
    my_count = Int(0).tag(sync=True)
    label = Unicode("Click me").tag(sync=True)

    def on_python_function(self,count):
        self.my_count = count + 1
        self.label = f"Clicked {self.my_count}"

    _esm = """
    import * as React from "react";

    export default function MyButton({on_python_function , label, my_count}) {
        return <button onClick={(event) => on_python_function(my_count) }>
            {label}
        </button>
    };"""
w2 = Widget2()
w2
maartenbreddels commented 1 year ago

They are slightly different. The first, will use the my_count Python property/trait, which uses the property itself. Imagine there are two events in flight, and they get executed after each other: `self.my_count=2.

In the second case, there are two message in flight, but with with the same payload (my_count=0). After executing the method twice, my_count=1.

maartenbreddels commented 4 months ago

We now have ValueWidget which has a special value trait, and a Widget which only has props, children and events: https://github.com/widgetti/ipyreact?tab=readme-ov-file#usage