npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@miurajs/miura-element

v0.4.7

Published

The core component system for the miura framework. Provides the `MiuraElement` base class for creating reactive web components with properties, computed properties, lifecycle hooks, async resources, error boundaries, two-way binding, and slot utilities.

Readme

@miurajs/miura-element

The core component system for the miura framework. Provides the MiuraElement base class for creating reactive web components with properties, computed properties, lifecycle hooks, async resources, error boundaries, two-way binding, and slot utilities.

Features

  • Decorators@component, @property, @state for cleaner definitions
  • Component Debug Optionsstatic debug or @debug(...) for layer labels, opt-outs, and richer dev overlays
  • Reactive Properties — Type-safe definitions with automatic type conversion and attribute reflection; each property is signal-backed
  • Internal Statestatic state() for private, non-reflected reactive state fields
  • Computed Properties — Derived values with dependency tracking and caching
  • Async Resources$resource() for component-scoped async state with idle, pending, resolved, and rejected states
  • Form State$form() for field binders, validation, dirty/touched tracking, and submit state
  • Lifecycle HooksonMount, onUnmount, willUpdate, shouldUpdate, updated, onAdopt
  • Error BoundariesonError handler with fallback UI and recovery
  • Two-Way Binding& prefix with bind() helper for form elements
  • AOT / JIT Compilerstatic compiler = 'AOT' to opt a component into the zero-DOM-query render path
  • Trusted HTML SubtreestrustedHTML() for sanitized/generated HTML with an afterRender enhancement hook
  • Fine-Grained Template Updates — direct signal-backed property reads can update individual bindings without a full rerender
  • Standalone SignalscreateSignal() / createComputed() for low-level usage outside components
  • Shared Signals$shared(key, initial) for lightweight app-wide reactive state
  • Router Bridge$route(), $routeSelect(), and $routeData() for reactive route context in components
  • Route Resources$routeResource() for param-driven async state tied to navigation
  • Slot UtilitiesquerySlotted() and onSlotChange() for managing distributed content
  • Decorators@component, @property, @computed for concise definitions
  • Islands Architecture<miura-island> wrapper for partial hydration with load, visible, and idle strategies
  • TypeScript — Full type safety with excellent DX

Installation

pnpm add @miurajs/miura-element

Quick Start

import { MiuraElement, html, css, component } from '@miurajs/miura-element';

@component({ tag: 'my-counter' })
class Counter extends MiuraElement {
  declare count: number;

  static properties = {
    count: { type: Number, default: 0 }
  };

  static styles = css`
    :host { display: block; padding: 1rem; }
    button { padding: 0.5rem 1rem; margin: 0 0.5rem; cursor: pointer; }
  `;

  increment = () => { this.count++; };
  decrement = () => { this.count--; };

  template() {
    return html`
      <h3>Count: ${this.count}</h3>
      <button @click=${this.decrement}>-</button>
      <button @click=${this.increment}>+</button>
    `;
  }
}

Decorators (Recommended)

MiuraElement provides TypeScript decorators for cleaner component definitions:

import { MiuraElement, html, css, component, debug, property, state } from '@miurajs/miura-element';

@component({ tag: 'user-card' })
@debug({ label: 'UserCard', showRenderTime: true })
export class UserCard extends MiuraElement {
  @property({ type: String, default: '' })
  name!: string;

  @property({ type: Number, default: 0 })
  age!: number;

  @state({ default: false })
  isEditing!: boolean;

  static get styles() {
    return css`
      :host { display: block; padding: 1rem; }
    `;
  }

  template() {
    return html`
      <h3>${this.name}, ${this.age}</h3>
      <p>Editing: ${this.isEditing}</p>
    `;
  }
}

Benefits:

  • No static properties object needed
  • Auto-registration with @component
  • @property for public props, @state for internal state
  • Type and property definition in one place

For debugger-specific behavior, you can also use static debug:

class BlogCard extends MiuraElement {
  static debug = {
    label: 'BlogCard',
    color: '12, 145, 255',
    report: true,
    layers: true
  };
}

