Components

Caspian components are just Python functions. They encapsulate logic, styles, and structure. Start simple with basic strings, or scale up to fully type-safe, reactive UI elements.

1. The Basics

A standard component handles prop merging and returns an HTML string. Use merge_classes to ensure Tailwind classes don't conflict.

Developer Speed Tip

Don't write boilerplate. If you have the VS Code Extension installed, just type:

caspcom + Tab

It automatically generates the standard structure seen below.

src/components/Container.py
from casp.html_attrs import get_attributes, merge_classes
from casp.component_decorator import component

@component
def Container(**props):
    # 1. Extract 'class' to merge it safely
    incoming_class = props.pop("class", "")
    final_class = merge_classes("mx-auto max-w-7xl px-4", incoming_class)

    # 2. Extract children (inner HTML)
    children = props.pop("children", "")

    # 3. Generate remaining attributes (id, data-*, etc.)
    attributes = get_attributes({
        "class": final_class
    }, props)

    return f'<div {attributes}>{children}</div>'

2. Type-Safe Props

Want TypeScript-like validation? Use Python's Literal type. Your editor will autocomplete variants, and linter will catch typos.

src/components/ui/Button.py
from typing import Literal, Any
from casp.component_decorator import component

# Define strict types for autocomplete
ButtonVariant = Literal["default", "destructive", "outline"]
ButtonSize = Literal["default", "sm", "lg"]

@component
def Button(
    children: Any = "",
    # Editor will now suggest: "default" | "destructive" | "outline"
    variant: ButtonVariant = "default", 
    size: ButtonSize = "default",
    **props,
) -> str:
    # ... implementation (merging classes based on variant/size) ...
    return f"<button {attrs}>{children}</button>"

3. HTML Templates & Reactivity

For complex layouts or reactive state, separate your code into an HTML file. Use render_html to bridge them.

Counter.py
from casp.component_decorator import component, render_html

@component
def Counter(**props):
    return render_html("Counter.html", **props)
Counter.html
<div>
  <h3>[[label]]</h3> <!-- Prop -->
  <button onclick="inc()">
     {count} <!-- State -->
  </button>
</div>

Passing Context Dictionaries

When you need precise control over variable names, or when passing state handlers that don't map 1:1 to props, you can pass a dictionary explicitly as the second argument to render_html.

src/components/ui/Dialog.py
from casp.component_decorator import component, render_html

@component
def Dialog(open: str = "", setOpen: str = ""):
    
    # You can now pass a dictionary context directly.
    # Useful for remapping props or injecting derived state.
    return render_html(__file__, {
        "open": open,
        "setOpen": setOpen,
        "dialogId": "main-dialog"
    })

4. Server-Side Logic (RPC)

Components can also host server-side logic using the @rpc decorator. This allows you to encapsulate business logic directly within the component file.

Important Note

RPCs in components are Global. Unlike Pages (which are scoped to the URL), component RPCs are registered by their function name. Ensure unique names to avoid conflicts.

src/components/ForgotPassword.py
from casp.rpc import rpc
from casp.component_decorator import component, render_html

@rpc()
def send_forgot_password_email(email: str) -> dict:
    # Server-side logic (e.g., SMTP, Database)
    print(f"Sending password reset to {email}")
    return {"status": "success", "message": "Email sent."}

@component
def ForgotPassword(
    openDialog: str = "", 
    setOpenDialog: str = ""
) -> str:
    return render_html(__file__, 
        openDialog=openDialog, 
        setOpenDialog=setOpenDialog
    )

5. Component Composition

Much like React, PulsePoint allows you to easily compose custom components together, passing down props and state handlers.

Crucial Rule: Every component HTML template must return a single parent element. The props intended for reuse within the component should be mapped directly to this root element using the [[propName]] syntax.

users/index.html (Parent View)
<!-- Import and use your custom component, passing state as props -->
<CreateUpdateDialog 
  openDialog="{openCreateUpdateDialog}" 
  setOpenDialog="{setOpenCreateUpdateDialog}" 
  selectedUser="{selectedUser}" 
  users="{users}" 
  setUsers="{setUsers}" 
/>
components/CreateUpdateDialog.py
from casp.component_decorator import component, render_html

# Define the component and accept the props passed from the parent
@component
def CreateUpdateDialog(
    openDialog: str, 
    setOpenDialog: str, 
    selectedUser: str | None = None, 
    users: str | None = None, 
    setUsers: str | None = None
):
    return render_html(__file__, {
        "openDialog": openDialog,
        "setOpenDialog": setOpenDialog,
        "selectedUser": selectedUser,
        "users": users,
        "setUsers": setUsers
    })
components/CreateUpdateDialog.html
<!-- MUST have a single root element. Map props to it using [[propName]] -->
<div 
  openDialog="[[openDialog]]" 
  setOpenDialog="[[setOpenDialog]]" 
  selectedUser="[[selectedUser]]" 
  users="[[users]]" 
  setUsers="[[setUsers]]"
>
  
  <!-- Now you can use the props inside your component's template / script -->
  <Dialog open="{openDialog}" onOpenChange="{setOpenDialog}">
    <!-- ... Dialog content ... -->
  </Dialog>

  <script>
    // Example: Reacting to selectedUser changing from parent
    pp.effect(() => {
      if (selectedUser) {
        setUserName(selectedUser.name);
      }
    }, [selectedUser]);
  </script>
</div>