Skip to content

puepy.Component

Components should not be created directly

In your populate() method, call t.tag_name() to create a component. There's no reason an application develop should directly instanciate a component instance and doing so is not supported.

Bases: Tag, Stateful

Components are a way of defining reusable and composable elements in PuePy. They are a subclass of Tag, but provide additional features such as state management and props. By defining your own components and registering them, you can create a library of reusable elements for your application.

Attributes:

Name Type Description
enclosing_tag str

The tag name that will enclose the component. To be defined as a class attribute on subclasses.

component_name str

The name of the component. If left blank, class name is used. To be defined as a class attribute on subclasses.

redraw_on_state_changes bool

Whether the component should redraw when its state changes. To be defined as a class attribute on subclasses.

redraw_on_app_state_changes bool

Whether the component should redraw when the application state changes. To be defined as a class attribute on subclasses.

props list

A list of props for the component. To be defined as a class attribute on subclasses.

Source code in puepy/core.py
class Component(Tag, Stateful):
    """
    Components are a way of defining reusable and composable elements in PuePy. They are a subclass of Tag, but provide
    additional features such as state management and props. By defining your own components and registering them, you
    can create a library of reusable elements for your application.

    Attributes:
        enclosing_tag (str): The tag name that will enclose the component. To be defined as a class attribute on subclasses.
        component_name (str): The name of the component. If left blank, class name is used. To be defined as a class attribute on subclasses.
        redraw_on_state_changes (bool): Whether the component should redraw when its state changes. To be defined as a class attribute on subclasses.
        redraw_on_app_state_changes (bool): Whether the component should redraw when the application state changes. To be defined as a class attribute on subclasses.
        props (list): A list of props for the component. To be defined as a class attribute on subclasses.
    """

    enclosing_tag = "div"
    component_name = None
    redraw_on_state_changes = True
    redraw_on_app_state_changes = True

    props = []

    def __init__(self, *args, **kwargs):
        super().__init__(*args, tag_name=self.enclosing_tag, **kwargs)
        self.state = ReactiveDict(self.initial())
        self.add_context("state", self.state)

        self.slots = {}

    def _handle_attrs(self, kwargs):
        self._handle_props(kwargs)

        super()._handle_attrs(kwargs)

    def _handle_props(self, kwargs):
        if not hasattr(self, "props_expanded"):
            self._expanded_props()

        self.props_values = {}
        for name, prop in self.props_expanded.items():
            value = kwargs.pop(prop.name, prop.default_value)
            setattr(self, name, value)
            self.props_values[name] = value

    @classmethod
    def _expanded_props(cls):
        # This would be ideal for metaprogramming, but we do it this way to be compatible with Micropython. :/
        props_expanded = {}
        for prop in cls.props:
            if isinstance(prop, Prop):
                props_expanded[prop.name] = prop
            elif isinstance(prop, dict):
                props_expanded[prop["name"]] = Prop(**prop)
            elif isinstance(prop, str):
                props_expanded[prop] = Prop(name=prop)
            else:
                raise PropsError(f"Unknown prop type {type(prop)}")
        cls.props_expanded = props_expanded

    def initial(self):
        """
        To be overridden in subclasses, the `initial()` method defines the initial state of the component.

        Returns:
            (dict): Initial component state
        """
        return {}

    def _on_state_change(self, context, key, value):
        super()._on_state_change(context, key, value)

        if context == "state":
            redraw_rule = self.redraw_on_state_changes
        elif context == "app":
            redraw_rule = self.redraw_on_app_state_changes
        else:
            return

        if redraw_rule is True:
            self.page.redraw_tag(self)
        elif redraw_rule is False:
            pass
        elif isinstance(redraw_rule, (list, set)):
            if key in redraw_rule:
                self.page.redraw_tag(self)
        else:
            raise Exception(f"Unknown value for redraw rule: {redraw_rule} (context: {context})")

    def insert_slot(self, name="default", **kwargs):
        """
        In defining your own component, when you want to create a slot in your `populate` method, you can use this method.

        Args:
            name (str): The name of the slot. If not passed, the default slot is inserted.
            **kwargs: Additional keyword arguments to be passed to Slot initialization.

        Returns:
            Slot: The inserted slot object.
        """
        if name in self.slots:
            self.slots[name].parent = Tag.stack[-1]  # The children will be cleared during redraw, so re-establish
        else:
            self.slots[name] = Slot(ref=f"slot={name}", slot_name=name, page=self.page, parent=Tag.stack[-1], **kwargs)
        slot = self.slots[name]
        if self.origin:
            slot.origin = self.origin
            if slot.ref:
                self.origin.refs[slot.ref] = slot
        return slot

    def slot(self, name="default"):
        """
        To be used in the `populate` method of code making use of this component, this method returns the slot object
        with the given name. It should be used inside of a context manager.

        Args:
            name (str): The name of the slot to clear and return.

        Returns:
            Slot: The cleared slot object.
        """
        #
        # We put this here, so it clears the children only when the slot-filler is doing its filling.
        # Otherwise, the previous children are kept. Lucky them.
        self.slots[name].children = []
        return self.slots[name]

    def __enter__(self):
        self.stack.append(self)
        self.origin_stack[0].append(self)
        self.component_stack.append(self)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.stack.pop()
        self.origin_stack[0].pop()
        self.component_stack.pop()
        return False

    def __str__(self):
        return f"{self.component_name or self.__class__.__name__} ({self.ref} {id(self)})"

    def __repr__(self):
        return f"<{self}>"