componentDebug(...) is still available as a compatibility alias, but @debug(...) is the preferred decorator name going forward.

See ../../docs/miura-element/decorators.md for full documentation and migration guide.

API Reference

Reactive Properties

static properties = {
  name:     { type: String,  default: 'John' },
  age:      { type: Number,  default: 30 },
  active:   { type: Boolean, default: false },
  items:    { type: Array,   default: [] },
  config:   { type: Object,  default: {} }
};

| Option | Description | |--------|-------------| | type | String, Number, Boolean, Array, Object | | default | Default value | | reflect | Reflect to HTML attribute (default: false) | | attribute | Custom attribute name (default: lowercase property name) |

Computed Properties

static computed() {
  return {
    fullName: {
      dependencies: ['firstName', 'lastName'],
      get() { return `${this.firstName} ${this.lastName}`.trim(); }
    },
    birthYear: {
      dependencies: ['age'],
      get() { return new Date().getFullYear() - this.age; },
      set(value: number) { this.age = new Date().getFullYear() - value; }
    }
  };
}

Async Resources

Use $resource() when a component needs to track async work and rerender automatically as the request state changes.

import { MiuraElement, html, component } from '@miurajs/miura-element';

@component({ tag: 'user-card' })
class UserCard extends MiuraElement {
  declare userId: string;
  user = this.$resource(() => fetch(`/api/users/${this.userId}`).then((r) => r.json()));

  static properties = {
    userId: { type: String, default: '1' }
  };

  template() {
    return this.user.view({
      idle: () => html`<p>Idle</p>`,
      pending: () => html`<p>Loading user...</p>`,
      ok: (user) => html`<h3>${user.name}</h3>`,
      error: (error) => html`<p>Failed: ${String(error)}</p>`
    });
  }
}

$resource() returns an object with:

| Field | Description | |------|-------------| | state | Current state: idle, pending, resolved, rejected | | loading | true while a request is in flight | | value / data | Latest resolved value | | error | Latest rejection value | | promise | The current in-flight promise, if any | | key | Normalized cache key when the resource participates in shared caching | | refresh() | Re-run the loader and update the component | | invalidate() | Clear the local or keyed cached state so the next refresh starts fresh | | refreshing | true when a stale value is being kept visible during revalidation | | hydrate(value) | Seed the resource with a resolved value without running the loader | | view() | Render a template for each resource state |

Pass { auto: false } if you want to create the resource without starting the first request immediately:

class SearchResults extends MiuraElement {
  results = this.$resource(() => this.fetchResults(), { auto: false });

  connectedCallback() {
    super.connectedCallback();
    void this.results.refresh();
  }
}

For reusable async state, pass a key. Keyed resources share cached state across components, dedupe in-flight loads, and can be invalidated explicitly:

class ProfileCard extends MiuraElement {
  declare userId: string;

  profile = this.$resource(
    () => fetch(`/api/users/${this.userId}`).then((r) => r.json()),
    { key: ['profile', this.userId] }
  );

  refreshProfile = () => {
    this.profile.invalidate();
    void this.profile.refresh();
  };
}

Miura also exposes resourceKey(...), invalidateResource(...), invalidateResourceNamespace(...), hasResourceCache(...), and clearResourceCache() for app-level cache control.

You can also tune shared cache behavior with:

  • staleTime to reuse fresh resolved data without refetching immediately
  • cacheTime to describe how long a keyed cache entry should stay around after it becomes inactive
profile = this.$resource(
  () => fetch(`/api/users/${this.userId}`).then((r) => r.json()),
  {
    key: ['profile', this.userId],
    staleTime: 30_000,
    cacheTime: 5 * 60_000
  }
);

Pass { staleWhileRevalidate: true } if you want a resolved value to stay visible while a refresh is in flight:

results = this.$resource(() => this.fetchResults(), {
  key: ['search', this.query],
  staleWhileRevalidate: true
});

Form State

