Examples & Tutorials
On this page 16
This section provides practical examples and step-by-step tutorials to help you master stx.
Basic Examples
Counter Component
A simple counter component demonstrating state management and events:
interface Props {
initialCount?: number
}
@component(''
props: {
initialCount: {
type: Number,
default: 0
}
},
setup(props) {
const count = ref(props.initialCount)
const increment = () => count.value++
const decrement = () => count.value--
return { count, increment, decrement }
}
})
<divclass="">
<button@click="decrement"></button>
<span>{{ count }}</span>
<button@click="increment"></button>
</div>
<style scoped>
.counter {
flex items-center gap-4;
}
button {
px-4 py-2 bg-blue-500 text-white rounded;
}
span {
text-xl font-bold;
}
</style>
Todo List
A complete todo list application showing component composition and state management:
interface Todo {
id: number
text: string
completed: boolean
}
interface TodoListProps {
title?: string
}
@component(''
props: {
title: {
type: String,
default: 'Todo List'
}
},
setup() {
const todos = ref<Todo[]>([])
const newTodo = ref('')
const addTodo = () => {
if (!newTodo.value.trim()) return
todos.value.push({
id: Date.now(),
text: newTodo.value,
completed: false
})
newTodo.value = ''
}
const toggleTodo = (id: number) => {
const todo = todos.value.find(t => t.id === id)
if (todo) todo.completed = !todo.completed
}
const removeTodo = (id: number) => {
todos.value = todos.value.filter(t => t.id !== id)
}
return {
todos,
newTodo,
addTodo,
toggleTodo,
removeTodo
}
}
})
<divclass="">
<h2>{{ title }}</h2>
<divclass="">
<input
type="text"
v-model="newTodo"
.enter="addTodo"
placeholder="Add new todo"
>
<button@click="addTodo">Add</button>
</div>
<ul>
@foreach(todosastodo)
<lix-class="">
<input
type="checkbox"
:checked="todo.completed"
="toggleTodo(todo.id)"
>
<span>{{ todo.text }}</span>
<button@click="todo">Delete</button>
</li>
</ul>
</div>
<style scoped>
.todo-list {
max-w-md mx-auto p-4;
}
.add-todo {
flex gap-2 mb-4;
}
input[type="text"] {
flex-1 px-4 py-2 border rounded;
}
button {
px-4 py-2 bg-blue-500 text-white rounded;
}
ul {
space-y-2;
}
li {
flex items-center gap-2 p-2 border rounded;
}
.completed span {
line-through text-gray-500;
}
</style>
Tutorials
Building a Blog
1. Project Setup
First, create a new stx project:
# Create new project
bunx create-stx-app my-blog
cd my-blog
# Install dependencies
bun install
2. Create Post Component
Create a reusable post component (components/Post.stx):
interface Post {
id: number
title: string
content: string
author: string
date: string
}
interface Props {
post: Post
}
@component(''
props: {
post: {
type: Object as PropType<Post>,
required: true
}
}
})
<articleclass="">
<h2>{{ post.title }}</h2>
<divclass="">
<span>By {{ post.author }}</span>
<span>{{ post.date }}</span>
</div>
<divclass="">
{{ post.content }}
</div>
</article>
<style scoped>
.post {
max-w-2xl mx-auto p-6 bg-white rounded shadow;
}
h2 {
text-2xl font-bold mb-4;
}
.meta {
text-sm text-gray-500 mb-4;
}
.content {
prose;
}
</style>
3. Create Blog Layout
Create a layout component (layouts/Blog.stx):
@component('')
<divclass="">
<header>
<nav>
<ahref="">Home</a>
<ahref="">About</a>
<ahref="">Contact</a>
</nav>
</header>
<main>
<slot></slot>
</main>
<footer>
<p>© 2024 My Blog</p>
</footer>
</div>
<style>
.blog-layout {
min-h-screen flex flex-col;
}
header {
bg-white shadow;
}
nav {
max-w-7xl mx-auto px-4 py-6 flex gap-4;
}
main {
flex-1 max-w-7xl mx-auto px-4 py-8;
}
footer {
bg-gray-100 py-6 text-center;
}
</style>
4. Create Blog Home Page
Create the home page (pages/index.stx):
import type { Post } from '../types'
interface Props {
posts: Post[]
}
@component(''
props: {
posts: {
type: Array as PropType<Post[]>,
required: true
}
}
})
<BlogLayout>
<h1>Welcome to My Blog</h1>
<divclass="">
@foreach(postsaspost)
<Post:post="post" />
</div>
</BlogLayout>
<style scoped>
h1 {
text-4xl font-bold mb-8 text-center;
}
.posts {
space-y-8;
}
</style>
5. Add State Management
Create a store for blog posts (stores/posts.ts):
import { createStore } from '@stacksjs/store'
import type { Post } from '../types'
export const usePostStore = createStore({
state: {
posts: [] as Post[]
},
actions: {
async fetchPosts() {
// Simulate API call
const response = await fetch('/api/posts')
this.posts = await response.json()
},
async addPost(post: Omit<Post, 'id'>) {
// Simulate API call
const response = await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(post)
})
const newPost = await response.json()
this.posts.push(newPost)
}
}
})
Advanced Patterns
Form Handling
Create a reusable form component with validation:
interface FormField {
name: string
type: string
label: string
required?: boolean
pattern?: string
minLength?: number
maxLength?: number
}
interface Props {
fields: FormField[]
onSubmit: (data: Record<stringany>) => void
}
@component(''
props: {
fields: {
type: Array as PropType<FormField[]>,
required: true
},
onSubmit: {
type: Function as PropType<PropsonSubmit']>,
required: true
}
},
setup(props) {
const formData = ref<Recordstringany>>({})
const errors = ref<Recordstringstring>>({})
const validate = () => {
errors.value = {}
props.fields.forEach(field => {
const value = formData.value[field.name]
if (field.required && !value) {
errors.value[field.name] = `${field.label} is required`
}
if (field.pattern && value && !new RegExp(field.pattern).test(value)) {
errors.value[field.name] = `${field.label} is invalid`
}
if (field.minLength && value && value.length < field.minLength) {
errors.value[field.name] = `${field.label} must be at least ${field.minLength} characters`
}
if (field.maxLength && value && value.length > field.maxLength) {
errors.value[field.name] = `${field.label} must be at most ${field.maxLength} characters`
}
})
return Object.keys(errors.value).length === 0
}
const handleSubmit = (e: Event) => {
e.preventDefault()
if (validate()) {
props.onSubmit(formData.value)
}
}
return {
formData,
errors,
handleSubmit
}
}
})
<form@submit="handleSubmit"class="">
@foreach(fieldsasfield)
<divclass="">
<label:for="field">{{ field.label }}</label>
<input
:id="field.name"
x-type="field.type"
x-name="field.name"
v-model="formData[field.name]"
:required="field.required"
:pattern="field.pattern"
:minlength="field.minLength"
:maxlength="field.maxLength"
>
@if(errorsfieldname])
<spanclass="">{{ errors[field.name] }}</span>
</div>
<buttontype="">Submit</button>
</form>
<style scoped>
.form {
max-w-md mx-auto;
}
.form-field {
mb-4;
}
label {
block mb-2 font-medium;
}
input {
w-full px-4 py-2 border rounded;
}
.error {
text-sm text-red-500 mt-1;
}
button {
w-full px-4 py-2 bg-blue-500 text-white rounded;
}
</style>
Dynamic Components
Create a component that loads other components dynamically:
interface Props {
componentName: string
props?: Record<stringany>
}
@component(''
props: {
componentName: {
type: String,
required: true
},
props: {
type: Object,
default: () => ({})
}
},
async setup(props) {
const component = ref(null)
onMounted(async () => {
try {
component.value = await import(`../components/${props.componentName}.stx`)
} catch (error) {
console.error(`Failed to load component: ${props.componentName}`, error)
}
})
return { component }
}
})
@if(component)
<component:is="component"v-bind="props" />
<divclass="">Loading</div>
<style scoped>
.loading {
p-4 text-center text-gray-500;
}
</style>
Best Practices
Component Organization
Follow these best practices for organizing components:
- Use a clear directory structure:
components/
├── common/ # Shared components
├── layout/ # Layout components
├── features/ # Feature-specific components
└── pages/ # Page components
- Keep components focused and single-responsibility
- Use TypeScript interfaces for props and events
- Document component usage with comments
Performance Optimization
Tips for optimizing stx applications:
- Use lazy loading for large components:
const MyLargeComponent = () => import('./MyLargeComponent.stx')
- Implement proper caching strategies:
const cachedData = useMemo(() => expensiveComputation(), [deps])
- Optimize renders with proper dependency tracking:
const derivedValue = computed(() => source.value * 2)
- Use efficient list rendering:
@foreach(itemsasitem)
<component:key="item":item="item" />