Skip to content

Craftit Examples

Real-world examples demonstrating common use cases and patterns with Craftit.

💡 Complete Applications

These are complete, production-ready examples. For API reference, see API Documentation. For basic usage patterns, see Usage Guide.

Table of Contents

Basic Examples

Counter Component

Simple interactive counter with increment/decrement buttons.

ts
import { defineElement, html, css } from '@vielzeug/craftit';
defineElement('simple-counter', {
  state: { count: 0 },
  template: (el) => html`
    <div class="counter">
      <h2>Count: ${el.state.count}</h2>
      <button class="decrement">-</button>
      <button class="reset">Reset</button>
      <button class="increment">+</button>
    </div>
  `,
  styles: [
    css`
      .counter {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 1rem;
        padding: 2rem;
      }
      button {
        padding: 0.5rem 1.5rem;
        font-size: 1rem;
        cursor: pointer;
      }
    `,
  ],
  onConnected(el) {
    el.on('.increment', 'click', () => el.state.count++);
    el.on('.decrement', 'click', () => el.state.count--);
    el.on('.reset', 'click', () => (el.state.count = 0));
  },
});

Todo List

Dynamic list with add/remove functionality.

ts
defineElement('todo-list', {
  state: {
    todos: ['Learn Craftit', 'Build components'],
    input: '',
  },
  template: (el) => html`
    <div class="todo-app">
      <h2>My Todos</h2>
      <div class="add-todo">
        <input type="text" placeholder="New todo..." value="${el.state.input}" />
        <button class="add">Add</button>
      </div>
      <ul class="todo-list">
        ${el.state.todos
          .map(
            (todo, i) => `
          <li>
            <span>${todo}</span>
            <button class="delete" data-index="${i}">×</button>
          </li>
        `,
          )
          .join('')}
      </ul>
    </div>
  `,
  onConnected(el) {
    el.on('input', 'input', (e) => {
      el.state.input = (e.currentTarget as HTMLInputElement).value;
    });
    el.on('.add', 'click', () => {
      if (el.state.input.trim()) {
        el.state.todos.push(el.state.input);
        el.state.input = '';
      }
    });
    el.on('.delete', 'click', (e) => {
      const index = +(e.currentTarget as HTMLElement).dataset.index!;
      el.state.todos.splice(index, 1);
    });
  },
});

Form Examples

Custom Input Component

Form-associated custom input with validation.

ts
defineElement('custom-input', {
  state: {
    value: '',
    error: '',
  },
  template: (el) => html`
    <div class="input-wrapper">
      <label>
        <span class="label">${el.label || 'Input'}</span>
        <input type="text" value="${el.state.value}" placeholder="${el.placeholder || ''}" />
      </label>
      ${el.state.error ? `<span class="error">${el.state.error}</span>` : ''}
    </div>
  `,
  formAssociated: true,
  observedAttributes: ['label', 'placeholder', 'required'] as const,
  onConnected(el) {
    el.on('input', 'input', (e) => {
      const value = (e.currentTarget as HTMLInputElement).value;
      el.state.value = value;
      // Validation
      if (el.required && !value) {
        el.state.error = 'This field is required';
        el.form?.valid({ valueMissing: true }, 'Required');
      } else {
        el.state.error = '';
        el.form?.valid();
      }
      el.form?.value(value);
    });
  },
});
// Usage
// <form>
//   <custom-input name="username" label="Username" required></custom-input>
//   <button type="submit">Submit</button>
// </form>

Complete Form

Full form example with multiple fields and validation.

ts
defineElement('registration-form', {
  state: {
    email: '',
    password: '',
    confirmPassword: '',
    errors: {} as Record<string, string>,
  },
  template: (el) => html`
    <form>
      <h2>Register</h2>
      <div class="field">
        <input type="email" placeholder="Email" value="${el.state.email}" name="email" />
        ${el.state.errors.email ? `<span class="error">${el.state.errors.email}</span>` : ''}
      </div>
      <div class="field">
        <input type="password" placeholder="Password" value="${el.state.password}" name="password" />
        ${el.state.errors.password ? `<span class="error">${el.state.errors.password}</span>` : ''}
      </div>
      <div class="field">
        <input
          type="password"
          placeholder="Confirm Password"
          value="${el.state.confirmPassword}"
          name="confirmPassword" />
        ${el.state.errors.confirmPassword ? `<span class="error">${el.state.errors.confirmPassword}</span>` : ''}
      </div>
      <button type="submit">Register</button>
    </form>
  `,
  onConnected(el) {
    el.on('input', 'input', (e) => {
      const input = e.currentTarget as HTMLInputElement;
      const name = input.name;
      const value = input.value;
      el.state[name] = value;
      el.validate();
    });
    el.on('form', 'submit', async (e) => {
      e.preventDefault();
      if (el.validate()) {
        await el.submitForm();
      }
    });
  },
  // Methods via set
  async onUpdated(el) {
    // Add helper methods
    (el as any).validate = () => {
      const errors: Record<string, string> = {};
      if (!el.state.email.includes('@')) {
        errors.email = 'Invalid email';
      }
      if (el.state.password.length < 8) {
        errors.password = 'Password must be at least 8 characters';
      }
      if (el.state.password !== el.state.confirmPassword) {
        errors.confirmPassword = 'Passwords do not match';
      }
      el.state.errors = errors;
      return Object.keys(errors).length === 0;
    };
    (el as any).submitForm = async () => {
      try {
        const response = await fetch('/api/register', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            email: el.state.email,
            password: el.state.password,
          }),
        });
        if (response.ok) {
          el.emit('registration-success');
        }
      } catch (error) {
        el.state.errors = { form: 'Registration failed' };
      }
    };
  },
});