Use $form() when a component needs local form state that works directly with Miura's two-way bindings.

import { MiuraElement, html, component } from '@miurajs/miura-element';

@component({ tag: 'profile-form' })
class ProfileForm extends MiuraElement {
  form = this.$form(
    { name: '', newsletter: false },
    {
      validate: (values) => ({
        name: values.name.trim() ? undefined : 'Name is required'
      }),
      validateAsync: async (values) => {
        const taken = await fetch(`/api/profile/check-name?name=${values.name}`).then((r) => r.json());
        return {
          name: taken.exists ? 'Name is already taken' : undefined
        };
      },
      validateAsyncOn: 'blur'
    }
  );

  async save() {
    await this.form.submit(async (values) => {
      await fetch('/api/profile', {
        method: 'POST',
        body: JSON.stringify(values)
      });
    });
  }

  template() {
    const name = this.form.field('name');
    const newsletter = this.form.field('newsletter');

    return html`
      <form @submit=${this.form.handleSubmit(async (values) => {
        await fetch('/api/profile', {
          method: 'POST',
          body: JSON.stringify(values)
        });
      })}>
        <input &value=${name} @blur=${name.touch}>
        <input type="checkbox" &checked=${newsletter}>
        <p>${name.showError ? name.error ?? '' : ''}</p>
        <p>${this.form.submitError ? 'Save failed' : ''}</p>
        <button ?disabled=${!this.form.valid || this.form.submitting} type="submit">
        ${this.form.submitting ? 'Saving...' : 'Save'}
        </button>
      </form>
    `;
  }
}

Nested field paths are supported too:

form = this.$form({
  profile: {
    name: '',
    meta: { featured: false }
  }
});

template() {
  return html`
    <input &value=${this.form.field('profile.name')}>
    <input type="checkbox" &checked=${this.form.field('profile.meta.featured')}>
  `;
}

$form() returns an object with:

| Field | Description | |------|-------------| | values / data | Current form values | | initialValues | Baseline values used for dirty checks and reset() | | errors | Current validation errors | | visibleErrors | Validation errors for touched fields only | | dirty | true when any field differs from its initial value | | valid | true when no validation errors are present | | validating | true while validateAsync() is running | | submitting | true while submit() is in flight | | submitError | Last error thrown by submit() | | submitResult | Last resolved value returned by submit() | | submitSucceeded | true after a successful submit until the form changes or state is cleared | | touched | Set of fields changed or manually touched | | field(name) | Returns a binder with value, set, touch, isTouched, isDirty, showError, and error | | set(name, value) | Set one field value | | patch(values) | Update multiple fields at once | | reset(values?) | Reset to the initial values or replace the baseline entirely | | touchAll() | Mark every field as touched | | shouldShowError(name) | true when a field is touched and currently invalid | | validate() | Re-run validation and return whether the form is valid | | validateAsync() | Run async validation and return whether the form is valid | | clearSubmitState() | Clear submitError and submitResult | | setErrors(errors, { touch }) | Apply field errors directly, useful for server validation responses | | clearErrors() | Clear current field errors | | failSubmit(error, { errors, touch }) | Record a submit failure, optionally map field errors, then rethrow | | view({ ... }) | Render idle, validating, submitting, success, or error submit states declaratively | | submit(handler) | Validate, set submitting, run the async handler, then clear submitting | | handleSubmit(handler) | Wrap a native form submit event, prevent default, and run submit(handler) |

Invalid submit() calls automatically mark all fields as touched, which makes it easy to reveal all validation messages after the first submit attempt without extra template logic.

Use validateAsync for checks like username uniqueness or server-backed business rules. It is explicit by default: Miura runs it during submit() or when you call form.validateAsync() yourself.

Automatic async validation is opt-in:

  • validateAsyncOn: 'manual' keeps validation explicit
  • validateAsyncOn: 'blur' runs async validation when a field is touched
  • validateAsyncOn: 'change' runs debounced async validation after value changes
  • validateAsyncDebounce controls the debounce delay for 'change' mode

