reflex icon indicating copy to clipboard operation
reflex copied to clipboard

Call event handlers from other event handlers

Open picklelo opened this issue 2 years ago • 3 comments

Currently the only way to call another event handler is to return it and create a chain. However this leads to poor code reusability in many cases.

Example in #596

class State(pc.State):
    
    count1: int = 0
    count2: int = 1
    sum_count: int = 0

    def increment_count1(self):
        time.sleep(1)
        self.count1 += 1

    def add_counts(self):
        self.sum_count = self.count1 + self.count2

    def increment_add(self):
        self.increment_count1()
        self.add_counts()

Currently the given increment_add function won't work in Pynecone. We also can't use an event chain like on_click=[State.increment_count1, State.add_counts] because event two relies on event one and it executes asynchronously. This only leaves us with one option

This should be very possible to implement.

picklelo avatar Feb 25 '23 18:02 picklelo

@picklelo any ideas on where to get started on this?

lucashofer avatar Feb 27 '23 14:02 lucashofer

@lucashofer So currently when you do something like self.event_handler within the code, it returns an EventHandler object rather than the function itself. To call the function, you need to do self.event_handler.fn(self).

I made a little app below to demonstrate:

"""Welcome to Pynecone! This file create a counter app."""
import pynecone as pc
import random


class State(pc.State):
    """The app state."""

    count = 0

    def print_state(self):
        """Print the state."""
        print(self.count)

    def increment(self):
        """Increment the count."""
        self.print_state.fn(self)
        self.count += 1

    def decrement(self):
        """Decrement the count."""
        self.print_state.fn(self)
        self.count -= 1

    def random(self):
        """Randomize the count."""
        self.print_state.fn(self)
        self.count = random.randint(0, 100)


def index():
    """The main view."""
    return pc.center(
        pc.vstack(
            pc.heading(State.count),
            pc.hstack(
                pc.button("Decrement", on_click=State.decrement, color_scheme="red"),
                pc.button(
                    "Randomize",
                    on_click=State.random,
                    background_image="linear-gradient(90deg, rgba(255,0,0,1) 0%, rgba(0,176,34,1) 100%)",
                    color="white",
                ),
                pc.button("Increment", on_click=State.increment, color_scheme="green"),
            ),
            padding="1em",
            bg="#ededed",
            border_radius="1em",
            box_shadow="lg",
        ),
        padding_y="5em",
        font_size="2em",
        text_align="center",
    )


# Add state and page to the app.
app = pc.App(state=State)
app.add_page(index, title="Counter")
app.compile()

What do you think's the best way do handle this? It is technically possible already - so we can either document it or somehow add some magic to clean up the API.

The only thing is - we still need to support returning event handlers to support chaining, etc. So I don't think we can easily just convert them all to the underlying functions.

picklelo avatar Mar 01 '23 22:03 picklelo

Sorry for the late reply on this⁠—currently finishing up my diss.

I think some magic should be added in the API. It took me quite a bit of searching through the code to realize state functions are converted to EventHandlers behind the scenes.

So to state the problem overall, just to make sure my understanding is correct:

  1. From a pure Python perspective, class functions should be callable by other class functions.
  2. Behind the scenes, State functions need to processed as event handlers when called either from a pc component or as part of an even chain.

To address 1) I think the state functions should be left alone. This will allow class functions to call one another directly as would be assumed.

Then behind the scenes we can handle turning those state functions into Event Handlers. Specifically, within the state the current __init_subclass__

events = {
            name: fn
            for name, fn in cls.__dict__.items()
            if not name.startswith("_") and isinstance(fn, Callable)
        }
        for name, fn in events.items():
            event_handler = EventHandler(fn=fn)
            setattr(cls, name, event_handler)

could have the final line changed to

cls.event_handlers[name] = event_handler

However, we still want to keep the API the same which means whenever an event is referenced we need the form on_click=State.event. Since in the structure I propose above, the State.event function is just a function, we need to use __qualname__ to get the name and state from which we can extract the correct event handler from the state's event handlers dict. This could be done in the state.process function and similar code already exists in middleware.HydrateMiddleware in conjunction with utils.format_event_handler.

Anyway, this is a rough idea and could use refinement, but I think long term will be more intuitive for users by abstracting away the machinery behind event handlers.

lucashofer avatar Mar 08 '23 13:03 lucashofer