Snice

Decorators & Patterns

Everything is declarative. TypeScript decorators for clean, maintainable components.

@element

Register a custom element. Extends HTMLElement with reactive properties and lifecycle.

@element('my-card')
class MyCard extends HTMLElement {
  @property() title = '';
  @property({ type: Number }) count = 0;

  @render()
  template() {
    return html`<h1>${this.title}</h1>`;
  }
}

@page

Routable page component with route parameters, guards, and lifecycle hooks.

// Guard receives context + route params
const auth: Guard<AppContext> = (ctx, params) => ctx.isAuthenticated();

@page({ tag: 'user-page', routes: ['/users/:id'], guards: [auth] })
class UserPage extends HTMLElement {
  // Route param :id auto-binds to this property
  @property() id = '';
  @property() user = null;

  @ready()
  async load() {
    // this.id is already set from URL
    this.user = await fetch(`/api/users/${this.id}`).then(r => r.json());
  }
}

@controller

Swappable behavior that attaches to elements. Switch implementations at runtime.

@controller('rest')
class RestLoader {
  async attach(el) {
    el.data = await fetch('/api/items').then(r => r.json());
  }
}

@controller('websocket')
class WsLoader {
  attach(el) {
    this.ws = new WebSocket('/ws');
    this.ws.onmessage = e => el.data = JSON.parse(e.data);
  }
  detach() { this.ws.close(); }
}

// <my-list controller="rest"> or <my-list controller="websocket">

@property & @watch

Reactive properties that trigger re-renders. Watch for changes.

@property() name = '';
@property({ type: Number }) count = 0;
@property({ type: Boolean, reflect: true }) active = false;

@watch('count')
onCountChange(newVal, oldVal) {
  console.log(`Count: ${oldVal} → ${newVal}`);
}

@render & @styles

Template method with differential rendering. Scoped CSS styles.

@render()
template() {
  return html`
    <div class="card">
      <h3>${this.title}</h3>
      <if ${this.expanded}>
        <p>${this.content}</p>
      </if>
    </div>
  `;
}

@styles()
componentStyles() {
  return css`
    :host { display: block; }
    .card { padding: 1rem; border-radius: 8px; }
  `;
}

@ready & @dispose

Lifecycle hooks for initialization and cleanup.

@ready()
onMount() {
  this.interval = setInterval(() => this.tick(), 1000);
  console.log('Component connected');
}

@dispose()
cleanup() {
  clearInterval(this.interval);
  this.subscription?.unsubscribe();
}

@query & @queryAll

DOM element references within shadow DOM.

@query('input') inputEl;
@queryAll('.item') items;

@ready()
setup() {
  this.inputEl.focus();
  console.log(`Found ${this.items.length} items`);
}

@on & @dispatch

Event delegation and custom event emission.

// @on: delegated event listener
@on('click', 'button.save')
handleSave(e) { this.save(); }

// @dispatch: return value becomes event.detail
@dispatch('status-changed')
updateStatus(status) {
  this.status = status;
  return { status };
}

// Stacked: DOM event triggers custom event
@on('click', '.item')
@dispatch('item-selected')
handleItemClick(e) {
  return { id: e.target.dataset.id };
}

@context

Receive router navigation context updates.

// Method receives Context on route changes
@context()
handleContext(ctx: Context) {
  this.user = ctx.application.user;
  this.route = ctx.navigation.route;
}

@request & @respond

Async generator pattern for parent-child communication.

// Requester: async generator yields payload, awaits response
@request('fetch-user')
async *fetchUser(id: string) {
  const user = await (yield { id });
  return user;
}

// Responder: receives payload, returns response
@respond('fetch-user')
async handleFetchUser({ id }) {
  return await fetch(`/api/users/${id}`).then(r => r.json());
}

// Usage: const user = await this.fetchUser('123');

Template Syntax

Property binding, conditionals, loops, and event shortcuts.

// Property binding
html`<input .value=${this.text}>`

// Conditionals
html`<if ${this.loading}><snice-spinner></snice-spinner></if>`
html`<if ${this.error}><span>${this.error}</span></if>`

// Loops (use .map())
html`${this.items.map(item => html`<li key=${item.id}>${item.name}</li>`)}`

// Keyboard shortcuts
html`<input @keydown:ctrl+s=${this.save}>`
html`<div @keydown:escape=${this.close}>`