For server-side validation, failSubmit() lets you map field errors and preserve the submit failure in one place:

await this.form.submit(async (values, form) => {
  try {
    await api.saveProfile(values);
  } catch (error) {
    form.failSubmit(error, {
      errors: {
        name: 'Name already exists'
      },
      touch: true
    });
  }
});

For state-specific UI around submit flows, view() keeps the branching close to the form:

${this.form.view({
  idle: () => html`<p>Ready</p>`,
  validating: () => html`<p>Checking...</p>`,
  submitting: () => html`<p>Saving...</p>`,
  success: () => html`<p>Saved</p>`,
  error: (error) => html`<p>${String(error)}</p>`
})}

Shared State

Use $shared() when multiple components should react to the same lightweight piece of state without setting up a larger store.

class ThemeToggle extends MiuraElement {
  theme = this.$shared('theme', 'light');

  template() {
    return html`
      <button @click=${() => this.theme(this.theme() === 'light' ? 'dark' : 'light')}>
        Theme: ${this.theme}
      </button>
    `;
  }
}

class ThemeBadge extends MiuraElement {
  theme = this.$shared('theme', 'light');

  template() {
    return html`<p>Current theme: ${this.theme}</p>`;
  }
}

Components using the same key receive the same shared signal. For larger workflows, actions, middleware, or persistence, use miura-data-flow.

Best practice is to namespace shared keys to avoid collisions. Prefer keys like blog-editor:theme over very generic keys like theme.

Miura also supports helpers for this:

import { createSharedNamespace, sharedKey } from '@miurajs/miura-element';

const blogShared = createSharedNamespace('blog-editor');

const draft = blogShared.use('draft', '');
const autosave = sharedKey('blog-editor', 'autosave');

You can also pass array keys directly:

this.theme = this.$shared(['blog-editor', 'theme'], 'light');

Context Injection

Use tree-scoped context when a parent should expose data or services to deep descendants without threading them through props or global shared keys.

import { MiuraElement, createContextKey, html, createSignal, type Signal } from '@miurajs/miura-element';

const themeContext = createContextKey<Signal<string>>('theme');

class ThemeProvider extends MiuraElement {
  theme = this.$signal('light');

  constructor() {
    super();
    this.$provide(themeContext, this.theme);
  }

  template() {
    return html`
      <button @click=${() => this.theme(this.theme() === 'light' ? 'dark' : 'light')}>
        Toggle
      </button>
      <theme-badge></theme-badge>
    `;
  }
}

class ThemeBadge extends MiuraElement {
  template() {
    const theme = this.$inject(themeContext, createSignal('light'));
    return html`<p>Theme: ${theme}</p>`;
  }
}

The nearest provider wins, so nested layouts can override context locally. For reactive context, provide a signal, resource, form, or another reactive primitive and let descendants bind to it directly.

Router Bridge

Use the router bridge helpers when a component should react to route context or loader data directly.

class ProfilePage extends MiuraElement {
  route = this.$route(router);
  pathname = this.$routeSelect(router, (context) => context?.pathname ?? '/');
  profile = this.$routeData(router, 'profile');

  template() {
    return html`
      <p>${this.pathname}</p>
      <h2>${this.profile()?.name ?? 'Loading...'}</h2>
    `;
  }
}

These helpers wrap the router's reactive route signals so components can consume route state without manually threading context.data through props.

When route params should drive async fetching, use $routeResource():

class ProfilePage extends MiuraElement {
  profile = this.$routeResource(
    router,
    (context) => context?.params.id,
    (id) => fetch(`/api/users/${id}`).then((r) => r.json()),
    {
      skip: (id) => !id,
      hydrateFromRouteData: 'profile',
      staleWhileRevalidate: true
    }
  );

  template() {
    return this.profile.view({
      idle: () => html`<p>Select a user</p>`,
      pending: () => html`<p>Loading...</p>`,
      ok: (user) => html`<h2>${user.name}</h2>`,
      error: (error) => html`<p>${String(error)}</p>`
    });
  }
}

