foreach.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. """Create a list of components from an iterable."""
  2. from __future__ import annotations
  3. import inspect
  4. from hashlib import md5
  5. from typing import Any, Callable, Iterable, Optional
  6. from reflex.components.component import Component
  7. from reflex.components.layout.fragment import Fragment
  8. from reflex.components.tags import IterTag
  9. from reflex.constants import MemoizationMode
  10. from reflex.vars import Var
  11. class Foreach(Component):
  12. """A component that takes in an iterable and a render function and renders a list of components."""
  13. _memoization_mode = MemoizationMode(recursive=False)
  14. # The iterable to create components from.
  15. iterable: Var[Iterable]
  16. # A function from the render args to the component.
  17. render_fn: Callable = Fragment.create
  18. # The theme if set.
  19. theme: Optional[Component] = None
  20. def _apply_theme(self, theme: Component):
  21. """Apply the theme to this component.
  22. Args:
  23. theme: The theme to apply.
  24. """
  25. self.theme = theme
  26. @classmethod
  27. def create(cls, iterable: Var[Iterable], render_fn: Callable, **props) -> Foreach:
  28. """Create a foreach component.
  29. Args:
  30. iterable: The iterable to create components from.
  31. render_fn: A function from the render args to the component.
  32. **props: The attributes to pass to each child component.
  33. Returns:
  34. The foreach component.
  35. Raises:
  36. TypeError: If the iterable is of type Any.
  37. """
  38. iterable = Var.create(iterable) # type: ignore
  39. if iterable._var_type == Any:
  40. raise TypeError(
  41. f"Could not foreach over var of type Any. (If you are trying to foreach over a state var, add a type annotation to the var.)"
  42. )
  43. component = cls(
  44. iterable=iterable,
  45. render_fn=render_fn,
  46. **props,
  47. )
  48. # Keep a ref to a rendered component to determine correct imports.
  49. component.children = [
  50. component._render(props=dict(index_var_name="i")).render_component()
  51. ]
  52. return component
  53. def _render(self, props: dict[str, Any] | None = None) -> IterTag:
  54. props = {} if props is None else props.copy()
  55. # Determine the arg var name based on the params accepted by render_fn.
  56. render_sig = inspect.signature(self.render_fn)
  57. params = list(render_sig.parameters.values())
  58. if len(params) >= 1:
  59. props.setdefault("arg_var_name", params[0].name)
  60. if len(params) >= 2:
  61. # Determine the index var name based on the params accepted by render_fn.
  62. props.setdefault("index_var_name", params[1].name)
  63. elif "index_var_name" not in props:
  64. # Otherwise, use a deterministic index, based on the rendered code.
  65. code_hash = md5(str(self.children[0].render()).encode("utf-8")).hexdigest()
  66. props.setdefault("index_var_name", f"index_{code_hash}")
  67. return IterTag(
  68. iterable=self.iterable,
  69. render_fn=self.render_fn,
  70. **props,
  71. )
  72. def render(self):
  73. """Render the component.
  74. Returns:
  75. The dictionary for template of component.
  76. """
  77. tag = self._render()
  78. component = tag.render_component()
  79. # Apply the theme to the children.
  80. if self.theme is not None:
  81. component.apply_theme(self.theme)
  82. return dict(
  83. tag.add_props(
  84. **self.event_triggers,
  85. key=self.key,
  86. sx=self.style,
  87. id=self.id,
  88. class_name=self.class_name,
  89. ).set(
  90. children=[component.render()],
  91. props=tag.format_props(),
  92. ),
  93. iterable_state=tag.iterable._var_full_name,
  94. arg_name=tag.arg_var_name,
  95. arg_index=tag.get_index_var_arg(),
  96. iterable_type=tag.iterable._var_type.mro()[0].__name__,
  97. )