Custom elements

Since 2.10, Micra.define("dropdown", { ... }) also registers a <micra-dropdown> tag. The element mounts itself when it enters the document and tears itself down when it leaves, so you don’t call Micra.start() or Micra.autoCleanup() for it.

<micra-counter>
  <span data-text="count"></span>
  <button @click="increment">+</button>
</micra-counter>

<script src="https://cdn.jsdelivr.net/npm/micra.js@2/dist/micra.min.js"></script>
<script>
  Micra.define("counter", {
    state: { count: 0 },
    increment() {
      this.state.count++;
    },
  });
</script>

Directives, props, this.$el, this.refs and lifecycle hooks all work the same as with data-component. The element sets data-component on itself, so Micra.start(), Micra.destroy(), Micra.instances() and selectors like [data-component="counter"] see it as a regular component once it has connected.

Markup that arrives later

The tag doesn’t care how it got into the page. htmx, Turbo Drive, Turbo Frames, a raw innerHTML assignment, or a CMS block all work without a bridge:

<button hx-get="/cart" hx-target="#slot">Show cart</button>
<div id="slot"></div>
<!-- /cart responds with <micra-cart>…</micra-cart>: it mounts on arrival,
     and the previous content is destroyed when it is swapped out -->

History snapshots are the one exception. Turbo’s page cache and htmx’s history hold rendered rows, and mounting on a restored snapshot duplicates them. For Turbo, add document.addEventListener("turbo:before-cache", () => Micra.destroy()). For htmx with hx-push-url, put hx-history="false" on the page.

What counts as leaving the document:

Which form to use

Use the tag by default. Both forms are supported, and they can sit on the same page.

Use When
<micra-name> The root is a plain container, and the markup may arrive at any time
<div data-component="name"> You mount explicitly and want to control when
<form data-component="name">, <nav …>, <section …>, <ul …> The root’s own element matters: a custom element can’t be a form or a landmark

For the last case you can also wrap the semantic element in the tag and keep the <form> inside it.

Things to know

The tag is inline by default. Browsers give unknown elements display: inline. A root holding block content needs one CSS rule, and a class with its own display overrides it:

micra-counter {
  display: block;
}

Timing while the page loads. While the HTML parser is still running, the element waits for DOMContentLoaded before mounting, so its children are complete. Code that reads an instance immediately after the script needs to wait as well, or call Micra.start(), which mounts right away and is safe alongside the tags:

Micra.define("dropdown", { /* … */ });

document.addEventListener("DOMContentLoaded", () => {
  const dropdown = Micra.instances().get(document.querySelector("micra-dropdown"));
  dropdown.state.items = items;
});

Markup inserted after the page has loaded mounts in a microtask.

Names. A tag is registered only for names of lowercase letters, digits and hyphens, starting with a letter. Micra.define("userCard", …) still works with data-component, but gets no tag; use "user-card". A micra-* element is always treated as a separate component by its parent, so don’t use that prefix for your own elements.

Fill it right away. The element mounts on the first connection. Inserting an empty tag and filling it in the same task works; filling it later, after a fetch, is too late for directives in that content.

Data from a parent. As with data-component, a nested component has its own scope: it doesn’t see the item of a surrounding data-each, and the parent doesn’t apply directives to it. Pass data through data-* attributes rendered by the server, or over the event bus.

WordPress. Post content is filtered for users without the unfiltered_html capability, and unknown tags are stripped there. Blocks and shortcodes rendered by PHP aren’t affected.