$routeResource() derives a stable cache key from the selected route value by default, so route-driven resources can reuse cached data naturally across navigation. You can override that with key, seed the resource from route loader data with hydrateFromRouteData, and keep stale data visible during refreshes with staleWhileRevalidate.

$routeData() can also return the full loader data object when you omit the key:

routeData = this.$routeData<{ profile?: Profile; permissions?: string[] }>(router);

And hydrateFromRouteData: true will hydrate a route resource from the whole route data payload:

profile = this.$routeResource(
  router,
  (context) => context?.params.id,
  (id) => fetch(`/api/users/${id}`).then((r) => r.json()),
  {
    hydrateFromRouteData: true,
    staleWhileRevalidate: true
  }
);

Styles

static styles = css`
  :host { display: block; padding: 1rem; }
  .title { font-size: 1.5rem; font-weight: bold; }
`;

Lifecycle Hooks

| Hook | When | Use Case | |------|------|----------| | onMount() | Once, after first render | Fetch data, init third-party libraries | | onUnmount() | On disconnect | Cancel fetches, dispose resources | | willUpdate(changed) | Before each render | Derive values from changed properties | | shouldUpdate(changed) | Before each render | Return false to skip an unnecessary render | | updated(changed) | After each render | Post-render DOM operations | | onAdopt() | adoptedCallback | Handle document changes |

@component({ tag: 'my-widget' })
class MyWidget extends MiuraElement {
  onMount() {
    this.data = await fetch('/api/data').then(r => r.json());
  }

  onUnmount() {
    this.abortController?.abort();
  }

  willUpdate(changed) {
    if (changed.has('items')) {
      this.filteredItems = this.items.filter(i => i.active);
    }
  }

  shouldUpdate(changed) {
    // Skip renders that only change internal bookkeeping
    return !changed.has('_internalTick');
  }
}

Error Boundaries

Override onError(error) to catch rendering errors. Return true to suppress console.error.

@component({ tag: 'safe-widget' })
class SafeWidget extends MiuraElement {
  onError(error: Error): boolean {
    this.shadowRoot!.innerHTML = `
      <div class="error">
        <h3>Something went wrong</h3>
        <p>${error.message}</p>
        <button onclick="this.getRootNode().host.recover()">Retry</button>
      </div>
    `;
    return true; // suppress console.error
  }
}

Two-Way Binding (&)

The & prefix creates a two-way binding that sets a DOM property and listens for the corresponding event to push changes back.

template() {
  return html`
    <!-- Using the bind() helper (recommended) -->
    <input &value=${this.bind('name')}>
    <input type="checkbox" &checked=${this.bind('agree')}>

    <!-- Using a tuple [currentValue, setter] -->
    <input &value=${[this.email, (v) => this.email = v]}>
  `;
}

Auto-detected events:

| Property | Event | |----------|-------| | value | input | | checked | change | | selected | change | | files | change | | (other) | input |

Slot Utilities

@component({ tag: 'my-card' })
class MyCard extends MiuraElement {
  onMount() {
    // Query slotted elements
    const headerEls = this.querySlotted('header');

    // React to slot changes
    this.onSlotChange('', (elements) => {
      this.hasContent = elements.length > 0;
    });
  }

  template() {
    return html`
      <div class="card">
        <slot name="header"></slot>
        <slot></slot>
        <slot name="footer"></slot>
      </div>
    `;
  }
}

Helper Methods

emit(eventName, detail?, options?)

Convenience wrapper for dispatching custom events.

@component({ tag: 'mui-drawer' })
class MuiDrawer extends MiuraElement {
  private handleClose() {
    this.open = false;
    this.emit('close');
  }

  private handleToggle() {
    this.collapsed = !this.collapsed;
    this.emit('toggle', { collapsed: this.collapsed });
  }

  private handleSelect(id: string) {
    // Emit with bubbles and composed for cross shadow-DOM
    this.emit('item-select', { id }, { bubbles: true, composed: true });
  }
}

