event.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. """Define event classes to connect the frontend and backend."""
  2. from __future__ import annotations
  3. import inspect
  4. from typing import Any, Callable, Dict, List, Set, Tuple
  5. from pynecone import utils
  6. from pynecone.base import Base
  7. from pynecone.var import BaseVar, Var
  8. class Event(Base):
  9. """An event that describes any state change in the app."""
  10. # The token to specify the client that the event is for.
  11. token: str
  12. # The event name.
  13. name: str
  14. # The routing data where event occurred
  15. router_data: Dict[str, Any] = {}
  16. # The event payload.
  17. payload: Dict[str, Any] = {}
  18. class EventHandler(Base):
  19. """An event handler responds to an event to update the state."""
  20. # The function to call in response to the event.
  21. fn: Callable
  22. class Config:
  23. """The Pydantic config."""
  24. # Needed to allow serialization of Callable.
  25. frozen = True
  26. def __call__(self, *args: Var) -> EventSpec:
  27. """Pass arguments to the handler to get an event spec.
  28. This method configures event handlers that take in arguments.
  29. Args:
  30. *args: The arguments to pass to the handler.
  31. Returns:
  32. The event spec, containing both the function and args.
  33. Raises:
  34. TypeError: If the arguments are invalid.
  35. """
  36. # Get the function args.
  37. fn_args = inspect.getfullargspec(self.fn).args[1:]
  38. # Construct the payload.
  39. values = []
  40. for arg in args:
  41. # If it is a Var, add the full name.
  42. if isinstance(arg, Var):
  43. values.append(arg.full_name)
  44. continue
  45. if isinstance(arg, FileUpload):
  46. return EventSpec(handler=self, upload=True)
  47. # Otherwise, convert to JSON.
  48. try:
  49. values.append(utils.json_dumps(arg))
  50. except TypeError as e:
  51. raise TypeError(
  52. f"Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}."
  53. ) from e
  54. payload = tuple(zip(fn_args, values))
  55. # Return the event spec.
  56. return EventSpec(handler=self, args=payload)
  57. class EventSpec(Base):
  58. """An event specification.
  59. Whereas an Event object is passed during runtime, a spec is used
  60. during compile time to outline the structure of an event.
  61. """
  62. # The event handler.
  63. handler: EventHandler
  64. # The local arguments on the frontend.
  65. local_args: Tuple[str, ...] = ()
  66. # The arguments to pass to the function.
  67. args: Tuple[Any, ...] = ()
  68. # Whether to upload files.
  69. upload: bool = False
  70. class Config:
  71. """The Pydantic config."""
  72. # Required to allow tuple fields.
  73. frozen = True
  74. class EventChain(Base):
  75. """Container for a chain of events that will be executed in order."""
  76. events: List[EventSpec]
  77. class Target(Base):
  78. """A Javascript event target."""
  79. checked: bool = False
  80. value: Any = None
  81. class FrontendEvent(Base):
  82. """A Javascript event."""
  83. target: Target = Target()
  84. key: str = ""
  85. # The default event argument.
  86. EVENT_ARG = BaseVar(name="_e", type_=FrontendEvent, is_local=True)
  87. class FileUpload(Base):
  88. """Class to represent a file upload."""
  89. pass
  90. # Special server-side events.
  91. def redirect(path: str) -> EventSpec:
  92. """Redirect to a new path.
  93. Args:
  94. path: The path to redirect to.
  95. Returns:
  96. An event to redirect to the path.
  97. """
  98. def fn():
  99. return None
  100. fn.__qualname__ = "_redirect"
  101. return EventSpec(
  102. handler=EventHandler(fn=fn),
  103. args=(("path", path),),
  104. )
  105. def console_log(message: str) -> EventSpec:
  106. """Do a console.log on the browser.
  107. Args:
  108. message: The message to log.
  109. Returns:
  110. An event to log the message.
  111. """
  112. def fn():
  113. return None
  114. fn.__qualname__ = "_console"
  115. return EventSpec(
  116. handler=EventHandler(fn=fn),
  117. args=(("message", message),),
  118. )
  119. def window_alert(message: str) -> EventSpec:
  120. """Create a window alert on the browser.
  121. Args:
  122. message: The message to alert.
  123. Returns:
  124. An event to alert the message.
  125. """
  126. def fn():
  127. return None
  128. fn.__qualname__ = "_alert"
  129. return EventSpec(
  130. handler=EventHandler(fn=fn),
  131. args=(("message", message),),
  132. )
  133. # A set of common event triggers.
  134. EVENT_TRIGGERS: Set[str] = {
  135. "on_focus",
  136. "on_blur",
  137. "on_click",
  138. "on_context_menu",
  139. "on_double_click",
  140. "on_mouse_down",
  141. "on_mouse_enter",
  142. "on_mouse_leave",
  143. "on_mouse_move",
  144. "on_mouse_out",
  145. "on_mouse_over",
  146. "on_mouse_up",
  147. "on_scroll",
  148. }