Skip to content

In-Depth Components

Defining components in PuePy is a powerful way to encapsulate data, display, and logic in a reusable way. Components become usable like tags in the populate() method of other components or pages, define slots, props, and events.

Data flow features

Slots

Slots are a mechanism to allow parent components to inject content (tags, other components, etc) into child components in specified locations. There can be one default or unamed slot, and any number of named slots.

  • Slots are defined in the populate() method of a component or page using self.insert_slot.
  • Slots are consumed in the code using the component with a context manager object and <component>.slot().

See the Components Tutorial Chapter for more information on slots.

Props

Props are a way to pass data to child components. Props must be defined by a component. When writing a component, you can simply include props as a list of strings, where each element is the name of a prop, or include instances of the Prop class. You can mix and match the two as well

class MyComponent(Component):
    props = [
        "title",  # (1)!
        Prop("author_name", "Name of Author", str, "Unknown") # (2)!
    ]
  1. This is a prop which only defines a name.
  2. To add extra metadata about a prop, you can also define a Prop instance.

Regardless of how you define props in your component, a full "expanded" list of props is available on self.props_expanded as a dictionary mapping prop name to Prop instance, with the Prop instance created automatically if only a name is specified.

Attributes

Keyword arguments passed to a component that do not match any known prop are considered attributes and stored in self.attrs. They are then inserted into the rendered DOM as HTML elements on the rendered attribute. This means that, for instance, you can pass arbitrary HTML attributes to newly created components without defining any custom props or other logic. Eg,

from puepy import Component, Page, t


class NameInput(Component):
    enclosing_tag = "input"

class MyPage(Page):
    def populate(self):
        t.name_input(id="name_input", placeholder="Enter your name")

Even if the NameInput component does not define a placeholder prop, the placeholder attribute will be rendered on the input tag.

When to use props vs attributes?

  • Use props when you want to pass data to a component that will be used in the component's logic or rendering.
  • Use attributes when you want to pass data to a component that will be used in the rendered HTML but not in the component's logic.

Events

Events are a way to allow child components to communicate with parent components. When writing a component, in your own code, you can emit an event by calling self.trigger_event. You can also optionally pass a detail dictionary to the event, which will be passed along (after Python to JavaScript conversion) to the browser's native event system in JavaScript.

For example, suppose you want to emit a custom event, greeting, with a type attribute:

class MyComponent(Component):
    def some_method(self):
        self.trigger_event("greeting", detail={"message": "Hello There"})

A consumer of your component can listen for this event by defining an on_greeting method in their component or page:

class MyPage(Page):
    def populate(self):
        t.my_component(on_greeting=self.on_greeting_sent)

    def on_greeting_sent(self, event):
        print("Incoming message from component", event.detail.get('message'))

Customization

You have several ways of controlling how your components are rendered. First, you can define what enclosing tag your component is rendered as. The default is a div tag, but this can be overridden:

class MyInputComponent(Component):
    enclosing_tag = "input"

You can also define default classes, default attributes, and the default role for your component:

class MyInputComponent(Component):
    enclosing_tag = "input"

    default_classes = ["my-input"]
    default_attributes = {"type": "text"}
    default_role = "textbox"

Parent/Child relationships

Each tag (and thus each component) in PuePy has a parent unless it is the root page. Consider the following example:

from puepy import Application, Page, Component, t

app = Application()


@t.component()
class CustomInput(Component):
    enclosing_tag = "input"

    def on_event_handle(self, event):
        print(self.parent)


class MyPage(Page):
    def populate(self):
        with t.div():
            t.custom_input()

In this example, the parent of the CustomInput instance is not the MyPage instance, it is the div, a puepy.Tag instance. In many cases, you will want to interact another relevant object, not necessarily the one immediately parental of your current instance. In those instances, from your components, you may reference:

  • self.page (Page instance): The page ultimately responsible for rendering this component
  • self.origin (Component or Page instance): The component that created yours in its populate() method
  • self.parent (Tag instance): The direct parent of the current instance

Additionally, parent instances have the following available:

  • self.children (list): Direct child nodes
  • self.refs (dict): Instances created during this instance's most recent populate() method

Warning

None of the attributes regarding parent/child/origin relationships should be modified by application code. Doing so could result in unexpected behavior.

Refs

In addition to parent/child relationships, most components and pages define an entire hierarchy of tags and components in the populate() method. If you want to reference components later, or tell PuePy which component is which (in case the ordering changes in sebsequent redraws), using a ref= argument when building tags:

class MyPage(Page):
    def populate(self):
        t.button("My Button", ref="my_button")

    def auto_click_button(self, ...):
        self.refs["my_button"].element.click()

For more information on why this is useful, see the Refs Tutorial Topic.