Options:

  • bubbles (default: false) - Event bubbles up the DOM
  • composed (default: false) - Event crosses shadow DOM boundary
  • cancelable (default: false) - Event can be prevented

hasSlot(name)

Checks if a named slot has assigned content. Useful for conditional rendering.

@component({ tag: 'mui-panel' })
class MuiPanel extends MiuraElement {
  template() {
    return html`
      <div class="panel">
        ${when(this.hasSlot('actions'),
          () => html`
            <div class="actions">
              <slot name="actions"></slot>
            </div>
          `
        )}
        <slot></slot>
      </div>
    `;
  }
}

Template Binding Reference

| Prefix | Type | Example | |--------|------|---------| | (none) | Text / Node | ${this.name} | | @ | Event | @click=${this.handler} | | . | Property | .value=${this.text} | | ? | Boolean attribute | ?disabled=${this.off} | | & | Two-way binding | &value=${this.bind('name')} | | # | Reference / Directive | #ref, #if, #for | | class | Class map | class=${{ active: true }} | | style | Style object | style=${{ color: 'red' }} |

Event modifiers via |: @click|prevent=${handler}, @click|prevent,stop=${handler}

Trusted HTML Subtrees

Use trustedHTML() when a component needs to render sanitized or generated HTML as DOM. Prefer it over .innerHTML=${...} because it is explicit, participates in node binding updates, and gives you an afterRender hook for post-processing:

import { html, trustedHTML } from '@miurajs/miura-element';

template() {
  return html`
    <article class="content">
      ${trustedHTML(this.renderedHtml, {
        afterRender: (root) => {
          renderMermaid(root);
          mountEmbeds(root);
        }
      })}
    </article>
  `;
}

Miura does not sanitize the string. Sanitize user content before wrapping it. trustHTML() remains available as a compatibility alias.

Conditional Rendering

import { when, choose } from '@miurajs/miura-element';

// when(condition, trueCase, falseCase?)
${when(this.loggedIn,
  () => html`<user-panel></user-panel>`,
  () => html`<login-form></login-form>`
)}

// choose(value, [...cases], default?)
${choose(this.status, [
  ['loading', () => html`<spinner></spinner>`],
  ['error',   () => html`<error-msg></error-msg>`],
  ['ready',   () => html`<content></content>`],
])}

Keyed List Rendering

import { repeat } from '@miurajs/miura-element';

${repeat(this.items,
  (item) => item.id,                              // key function
  (item, i) => html`<item-card .data=${item}>`     // template function
)}

Uses an LIS-based (Longest Increasing Subsequence) diffing algorithm to compute the minimal set of DOM moves when items are reordered.

Async Rendering

import { createAsyncTracker, resolveAsync } from '@miurajs/miura-element';

// Create tracker (e.g. in onMount or event handler)
this.userTracker = createAsyncTracker(
  fetchUser(this.userId),
  () => this.requestUpdate()
);

// In template()
${resolveAsync(this.userTracker,
  (user) => html`<p>Hello ${user.name}</p>`,       // resolved
  ()     => html`<p>Loading...</p>`,                // pending
  (err)  => html`<p>Error: ${err.message}</p>`      // rejected
)}

Virtual Scrolling

Use the #virtualScroll directive to virtualize a large list. The directive manages the scroll container, spacer, and visible slice internally — no manual scroll listeners needed:

template() {
  return html`
    <div #virtualScroll=${{
      items: this.items,       // full list (e.g. 10,000 items)
      itemHeight: 40,          // fixed row height in px
      containerHeight: 400,    // viewport height in px
      render: (item, i) => html`<div class="row">${item.name}</div>`,
      overscan: 3,             // extra rows above/below viewport
    }}></div>
  `;
}

The lower-level computeVirtualSlice() function is also available for custom implementations:

import { computeVirtualSlice } from '@miurajs/miura-element';