Advanced Examples

Data Table with Sorting

ts
type User = {
  id: number;
  name: string;
  email: string;
  role: string;
};
defineElement('data-table', {
  state: {
    users: [
      { id: 1, name: 'Alice', email: 'alice@example.com', role: 'Admin' },
      { id: 2, name: 'Bob', email: 'bob@example.com', role: 'User' },
      { id: 3, name: 'Charlie', email: 'charlie@example.com', role: 'User' },
    ] as User[],
    sortBy: 'name' as keyof User,
    sortOrder: 'asc' as 'asc' | 'desc',
  },
  template: (el) => {
    const sorted = [...el.state.users].sort((a, b) => {
      const aVal = a[el.state.sortBy];
      const bVal = b[el.state.sortBy];
      const mult = el.state.sortOrder === 'asc' ? 1 : -1;
      return aVal > bVal ? mult : aVal < bVal ? -mult : 0;
    });
    return html`
      <table>
        <thead>
          <tr>
            <th data-col="name">
              Name ${el.state.sortBy === 'name' ? (el.state.sortOrder === 'asc' ? '↑' : '↓') : ''}
            </th>
            <th data-col="email">
              Email ${el.state.sortBy === 'email' ? (el.state.sortOrder === 'asc' ? '↑' : '↓') : ''}
            </th>
            <th data-col="role">
              Role ${el.state.sortBy === 'role' ? (el.state.sortOrder === 'asc' ? '↑' : '↓') : ''}
            </th>
          </tr>
        </thead>
        <tbody>
          ${sorted
            .map(
              (user) => `
            <tr>
              <td>${user.name}</td>
              <td>${user.email}</td>
              <td>${user.role}</td>
            </tr>
          `,
            )
            .join('')}
        </tbody>
      </table>
    `;
  },
  onConnected(el) {
    el.on('th', 'click', (e) => {
      const col = (e.currentTarget as HTMLElement).dataset.col as keyof User;
      if (el.state.sortBy === col) {
        el.state.sortOrder = el.state.sortOrder === 'asc' ? 'desc' : 'asc';
      } else {
        el.state.sortBy = col;
        el.state.sortOrder = 'asc';
      }
    });
  },
});

Async Data Loading

ts
defineElement('user-profile', {
  state: {
    userId: 1,
    user: null as any,
    loading: false,
    error: null as string | null,
  },
  template: (el) => html`
    <div class="profile">
      ${el.state.loading
        ? `
        <div class="loading">Loading...</div>
      `
        : el.state.error
          ? `
        <div class="error">${el.state.error}</div>
      `
          : el.state.user
            ? `
        <div class="user-info">
          <h2>${el.state.user.name}</h2>
          <p>${el.state.user.email}</p>
          <p>Role: ${el.state.user.role}</p>
        </div>
      `
            : ''}
    </div>
  `,
  observedAttributes: ['user-id'] as const,
  async onConnected(el) {
    await (el as any).loadUser();
  },
  onAttributeChanged(name, oldVal, newVal, el) {
    if (name === 'user-id' && oldVal !== newVal) {
      el.state.userId = Number(newVal);
      (el as any).loadUser();
    }
  },
  async onUpdated(el) {
    (el as any).loadUser = async () => {
      await el.set(async (state) => {
        try {
          const response = await fetch(`/api/users/${state.userId}`);
          const user = await response.json();
          return { ...state, user, loading: false, error: null };
        } catch (error) {
          return {
            ...state,
            user: null,
            loading: false,
            error: 'Failed to load user',
          };
        }
      });
    };
  },
});
ts
defineElement('modal-dialog', {
  state: {
    isOpen: false,
    title: '',
    content: '',
  },
  template: (el) => html`
    ${el.state.isOpen
      ? `
      <div class="modal-overlay">
        <div class="modal">
          <div class="modal-header">
            <h2>${el.state.title}</h2>
            <button class="close">×</button>
          </div>
          <div class="modal-body">
            ${el.state.content}
          </div>
          <div class="modal-footer">
            <button class="cancel">Cancel</button>
            <button class="confirm">Confirm</button>
          </div>
        </div>
      </div>
    `
      : ''}
  `,
  styles: [
    css`
      .modal-overlay {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background: rgba(0, 0, 0, 0.5);
        display: flex;
        align-items: center;
        justify-content: center;
        z-index: 1000;
      }
      .modal {
        background: white;
        border-radius: 8px;
        min-width: 400px;
        max-width: 90vw;
        max-height: 90vh;
        overflow: auto;
      }
      .modal-header {
        display: flex;
        justify-content: space-between;
        padding: 1rem;
        border-bottom: 1px solid #eee;
      }
      .modal-body {
        padding: 1rem;
      }
      .modal-footer {
        display: flex;
        gap: 0.5rem;
        justify-content: flex-end;
        padding: 1rem;
        border-top: 1px solid #eee;
      }
    `,
  ],
  onConnected(el) {
    el.on('.close', 'click', () => {
      el.state.isOpen = false;
      el.emit('close');
    });
    el.on('.cancel', 'click', () => {
      el.state.isOpen = false;
      el.emit('cancel');
    });
    el.on('.confirm', 'click', () => {
      el.emit('confirm');
      el.state.isOpen = false;
    });
    el.on('.modal-overlay', 'click', (e) => {
      if (e.target === e.currentTarget) {
        el.state.isOpen = false;
        el.emit('close');
      }
    });
  },
});
// Usage
const modal = document.querySelector('modal-dialog');
modal.state.title = 'Confirm Action';
modal.state.content = 'Are you sure you want to continue?';
modal.state.isOpen = true;
modal.addEventListener('confirm', () => {
  console.log('User confirmed!');
});

