Skip to content

Components

stx components are reusable UI elements that encapsulate template, logic, and styling. This guide covers everything you need to know about creating and using components.

Component Basics

Component Structure

A basic component consists of:

  1. TypeScript interfaces (optional)
  2. Component definition
  3. Template
  4. Styles (optional)
html
@ts
interface ButtonProps {
  type?: 'primary' | 'secondary'
  size?: 'sm' | 'md' | 'lg'
  disabled?: boolean
}
@endts

@component('Button', {
  props: {
    type: 'primary',
    size: 'md',
    disabled: false
  }
})
  <button
    class="btn btn-{{ type }} btn-{{ size }}"
    :disabled="disabled"
  >
    <slot></slot>
  </button>

  <style>
    .btn {
      @apply rounded font-medium;
    }
    .btn-primary {
      @apply bg-blue-500 text-white;
    }
    .btn-secondary {
      @apply bg-gray-500 text-white;
    }
  </style>
@endcomponent

Using Components

Import and use components in your templates:

html
@import { Button } from '../components/Button'

<div class="actions">
  <Button
    type="primary"
    size="lg"
    @click="handleClick"
  >
    Click Me
  </Button>
</div>

Component Features

Props

Define and validate component props:

html
@ts
interface CardProps {
  title: string
  subtitle?: string
  image?: string
  loading?: boolean
}
@endts

@component('Card', {
  props: {
    title: '',
    subtitle: undefined,
    image: undefined,
    loading: false
  },
  // Prop validation
  propValidators: {
    title: (value) => value.length > 0,
    image: (value) => value?.startsWith('http')
  }
})
  <div class="card">
    @if(loading)
      <loading-spinner />
    @else
      @if(image)
        <img :src="image" :alt="title" />
      @endif

      <div class="card-content">
        <h2>{{ title }}</h2>
        @if(subtitle)
          <p>{{ subtitle }}</p>
        @endif
        <slot></slot>
      </div>
    @endif
  </div>
@endcomponent

Slots

Use slots to inject content into components:

html
@component('Panel')
  <div class="panel">
    <!-- Default slot -->
    <slot></slot>

    <!-- Named slots -->
    <slot name="header"></slot>
    <slot name="footer"></slot>

    <!-- Slots with fallback content -->
    <slot name="empty">
      <p>No content available</p>
    </slot>
  </div>
@endcomponent

<!-- Usage -->
<Panel>
  @slot('header')
    <h2>Panel Title</h2>
  @endslot

  <p>Main content goes here</p>

  @slot('footer')
    <button>Close</button>
  @endslot
</Panel>

Events

Emit and handle component events:

html
@component('SearchInput', {
  props: {
    placeholder: 'Search...'
  }
})
  <div class="search">
    <input
      type="text"
      :placeholder="placeholder"
      @input="handleInput"
    />
    <button @click="handleClear">Clear</button>
  </div>

  @ts
  function handleInput(e: Event) {
    const value = (e.target as HTMLInputElement).value
    $emit('search', value)
  }

  function handleClear() {
    $emit('clear')
  }
  @endts
@endcomponent

<!-- Usage -->
<SearchInput
  @search="onSearch"
  @clear="onClear"
/>

Lifecycle Hooks

Use lifecycle hooks to run code at specific times:

html
@component('DataFetcher')
  @ts
  let data: any[] = []
  let loading = true
  let error: Error | null = null

  // Called when component is mounted
  onMounted(async () => {
    try {
      data = await fetchData()
    } catch (e) {
      error = e
    } finally {
      loading = false
    }
  })

  // Called before component is unmounted
  onUnmounted(() => {
    // Cleanup
  })

  // Called when props change
  onPropsChanged((newProps, oldProps) => {
    // Handle prop changes
  })
  @endts

  <div class="data-fetcher">
    @if(loading)
      <loading-spinner />
    @elseif(error)
      <error-message :error="error" />
    @else
      <data-display :items="data" />
    @endif
  </div>
@endcomponent

State Management

Manage component state:

html
@component('Counter')
  @ts
  let count = 0

  // Computed property
  const isEven = computed(() => count % 2 === 0)

  // Methods
  function increment() {
    count++
  }

  function decrement() {
    count--
  }

  // Watch for changes
  watch(count, (newValue, oldValue) => {
    console.log(`Count changed from ${oldValue} to ${newValue}`)
  })
  @endts

  <div class="counter">
    <button @click="decrement">-</button>
    <span>{{ count }}</span>
    <button @click="increment">+</button>
    <p>Count is {{ isEven ? 'even' : 'odd' }}</p>
  </div>
@endcomponent

Advanced Features

Async Components

Load components asynchronously:

html
@component('AsyncComponent')
  @async
    @loading
      <loading-spinner />
    @error
      <error-message :error="error" />
    @success
      <!-- Heavy component content -->
    @endasync
@endcomponent

Error Boundaries

Handle component errors:

html
@component('ErrorBoundary')
  @errorBoundary
    @error
      <div class="error">
        <h3>Something went wrong</h3>
        <p>{{ error.message }}</p>
        <button @click="retry">Retry</button>
      </div>
    @else
      <slot></slot>
    @enderror
  @enderrorBoundary
@endcomponent

Component Composition

Compose components together:

html
@component('UserProfile')
  @ts
  interface Props {
    userId: string
  }
  @endts

  <div class="profile">
    <ErrorBoundary>
      <AsyncComponent>
        <UserInfo :id="userId" />
        <UserStats :id="userId" />
        <UserPosts :id="userId" />
      </AsyncComponent>
    </ErrorBoundary>
  </div>
@endcomponent

Best Practices

  1. Component Design

    • Keep components focused and single-purpose
    • Use TypeScript interfaces for props
    • Document component APIs
    • Follow naming conventions
  2. Performance

    • Use async components for large components
    • Implement proper cleanup in onUnmounted
    • Optimize re-renders with memo
    • Use lazy loading when appropriate
  3. State Management

    • Keep state as local as possible
    • Use computed properties for derived state
    • Watch for side effects
    • Consider global state for app-wide data
  4. Testing

    • Write unit tests for components
    • Test edge cases and error states
    • Test component events
    • Test async behavior

Component Organization

Recommended file structure:

components/
├── common/
│   ├── Button.stx
│   ├── Card.stx
│   └── Input.stx
├── layout/
│   ├── Header.stx
│   └── Footer.stx
├── features/
│   ├── UserProfile.stx
│   └── SearchBar.stx
└── pages/
    ├── Home.stx
    └── About.stx

Next Steps

Released under the MIT License.