const vs = computeVirtualSlice({
  items: this.items,
  itemHeight: 40,
  containerHeight: 400,
  render: (item, i) => html`<div class="row">${item.name}</div>`,
  overscan: 3,
}, this.scrollTop);

// vs.visibleItems, vs.totalHeight, vs.startIndex, etc.

Internal State (static state())

Use static state() for reactive fields that are private to the component and should not reflect to HTML attributes:

@component({ tag: 'search-box' })
class SearchBox extends MiuraElement {
  // Public properties — reflected, observable from outside
  static properties = {
    placeholder: { type: String, default: '' },
  };

  // Internal state — reactive but not reflected
  static state() {
    return {
      query:     { type: String,  default: '' },
      loading:   { type: Boolean, default: false },
      results:   { type: Array,   default: [] },
    };
  }

  declare placeholder: string;
  declare query: string;
  declare loading: boolean;
  declare results: unknown[];
}

AOT / JIT Rendering Compiler

Every component defaults to JIT (Just-in-Time) rendering via the TemplateProcessor pipeline, which supports every binding type. Add static compiler = 'AOT' as const to opt a component class into the faster compiled path:

@component({ tag: 'data-row' })
class DataRow extends MiuraElement {
  static compiler = 'AOT' as const;
  declare label: string;
  declare value: number;
  static properties = {
    label: { type: String, default: '' },
    value: { type: Number, default: 0 },
  };

  template() {
    return html`<td>${this.label}</td><td>${this.value}</td>`;
  }
}

| | JIT (default) | AOT | |---|---|---| | First render | TemplateProcessorBinding[] | TemplateCompilerrender(){ fragment, refs } | | Updates | instance.update(values) | Direct refs[N].el.prop = vzero DOM queries | | Directives / repeat() | ✅ Full support | ✅ Delegated to NodeBinding / DirectiveBinding | | Signals / trustedHTML() | ✅ Full support | ✅ Signal unwrap + runtime binding delegation | | Best for | All components | High-frequency updates, list rows, counters |

Miura can promote direct template reads of signal-backed properties into binding-level subscriptions. For example, ${this.label} can update the text binding directly when label changes; ${this.label.toUpperCase()} still rerenders the component because the expression is transformed. This promotion works in both JIT and AOT templates, including trusted HTML subtrees.

Standalone Signals

Create reactive values outside of components:

import { createSignal, createComputed } from '@miurajs/miura-element';

const count = createSignal(0);
const label = createComputed(() => `Count: ${count()}`);

count(count() + 1);
console.log(label()); // 'Count: 1'

Signals created with createSignal() / createComputed() can be passed directly into html bindings.

For decorated signal-backed fields, use this.$.fieldName in templates when you want the direct fine-grained binding path while keeping plain property syntax in component logic. These field refs expose .value, .peek(), .subscribe(), and .map(...) in addition to being directly bindable. this.$ref('fieldName') remains available when you need explicit programmatic access.

Component updates and fine-grained binding updates are scheduled through the shared Miura render scheduler. Multiple same-tick writes to the same component or binding collapse into one DOM pass, while lifecycle hooks still run after the committed update.

When Miura skips fine-grained direct-read promotion because a fallback value is ambiguous, it emits a structured debugger warning instead of silently risking a wrong binding. This is most useful for patterns like ${this.subtitle || 'Default'} where the initial value is an empty string.

For event channels, use this.$emit(channel, value?) to publish and this.$on(channel, handler) / this.$once(channel, handler) to subscribe with automatic cleanup on disconnect.

Best Practices

  1. Use computed properties for derived state instead of manual updates
  2. Keep dependency arrays minimal — only what the computed actually reads
  3. Use static state() for internal UI state — keeps it off attributes and out of observed properties
  4. Use arrow functions for event handlers — automatic this binding
  5. Use static compiler = 'AOT' for hot paths — rows, counters, table cells that update frequently
  6. Use shouldUpdate sparingly — only to skip truly unnecessary renders
  7. Clean up in onUnmount — abort controllers, remove global listeners, dispose resources
  8. Use & binding for forms — cleaner than manual @input + .value wiring

