A small app

Start from one empty HTML file and build a counter, a dropdown, and a tag search, then see how to hand values down between scopes. By the end you will have used almost all of hyper-scope's core.

Create an empty file somewhere on your computer, with a name like i-love-hyper-scope.html. Paste in the content below and open it in a browser.

If you see "I love hyper-scope" on the screen, you are ready. Put the scripts in head. Tools that swap the page may run scripts inside body again, and there must be only one runtime.

Build a counter

This is the smallest example. It has all three basics: holding state, listening for events, and showing the state on screen.

Declare state

The element with h-scope and everything inside it form one scope. The object you write as the value becomes that scope's state. Here it is just count.

→ More on h-scope

Listen for events

@click is short for h-on:click. Each click runs the expression after it, count++. Inside an expression you read and write scope values by their names.

→ More on h-on

React to changes

h-text fills the element's text with the result of the expression. When count changes, hyper-scope fills it again on its own. You never have to ask for a re-render.

→ More on h-text

Next is a dropdown that opens on click and closes when you click outside it.

Show and hide

h-show shows the element only while the expression is true. The element stays in the DOM and only its display changes. To add and remove it entirely, use <template h-if>.

→ More on h-show

Make your own attribute

h-outside is not a built-in attribute. This site registers it with a plugin. You can make the attributes you need in a few lines like this. The function you return is called when the element goes away, and cleans up.

→ More on directive

Build a search input

This example is a little longer. It searches HTML tags by name and filters the list by what you type.

Put values in text

To put a value into text, as in {{ tag.name }}, set h-text-inline on the element that holds that text. It reads only the text directly under the element. To read the text of inner elements too, write a selector as the value. The example above uses <li h-text-inline="code">, so it reads the text of <li> and the text of the <code> inside it. Without it, {{ }} stays as plain text. Server-rendered pages contain text written by users, so it reads only in the elements you mark. Attributes such as h-text work without h-text-inline. h-text-inline →

Write it on several lines

The value of h-scope is a plain JavaScript object, so you can split it across lines. If it grows longer, move it into a <script type="scope"> directly under the scope. Whatever you return there is merged into the scope.

Bind to inputs

h-model keeps an input and a value in sync. Typing changes search, and changing search changes the input. If the server rendered a value, that becomes the initial value.

→ More on h-model

Computed values

get found() is a JavaScript getter. You read it like a value, not call it like a function. When tags or search, which it reads, change, every place that uses this value changes too.

Instead of writing a getter in the object, you can also use an attribute. h-computed:name="expression" creates a computed value that the element and its contents can read. It is read-only. Attribute names become lowercase, so write total-price and read totalPrice.

→ More on h-computed

Loop

h-for renders its contents once for each item in a list. With :key, the same item reuses its element when the list changes.

→ More on h-for

Hand values down to descendants

When you put a scope inside a scope, the inner one cannot see the outer one's values. h-scope is a boundary. If the value is just one level out, take it with h-inherit="name". But the inner scope has to know the outer scope's variable name, so this breaks easily as the distance grows.

h-provide and h-inject put a label between them. The ancestor says "I provide this under this label", and the descendant says "I receive whatever is under this label". Neither needs to know the other's variable names.

Provide

h-provide:theme="theme" hands a value down under the label theme, from that element inward. The expression is evaluated where the element sits. So even if the same element has h-scope, the provide comes before that scope, and that scope can receive it right away. You can also put it on an element without h-scope. Descendants any number of levels down can receive it.

When a scope provides its own value from a script, use $provide('label', value) (in a function scope, the provide tool).

Inject

h-inject="theme" walks up the DOM, finds what the nearest ancestor provided, and reads it under that name. Separate several with spaces: h-inject="theme cart". An injected name is read-only, so you cannot assign another value to it (doing so logs a console error). You can change the inside of the received object (theme.color). If the value itself must be replaced, ask the providing side with $emit.

To receive under a different name, or to set a value for when nobody provides one, use $inject in an expression: h-scope="{ look: $inject('theme', { color: 'gray' }) }".

Changes flow both ways

What goes down is the same object, not a copy. When the ancestor changes theme.color, the receiver's screen changes. When a descendant changes the received theme.color, the ancestor's side changes. So do not provide a number or a string directly. Wrap it in an object. A bare value only passes over once, as it was at that moment.

When to use it

For a value that every page shares, hand it down to the whole document with the plugin's provide. It survives across visits. Use h-provide when the value belongs "only inside this region". Two regions on a page can provide the same label separately, and each descendant receives the one from its own ancestor.

→ More on h-provide · h-inject

Recap

If you made it this far, you have used these attributes.

The rest is in Attributes, Properties, and Functions. Form validation and Turbo are covered in Extensions.