Try It
Every example on this page is a real MoonBit program. Edit an example. Then press Ctrl/Cmd + Enter, or press the Run button. The MoonBit compiler runs in your browser. It links your program against the tutuca-mb library and mounts the result. The first run loads the compiler. This takes some time. Later examples compile in a few seconds.
Each editor has three tabs. Component shows the
MoonBit code of the example. View shows the
.html file with its views. Generated
shows what tutuca gen makes from that view: the
compiled view tree and a typed message enum, compiled as part of the
same package. You can edit the View tab. The Generated tab then
changes also. Add an @on handler in the view and answer
it in the same file's logic: section. If you do not
answer it there, the next run fails until the component's
match handles it.
Hello, Counter
Toggle — no update code
The schema declares a boolean field. The generator makes a
$toggleOpen mutator for it. @show reads the
field. No hand-written update code is necessary.
A card — the same file, with no MoonBit toolchain
The examples above compile MoonBit in your browser, and that costs a 5.5 MB payload. This example does not. A card is the same view file: schema, script, and templates. The page compiles the card itself — into a WebAssembly module it then instantiates — and that compiler is part of the runtime already on this page. Thus the card compiles and mounts in milliseconds. Edit the card. It mounts again while you type. There is no run step.
You lose the parts that need the MoonBit compiler. A card cannot
name a MoonBit value. Thus there are no imports and no closures, and
a handler gen would refuse has nowhere to go.
Every handler must use the language in the
logic: section. You keep everything else, including
the checks. The diagnostics below the preview are the same ones
tutuca gen reports. They have the same line and
column numbers.
The card carries no CSS. Its views name the
margaui
component classes — card, btn,
input, badge — with Tailwind utility classes
beside them. <mb-card margaui> compiles the classes
that this card used. The compilation happens in your browser.
The card tutorial builds a card step by step, with live examples at each step. It covers the state block and its generated code, the handler language, derived values, loops, messages, and the limits of a card. The card playground uses the same compiler and has more panels: the state tree, the dispatch log, the card's test scenes, the compiled module itself, and a structured editor that shows one block of the file at a time.
Examples
These examples are ports of the examples on the Tutuca landing page. Each example is editable. Change the code and run it again. For the full set, compiled ahead of time, see the storybook gallery.
Basics
To-Do List
This example shows a list of child Item components. You
can edit each item. <x render-it> renders the
current instance of the loop. .items.removeAt @key
deletes an item by key — a generated mutator, reached by writing to
the field, with no handler between.
Two-way Text Binding
:value reads a field.
@on.input=".name = e.value" writes it back.
@text shows the new value immediately.
Live Markdown, Sanitized
@setinnermd takes a markdown string. It replaces the
children of the element with the nodes from the parsed string:
headings, lists, tables, GFM task lists, and footnotes. Type in the
left pane. The right pane then renders again.
Scroll the source to the marked rule. The text after the rule is
attack markup. None of it runs. The sanitizer removes SVG
<script>, <iframe>,
<base>, on* handlers, and
javascript: URLs. SVG <script> is not
the same element as the HTML <script>. Its name
includes its namespace. The sanitizer judges every node in the tree
with the same rules. Thus there is no allow-list only for markdown.
If the sanitizer refuses a URL, it removes the attribute. It keeps the
element. The link keeps its text and loses its target.
There are two failure modes. Both are safe. Markup that the parser
knows becomes an element. The sanitizer judges it and removes it. A
line in the report log records this. Markup that the parser does not
know — a bare <script> is one example — never
becomes an element. It stays on the page as visible text. Text
reaches the DOM through createTextNode. Thus the browser
never parses text as markup.
The markup never becomes a string. The payload is parsed one time and
built into vdom nodes. The code never assigns
innerHTML. This removes the second parse that
mutation-XSS needs. See
docs/sanitizer.md.
HTML and SVG, Sanitized
These two directives are about markup itself, not a source language.
@setinnerhtml and @setinnersvg take a
string that is already markup: a CMS body, a server-rendered
fragment, or a chart that another program drew. They replace the
children of the element with the nodes from the parsed string.
Developers usually reach for @dangerouslysetinnerhtml to
solve this case.
You do not give these two directives a permission. This is not a
shortcut. The dangerous directive needs a permission because its
fallback is innerHTML. If you mount an app without the
filter, the payload goes to the browser without checks. These two
directives fail closed: with no filter, they show an empty element.
Thus there is no unchecked path for a host to permit.
Edit either pane and look at the rule. Below the rule:
<script> in both namespaces,
<iframe>, <base>, SVG
<use>, on* handlers, and
javascript: URLs, with or without entity obfuscation.
Two cases are worth a look in devtools. The sanitizer refuses SVG
<animate> as an element, not because of anything
in its value. SMIL writes href in the browser after all
checks finish. Thus the href that the URL rule checked
is not the one that would navigate. Also, the sanitizer removes the
style attribute. This is the one rule these two
directives add and @dangerouslysetinnerhtml does not
have. url(…) requests a resource from an origin that the
payload chose. Nothing here parses CSS. Thus it cannot tell a
background image from a beacon.
The payload in the SVG pane has no <svg> root of
its own. This shows the difference between the two directives.
@setinnersvg parses in SVG context. A bare fragment
works. The payload cannot leave the namespace it was promised.
Interaction Patterns
Tree Navigation
Click a folder to open or close it. Click a file to select it.
Drag and Drop
Drag an entry to change its position. Type in the input to filter the list.
Filter and Paginate
The search box narrows the list. The pager shows one part of the
matches at a time. You can edit or delete a row on page 2 of a
filtered view, and the correct row changes.
@loop-with returns the original keys of the matching
rows. Thus filtering and paging do not change the identity of a row.
Web Component & Custom Event
tutuca can host a third-party web component and react to its
CustomEvent
(@on.emoji-click="onEmojiClick e.value"). This is a core
tutuca feature. The example needs an external custom element on the
page. See it in the
storybook gallery.
Full Applications
JSON Editor
This is a recursive editor. Every JSON node is a component. Each component renders its children.
Personal Site
This is a list of entries. You can filter by category and role, sort
the entries, and solo or unsolo an entry with alt-click. After mount,
the app raises an ask(…, ~route: lex) to load the data. A handler
on the module answers the intent.
Visual WebAssembly
This is a larger app. It is an editor for WebAssembly modules. An instruction table builds its approximately 19 views at run time. Thus this is the one example that cannot compile its views ahead of time. See it in the storybook gallery.
Composability
All Together Now
Every example exports its components independently. No example knows that another example composes it. This root imports several examples. It renders each one in its own tab. The glue code is small.
Storybook
A storybook shows components in several states, with a sidebar, a
live preview, and a Lint panel for each story. tutuca-mb ships a
compiled storybook. See the full storybook.
You can also serve it with
./cli/tutuca storybook.
Testing Components
tutuca-mb components are plain values. Thus you can unit-test their
methods, input handlers, and loop logic. Tests run in the toolchain,
not in this browser sandbox. Use moon test for the
library. Use the Lint panel in the
storybook for checks on single components.
Get Started
tutuca-mb is the MoonBit port of Tutuca. You write a component as a plain MoonBit value. A component has a name, an HTML-like view string, and optional fields, methods, and event handlers. You mount components with a host adapter. The same components compile to three backends:
- js — the browser default (these playgrounds use it)
- wasm-gc — the same components as WebAssembly
- native — the command-line tooling (lint, render, storybook)
This folder is a complete build. The browser demos need a static file
server. ES modules do not load from file:// URLs:
python3 -m http.server 8000
Then open http://localhost:8000/ in your browser:
- Full playground — a larger editor with inspectors for state and activity
- Storybook — the compiled gallery of every ported docs example, with a Lint panel for each story
- Counter demo (js) · (WebAssembly)
- Card playground — write a component in one file and watch it compile to a WebAssembly module and mount, in the page, with no toolchain. The module is one file that carries its own manifest; the format and the host are in the published package (see docs/dynamic-components.md).
./cli/tutuca --help— the native command-line tool
What's Next
- Full playground — see the state and the activity log while you interact
- Storybook — browse every component in several states
- Source code — this MoonBit port (the original JS framework)