initial()

To be overridden in subclasses, the initial() method defines the initial state of the component.

Returns:

Type Description
dict

Initial component state

Source code in puepy/core.py
def initial(self):
    """
    To be overridden in subclasses, the `initial()` method defines the initial state of the component.

    Returns:
        (dict): Initial component state
    """
    return {}

insert_slot(name='default', **kwargs)

In defining your own component, when you want to create a slot in your populate method, you can use this method.

Parameters:

Name Type Description Default
name str

The name of the slot. If not passed, the default slot is inserted.

'default'
**kwargs

Additional keyword arguments to be passed to Slot initialization.

{}

Returns:

Name Type Description
Slot

The inserted slot object.

Source code in puepy/core.py
def insert_slot(self, name="default", **kwargs):
    """
    In defining your own component, when you want to create a slot in your `populate` method, you can use this method.

    Args:
        name (str): The name of the slot. If not passed, the default slot is inserted.
        **kwargs: Additional keyword arguments to be passed to Slot initialization.

    Returns:
        Slot: The inserted slot object.
    """
    if name in self.slots:
        self.slots[name].parent = Tag.stack[-1]  # The children will be cleared during redraw, so re-establish
    else:
        self.slots[name] = Slot(ref=f"slot={name}", slot_name=name, page=self.page, parent=Tag.stack[-1], **kwargs)
    slot = self.slots[name]
    if self.origin:
        slot.origin = self.origin
        if slot.ref:
            self.origin.refs[slot.ref] = slot
    return slot

slot(name='default')

To be used in the populate method of code making use of this component, this method returns the slot object with the given name. It should be used inside of a context manager.

Parameters:

Name Type Description Default
name str

The name of the slot to clear and return.

'default'

Returns:

Name Type Description
Slot

The cleared slot object.

Source code in puepy/core.py
def slot(self, name="default"):
    """
    To be used in the `populate` method of code making use of this component, this method returns the slot object
    with the given name. It should be used inside of a context manager.

    Args:
        name (str): The name of the slot to clear and return.

    Returns:
        Slot: The cleared slot object.
    """
    #
    # We put this here, so it clears the children only when the slot-filler is doing its filling.
    # Otherwise, the previous children are kept. Lucky them.
    self.slots[name].children = []
    return self.slots[name]