Components let you define reusable custom elements using a <script type="text/hyperscript-template"> tag.
You write markup with ${} interpolation, attach hyperscript with _=, and the
runtime handles reactive re-rendering via morphing.
Components is an extension and must be loaded separately after hyperscript.
See the component feature page for installation details.
A component is a <script type="text/hyperscript-template"> with a component attribute. The value is the
tag name you will use (it must contain a dash, per the custom elements spec):
<script type="text/hyperscript-template" component="hello-world">
<span>Hello World</span>
</script>
<hello-world></hello-world>
<hello-world></hello-world>
Place the <script type="text/hyperscript-template"> anywhere on the page. The tag becomes available from then on,
so you can use it multiple times. Each instance is independent.
Component templates use hyperscript's template syntax. ${expr} interpolates an
expression. #for, #if, #else, and #end give you loops and conditionals.
<script type="text/hyperscript-template" component="user-card" _="init set ^user to attrs.data">
<h3>${^user.name}</h3>
#if ^user.admin
<span class="badge">admin</span>
#end
<ul>
#for tag in ^user.tags
<li>${tag}</li>
#end
</ul>
</script>
Interpolated values are HTML-escaped by default. Use ${unescaped expr} when you
need raw HTML (use with care).
Use DOM-scoped variables (^var) for component state. Each component instance gets
its own isolated DOM scope, so two instances do not share state.
<script type="text/hyperscript-template" component="click-counter" _="init set ^count to 0">
<button _="on click increment ^count">+</button>
<span class="label">Clicks: ${^count}</span>
</script>
<click-counter></click-counter>
<br>
<click-counter></click-counter>
Clicking one counter does not affect the other. The init block on the component
runs once per instance and is the usual place to set initial state.
Template expressions are reactive. When a ^var they read changes, the template
re-renders. Rendering uses morphing, so focus, scroll position, and event handlers
are preserved across updates.
<script type="text/hyperscript-template" component="live-greeter" _="init set ^name to 'World'">
<input type="text" _="bind ^name to my value" />
<p>Hello, ${^name}!</p>
</script>
<live-greeter></live-greeter>
Typing in the input updates ^name, which re-renders the paragraph. The input
itself keeps its focus and cursor position because morphing preserves it.
A component can read values from its own HTML attributes via the attrs object.
An attribute access is lazily is parsed as a hyperscript expression in the parent's scope.
This allows you to pass variables and objects as naturally as you would any hyperscript expression:
<script type="text/hyperscript-template" component="user-card" _="init set ^user to attrs.data">
<h3>${^user.name}</h3>
<p>${^user.email}</p>
</script>
<div _="init set $currentUser to { name: 'Carson', email: 'carson@example.com' }">
<user-card data="$currentUser"></user-card>
</div>
Here attrs.data reads the data attribute ("$currentUser"), parses it as a hyperscript
expression, and evaluates it against the enclosing scope. The component receives the
actual object, not a string.
If you want the raw attribute string instead, use the usual @attr syntax. @data
gives you "$currentUser" as a string, while attrs.data gives you the resolved value.
Note that reactive bindings work through the attrs object, so you can bind external values to
internal component state.
<slot/> elements let callers pass content into a component. The parent's markup
is projected into the template at the slot position:
<script type="text/hyperscript-template" component="my-card">
<div class="frame">
<slot/>
</div>
</script>
<my-card>
<p>This paragraph ends up inside the card.</p>
</my-card>
This paragraph ends up inside the card.
Named slots let you project multiple regions:
<script type="text/hyperscript-template" component="my-layout">
<div class="frame">
<header><slot name="title"/></header>
<main><slot/></main>
<footer><slot name="footer"/></footer>
</div>
</script>
<my-layout>
<strong slot="title">Page Title</strong>
<p>This goes into the default slot.</p>
<span slot="footer">Footer text</span>
</my-layout>
This goes into the default slot.
Footer textSlotted content is scoped to the parent, not the component. A ^var in slotted
markup resolves against the parent's scope, not the component's. This is usually
what you want: the caller's content should see the caller's variables.
A <style> block placed inside a component definition is automatically
scoped to the component's tag name. At registration time, hyperscript lifts
the <style> out of the template, wraps its contents in a CSS
@scope rule,
and inserts a single <style> element immediately after the component definition.
<script type="text/hyperscript-template" component="badge-pill">
<slot/><span class="count">${attrs.count}</span>
</script>
<badge-pill count="3">Inbox</badge-pill>
<badge-pill count="12">Drafts</badge-pill>
<p class="count">This outer <code>.count</code> is unaffected by the component's styles.</p>
This outer .count is unaffected by the component's styles.
Inside a scoped style block:
.count or [role=tab] match descendants of any
instance of the component.:scope targets the component root itself.That means you write :scope { display: block } instead of
badge-pill { display: block }, and .count instead of badge-pill .count -
the @scope wrapper does the prefixing for you.
The wrapped style block is emitted once per component definition, not
per instance, so a hundred <badge-pill> elements on the page still share
a single stylesheet.
Each component instance gets dom-scope="isolated", which stops ^var lookups at
the component boundary. This keeps component state private and avoids collisions
with outer scopes. See dom-scope in the
language docs for all the scoping options.
A component with reactive state, attrs input, and two-way binding:
<script type="text/hyperscript-template" component="filter-list"
_="init set ^items to attrs.items
set ^query to ''">
<input type="text" placeholder="Search..." _="bind ^query to my value" />
<ul>
#for item in ^items where its name contains ^query ignoring case
<li>${item.name}</li>
#else
<li>No matches</li>
#end
</ul>
</script>
<filter-list items="[{name:'Macintosh 128K'}, {name:'Macintosh Plus'}, {name:'Macintosh SE'},
{name:'Macintosh II'}, {name:'PowerBook 100'}, {name:'Quadra 700'}]"></filter-list>
Typing in the input updates ^query, which re-runs the #for comprehension,
which re-renders the list.
When there are no matches, the #else branch shows the "No matches" message.
Components are one way to package reusable logic. Hyperscript also has a
lighter-weight sibling: behavior. A behavior is a block
of hyperscript that you install onto an existing element, rather than a new
custom element you instantiate.
behavior Removable
on click
remove me
end
end
They can accept arguments:
behavior Removable(removeButton)
on click from removeButton
remove me
end
end
They are installed onto an element with install:
<div class="banner" _="install Removable(removeButton: #close-banner)">
...
</div>
For a more substantial example, see the Drag to Reorder pattern.
Behaviors and components solve two different problems:
| Behaviors | Components | |
|---|---|---|
| Definition | A block of hyperscript | A <script type="text/hyperscript-template"> with markup and _= |
| Use | install on any existing element |
Place a custom tag in your HTML |
| Markup | No, attaches to existing DOM | Yes, renders its own markup |
| State | Shares the host element's scope | Isolated DOM scope per instance |
| Reactive | No, plain hyperscript | Yes, template re-renders on change |
Use a behavior when you have a bit of logic you want to compose onto arbitrary elements.
Use a component when you want a self-contained piece of UI with its own markup and state.