🖥️ Server-side Utilities (@miurajs/miura-element/server)

Import from @miurajs/miura-element/server in Node.js / SSR / SSG contexts. Zero DOM dependency.

import { createIslandHTML, IslandRegistry, renderIslands } from '@miurajs/miura-element/server';

createIslandHTML(def)

Generate a single <miura-island> HTML string from a definition object:

const html = createIslandHTML({
  component: 'my-counter',
  props:     { count: 5 },
  hydrate:   'visible',
  placeholder: '<my-counter count="5">5</my-counter>',
});
// → <miura-island component="my-counter" hydrate="visible">
//     <script type="application/json">{"count":5}</script>
//     <my-counter count="5">5</my-counter>
//   </miura-island>

IslandRegistry

Register island defaults once at app boot; look them up anywhere in your SSR templates:

IslandRegistry.register('my-counter', { props: { count: 0 }, hydrate: 'load' });
IslandRegistry.register('app-chart',  { props: { data: [] }, hydrate: 'visible' });

// In a route handler — override props per-request
const html = IslandRegistry.render('my-counter', { props: { count: req.session.count } });

renderIslands(defs)

Batch-render an array of island definitions and get a typed manifest back:

const { rendered, manifest } = renderIslands([
  { component: 'my-counter', props: { count: 5 } },
  { component: 'app-chart',  props: { data: [] }, hydrate: 'visible' },
]);
// manifest.total === 2
// manifest.entries[].component, .hydrate, .count

🏝️ Islands Architecture (<miura-island>)

<miura-island> is a partial hydration wrapper. It renders static SSR'd HTML immediately and lazily creates the interactive component when the chosen strategy fires.

<!-- Hydrate immediately (default) -->
<miura-island component="my-counter">
  <script type="application/json">{"count": 5}</script>
  <!-- SSR placeholder shown before JS runs -->
  <my-counter count="5">5</my-counter>
</miura-island>

<!-- Hydrate when scrolled into view -->
<miura-island component="app-chart" hydrate="visible">
  <script type="application/json">{"data": [1,2,3]}</script>
  <div class="chart-placeholder">…</div>
</miura-island>

<!-- Hydrate during browser idle time -->
<miura-island component="like-button" hydrate="idle" data-props='{"liked":false}'>
</miura-island>

Hydration strategies

| hydrate value | When it fires | |---|---| | "load" (default) | Immediately in connectedCallback | | "visible" | When the island enters the viewport (IntersectionObserver, 200 px root margin) | | "idle" | During browser idle time (requestIdleCallback, 2 s timeout fallback) | | anything else | Never — call island.hydrate() imperatively |

Props channel

Props are applied as properties (not attributes) on the created element for full type fidelity:

  1. First source: <script type="application/json"> child element (recommended for SSR)
  2. Second source: data-props attribute (JSON string)

Inside a hydrated component, use $islandProps() to access the full serialized props object without manually re-parsing the script payload:

class PostIsland extends MiuraElement {
  props = this.$islandProps<{ slug: string; post: { title: string } }>();

  template() {
    return html`<h2>${this.props.post.title}</h2>`;
  }
}

When the server already sent data you want to keep using on the client, $islandResource() can start from that payload and optionally revalidate:

class PostIsland extends MiuraElement {
  props = this.$islandProps<{ slug: string; post: Post }>();

  post = this.$islandResource(
    () => this.props.post,
    () => fetch(`/api/posts/${this.props.slug}`).then((r) => r.json()),
    {
      key: () => ['post', this.props.slug],
      staleWhileRevalidate: true
    }
  );
}

Set revalidate: false when the island payload should remain fully static on the client.

Events

miura-island:hydrated bubbles from the island after the component mounts. event.detail.element is the created component, event.detail.props are the resolved props.

island.addEventListener('miura-island:hydrated', (e) => {
  console.log('hydrated', e.detail.element);
});

// Imperative hydration
island.hydrate();

License

MIT