Framework Integration

Craftit web components work seamlessly with all major frameworks. Here's how to integrate the same counter component across different frameworks:

tsx
import { defineElement, html } from '@vielzeug/craftit';
import { useEffect, useRef, useState } from 'react';

// Define the web component
defineElement('counter-component', {
  state: { count: 0 },
  template: (el) => html`
    <div>
      <p>Count: ${el.state.count}</p>
      <button class="increment">+</button>
    </div>
  `,
  onConnected(el) {
    el.on('.increment', 'click', () => el.state.count++);
  },
});

// Use in React
function CounterWrapper() {
  const ref = useRef<any>(null);
  const [count, setCount] = useState(0);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;

    const unwatch = el.watch(
      (state: any) => state.count,
      (count: number) => setCount(count),
    );

    return unwatch;
  }, []);

  return (
    <div>
      <counter-component ref={ref} />
      <p>React sees count: {count}</p>
    </div>
  );
}
vue
<script setup lang="ts">
import { defineElement, html } from '@vielzeug/craftit';
import { ref, onMounted } from 'vue';

defineElement('counter-component', {
  state: { count: 0 },
  template: (el) => html`
    <div>
      <p>Count: ${el.state.count}</p>
      <button class="increment">+</button>
    </div>
  `,
  onConnected(el) {
    el.on('.increment', 'click', () => el.state.count++);
  },
});

const counter = ref<any>(null);
const count = ref(0);

onMounted(() => {
  counter.value?.watch(
    (state: any) => state.count,
    (val: number) => (count.value = val),
  );
});
</script>

<template>
  <div>
    <counter-component ref="counter" />
    <p>Vue sees count: {{ count }}</p>
  </div>
</template>
svelte
<script lang="ts">
  import { defineElement, html } from '@vielzeug/craftit';
  import { onMount } from 'svelte';

  defineElement('counter-component', {
    state: { count: 0 },
    template: (el) => html`
      <div>
        <p>Count: ${el.state.count}</p>
        <button class="increment">+</button>
      </div>
    `,
    onConnected(el) {
      el.on('.increment', 'click', () => el.state.count++);
    }
  });

  let counter: any;
  let count = 0;

  onMount(() => {
    counter?.watch(
      (state: any) => state.count,
      (val: number) => count = val
    );
  });
</script>

<counter-component bind:this={counter} />
<p>Svelte sees count: {count}</p>
ts
import { defineElement, html } from '@vielzeug/craftit';

defineElement('counter-component', {
  state: { count: 0 },
  template: (el) => html`
    <div>
      <p>Count: ${el.state.count}</p>
      <button class="increment">+</button>
    </div>
  `,
  onConnected(el) {
    el.on('.increment', 'click', () => el.state.count++);
  },
});

// Use directly in HTML
const counter = document.createElement('counter-component');
document.body.appendChild(counter);

// Watch state changes
let count = 0;
counter.watch(
  (state) => state.count,
  (val) => {
    count = val;
    console.log('Count:', count);
  },
);

Testing Examples

Unit Testing

ts
import { defineElement, html, attach, destroy } from '@vielzeug/craftit';
describe('Counter Component', () => {
  it('increments count when button clicked', async () => {
    defineElement('test-counter', {
      state: { count: 0 },
      template: (el) => html`
        <div class="count">${el.state.count}</div>
        <button class="increment">+</button>
      `,
      onConnected(el) {
        el.on('.increment', 'click', () => el.state.count++);
      },
    });
    const el = document.createElement('test-counter');
    await attach(el);
    expect(el.find('.count')?.textContent).toBe('0');
    el.find<HTMLButtonElement>('.increment')?.click();
    await el.flush();
    expect(el.find('.count')?.textContent).toBe('1');
    destroy(el);
  });
  it('handles state updates', async () => {
    const el = document.createElement('test-counter');
    await attach(el);
    await el.set({ count: 10 });
    expect(el.find('.count')?.textContent).toBe('10');
    destroy(el);
  });
});