A portable and modular virtual DOM library for Haxe, with inline markup support.
Started as port of snabbdom, rewritten to Haxe and extended to provide additional features.
Current focus is JS
target in the browser to manipulate HTML DOM elements, but the library is designed to work with any hierarchy of elements, even on native Haxe targets such as C++
, as long as you provide a corresponding Backend
implementation. In other words, this library is not specific to HTML5 or the Web browser and could be used in other contexts!
- Initial port of snabbdom core
- HTML backend
- Additional modules (in progress)
- Haxe Inline XML (JSX-like feature)
- Non-HTML backends?
Wisdom uses it's own markup syntax, surrounted with '<>
and '
tokens. That is, you write your markup inside a single-quoted string that starts with <>
.
By default, this markup won't be syntax highlighted in VSCode, but you can install the haxe-wisdom VSCode extension to support it.
This guide covers the template markup syntax that can be used within '<>' and '
tokens. The markup provides a powerful way to create structured templates with control flow, components, and dynamic content.
Templates are wrapped in single quotes with <>
marker:
'<><tag>hello</tag>'
For convenience, the '<>
and '
surrounding delimiters will be omitted in the next code examples.
Custom components start with an uppercase letter:
<UserProfile user=$currentUser />
<Button type="primary" onclick=$handleClick>
Click me
</Button>
Regular elements (HTML elements when using the html backend) start with lowercase letters:
<div class="container">
<h1>$title</h1>
<p class="content">$content</p>
</div>
Wisdom markup is compatible with Haxe's single-quoted string interpolation.
Use $
for simple variable interpolation:
<div>Hello, $name!</div>
Use ${}
for expressions:
<div>Total: ${price * quantity}</div>
<div>Full name: ${firstName + " " + lastName}</div>
To use literal $
characters, double them:
<div>Price: $$99.99</div>
You can use three types of comments in your markup:
// Single line comment
/*
Multi-line
comment
*/
<!-- XML-style comment -->
Basic if/else conditional rendering:
<if ${user.isLoggedIn}>
Welcome back, ${user.name}!
<else>
Please log in
</if>
With elseif:
<if ${score >= 90}>
Grade: A
<elseif ${score >= 80}>
Grade: B
<elseif ${score >= 70}>
Grade: C
<else>
Grade: F
</if>
Iterate over arrays or iterables:
<foreach $cities ${(i:Int, city:String) -> '<>
<li>$i. $city</li>
'} />
<switch $status>
<case "active">User is active</case>
<case "pending">User is pending approval</case>
<case "suspended">Account suspended</case>
<default>
Unknown status
</default>
</switch>
Case values can be any haxe primitive like 123
, "hello"
, 'world'
, true
, false
, 0xFF00FF
or interpolated values ($value
, ${...}
).
Alternative haxe-style default case is also supported:
<switch $mood>
<case "happy"> :) </case>
<case "sad"> :( </case>
<case _> :| </case>
</switch>
Attributes can use expressions:
<div class=${getClassName()}>
<input type="text" value=${formData.value} />
<img src=${"/images/" + imagePath} />
</div>
Use if/unless for conditional attributes:
<div if=${showElement}>
Conditional content
</div>
<button class="btn" unless=$submitForbidden>
Submit
</button>
Use the key attribute for optimizing list rendering:
<ul class="cities">
<foreach $cities ${(i:Int, city:City) -> '<>
<li key=${city.id}>
${city.name}
</li>
'} />
</ul>
# Project setup
-cp src
--main Main
--js html/app.js
# Use wisdom library
--library wisdom
# Use tracker library (optional)
--library tracker
# Use HTML backend
-D wisdom_html
import wisdom.HtmlBackend;
import wisdom.Wisdom;
import wisdom.X;
import wisdom.modules.AttributesModule;
import wisdom.modules.ClassModule;
import wisdom.modules.ListenersModule;
import wisdom.modules.PropsModule;
import wisdom.modules.StyleModule;
Your class must implement
the wisdom.X
interface in order to support '<> ... '
markup syntax.
class MyApp implements X {
...
}
// Initialize Wisdom with required modules and HTML backend
var wisdom = new Wisdom([
ClassModule.module(),
StyleModule.module(),
PropsModule.module(),
AttributesModule.module(),
ListenersModule.module()
],
new HtmlBackend()
);
Let's assume our HTML file used to load our code has a <div id="container"></div>
.
We can use patch()
to bind our virtual dom to it:
// Initial binding with the actual #container HTML element
// (keep a reference to the vdom with the `container` variable)
var container = wisdom.patch(
document.getElementById('container'),
'<>
<div class="my-wisdom-words">
Some words of wisdom
</div>
'
);
// Patching again: use the previous `container` reference,
// then replace it with the patched one
container = wisdom.patch(
container,
'<>
<div class="my-wisdom-words">
More words of wisdom...
</div>
'
);
patch()
can be called again and again with new content to update the nodes as needed. The changes will be reflected to the actual HTML page (when using html backend).
Components are special functions that can be invoked by your wisdom markup:
@x function Hello(name:String) '<>
<div class="hello">
Hello $name!
</div>
';
We now have a component that can take a single parameter name
. Here is how we can use it in markup:
<div class="my-markup">
<Hello name="Jeremy" />
</div>
Components can handle child nodes with the special children
parameter
@x function HelloWithChildren(name:String, children:Array<wisdom.VNode>) '<>
<div class="hello">
Hello $name!
<div class="some-children">
$children
</div>
</div>
';
Usage in markup:
<div class="my-markup">
<HelloWithChildren name="John">
<p>Some more content</p>
<p>And even <strong>more content!</strong></p>
</HelloWithChildren>
</div>
Reactivity with tracker library
To enable reactivity, you'l need to have the tracker
haxe library enabled in your hxml:
# Use tracker library
--library tracker
When tracker is available, you can create reactive virtual dom trees using a single reactive()
call instead of consecutive patch()
calls:
wisdom.reactive(
document.getElementById('container'),
'<>
<div class="reactive-wisdom">
The best reaction is wisdom.
<HelloAndCount name="Jane" />
</div>
'
);
When using wisdom.reactive()
, the virtual dom will be patched automatically any time an observable value that it depends on changes.
You might have noticed we are invoking another component named HelloAndCount
. Here is how it looks like:
/**
* A reactive component with its own state
*/
@x function HelloAndCount(
@state count:Int = 0,
name:String
) {
function increment() {
count++;
}
return '<>
<div onclick=$increment>
Hello $name<br />
Count: ${count}
</div>
';
}
This time, the component provides its own state: any argument prefixed with the @state
meta will become an observable variable of the component's state. If any of those values are changing, the component will be rendered again and the virtual dom automatically patched in reaction.
In this example, clicking on the element will trigger an increment of the count
variable an make the component render itself again, because it is used within a wisdom.reactive()
call.
Markup roots (reactive component roots, markup roots used with wisdom.patch()
) should be wrapped into a single node.
Valid:
<>
<div>
<p>node 1</p>
<p>node 2</p>
</div>
Invalid:
<>
<p>root node 1</p>
<p>root node 2</p>
This library has been ported, adapted to Haxe and extended by Jérémy Faivre from the work of Simon Friis Vindum who created the very well designed snabbdom ⚡️ library.