div/ref
script
-- ref().innerHTML = "Hello World";
Caution
The node reference is only available in the browser. Attempting to access a DOM node from the server will result in an error.
class=
In addition to strings, Marko supports passing arrays and objects to the class=
attribute.
<!-- String -->
<div class="a c"/>
<!-- Object -->
<div class={
a: true,
b: false,
c: true,
}/>
<!-- Array -->
<div class=[
"a",
null,
{
c: true,
},
]/>
<!-- String -->
div.a.c
<!-- Object -->
div class={
a: true,
b: false,
c: true,
}
<!-- Array -->
div class=[
"a",
null,
{
c: true,
},
]
All examples above result in the same HTML:
<div class="a c"></div>
style=
In addition to strings, Marko supports passing arrays and objects to the style=
attribute.
<!-- String -->
<div style="display:block;margin-right:16px"/>
<!-- Object -->
<div style={ display: "block", color: false, "margin-right": 16 }/>
<!-- Array -->
<div style=["display:block", null, { "margin-right": 16 }]/>
<!-- String -->
div style="display:block;margin-right:16px"
<!-- Object -->
div style={ display: "block", color: false, "margin-right": 16 }
<!-- Array -->
div style=["display:block", null, { "margin-right": 16 }]
All examples above result in the same HTML:
<div style="display:block;margin-right:16px;"></div>
Attributes on native tags that begin with on
followed by -
or a capital letter are attached as event handlers.
When the attribute starts with on-
the event name casing is preserved, otherwise the event name is all lowercased.
onDblClick
→ dblclick
on-DblClick
→ DblClick
<button onClick() {
alert("Hi!");
}>
Say Hi
</button>
// equivalent to
<button on-click() {
alert("Hi!");
}>
Say Hi
</button>
button onClick() {
alert("Hi!");
}
-- Say Hi
// equivalent to
button on-click() {
alert("Hi!");
}
-- Say Hi
Note
Event handlers are typically written using the method shorthand for readability.
The value for the attribute must be either a function or a falsy value, allowing for conditional event handlers:
<let/clicked=false/>
<button onClick=!clicked &&
(() => {
alert("First click!");
clicked = true;
})>
Click me!
</button>
let/clicked=false
button onClick=!clicked &&
(() => {
alert("First click!");
clicked = true;
})
-- Click me!
Tip
Since native events are all lowercase, the onCamelCase
event naming can help with readability of multi-word events:
<canvas onContentVisibilityAutoStateChange() {}/>
canvas onContentVisibilityAutoStateChange() {}
Some custom elements may emit non lowercase event names, in which case (pun intended 😏) you should use on-
which preserves the casing.
Caution
Even though Marko does support native HTML inline event handler attributes, it's recommended to avoid them since they're detached from Marko's reactivity system and may lead to CSP / XSS issues.
<button onclick="this.innerHTML++">
0
</button>
button onclick="this.innerHTML++" -- 0
Some native tags in Marko have additional attributes that make them controllable. These attributes end with Change
and are designed to work with the bind shorthand.
For DOM elements that maintain internal state separate from an associated attribute, Marko uses "uncontrolled" attributes by default, meaning it only sets the attribute value and not the internal value.
<input value="hello">
input value="hello"
Above is among the simplest of examples, but interestingly its behavior is different across frameworks in subtle ways.
In some frameworks, like React, this would be a "read-only" <input>
. Marko takes a different approach, allowing the input's state to be managed natively by the browser.
Adding state introduces some nuances in behavior.
<let/message="hello"/>
<input value=message>
<div>${message}</div>
<button onClick() {
message = "goodbye";
}>
Click Me
</button>
let/message="hello"
input value=message
div -- ${message}
button onClick() {
message = "goodbye";
}
-- Click Me
In this example, typing in the <input>
and then clicking the <button>
might not behave as expected. The <div>
text updates only when the button is clicked, and the <input>
doesn't reflect the new "goodbye" value.
This occurs because there are two separate states, which update independently:
<let/message>
<input>
valueTo synchronize these two states and their updates, Marko includes a special valueChange
attribute on <input>
.
<let/message="hello"/>
<input value=message valueChange() {}>
<div>${message}</div>
<button onClick() {
message = "goodbye";
}>
Click Me
</button>
let/message="hello"
input value=message valueChange() {}
div -- ${message}
button onClick() {
message = "goodbye";
}
-- Click Me
The valueChange
attribute transforms the behavior:
<input>
updates both the <input>
and the <div>
<button>
updates both the <input>
and the <div>
There is now only one state! This synchronization occurs because valueChange
:
<input>
changesmessage
variable, which then updates the value=
attributeThe valueChange
function is called whenever the <input>
would normally update, allowing a parent component to synchronize its state with the input's internal state.
<let/message="hello"/>
<input
value=message
valueChange(newMessage) {
message = newMessage;
}
>
<div>${message}</div>
<button onClick() {
message = "goodbye";
}>
Click Me
</button>
let/message="hello"
input [
value=message
valueChange(newMessage) {
message = newMessage;
}
]
div -- ${message}
button onClick() {
message = "goodbye";
}
-- Click Me
In this example, there is a single state and updates from both sources are handled. Typing in the <input>
and clicking the <button>
cause changes to both the <div>
and the <input>
itself. Everything is in sync!
Marko has a shorthand for simple reflective change handlers like this, allowing the example to be simplified to:
<let/message="Hello"/>
<input value:=message>
<div>${message}</div>
<button onClick() {
message = "Goodbye";
}>
Click Me
</button>
let/message="Hello"
input value:=message
div -- ${message}
button onClick() {
message = "Goodbye";
}
-- Click Me
With this shorthand all that is needed to go from "uncontrolled" to "controlled" for the value
attribute was to swap from value=
to value:=
.
For cases besides the most simple, manual valueChange
handlers are required.
<let/message="hello"/>
<input
value=message
valueChange(newMessage) {
message = newMessage.toLowerCase();
}
>
<div>${message}</div>
<button onClick() {
message = "goodbye";
}>
Click Me
</button>
let/message="hello"
input [
value=message
valueChange(newMessage) {
message = newMessage.toLowerCase();
}
]
div -- ${message}
button onClick() {
message = "goodbye";
}
-- Click Me
All changes to this <input>
are intercepted and manipulated. In this example, all UPPERCASE characters are automatically converted to lowercase. This pattern is useful for input masking and more - and it's built in!
// uncontrolled - The browser owns the state
<input value="hello">
// controlled - The `inputValue` tag variable owns the state
<let/inputValue="hello"/>
<input value:=inputValue>
// controlled - Modifications to `<input>` are transformed
<let/creditCardNumber="5555 5555 555"/>
<input
value=creditCardNumber
valueChange(v) {
creditCardNumber = [...v.replace(/\D/g, "").matchAll(/\d{1,4}/g)].join(" ");
}
>
// uncontrolled - The browser owns the state
input value="hello"
// controlled - The `inputValue` tag variable owns the state
let/inputValue="hello"
input value:=inputValue
// controlled - Modifications to `<input>` are transformed
let/creditCardNumber="5555 5555 555"
input [
value=creditCardNumber
valueChange(v) {
creditCardNumber = [...v.replace(/\D/g, "").matchAll(/\d{1,4}/g)].join(" ");
}
]
<input>
(valueChange=
, checkedChange=
, checkedValueChange=
)The <input>
tag has 3 change handlers, which are each related to an input type.
The value=
attribute may be controlled with valueChange=
<let/text=""/>
<input type="text" value:=text>
<input
type="text"
value=text
valueChange(value) {
text = value.toLowerCase();
}
>
let/text=""
input type="text" value:=text
input [
type="text"
value=text
valueChange(value) {
text = value.toLowerCase();
}
]
Caution
The value of <input>
is always a string, so numbers need to be casted.
<let/number=0/>
// ❌ (INCORRECT) this will set number to a string when updated
<input type="number" value:=number>
// ✅ cast the string value to a number during the change handler
<input
type="number"
value=number
valueChange(value) {
number = +value;
}
>
let/number=0
// ❌ (INCORRECT) this will set number to a string when updated
input type="number" value:=number
// ✅ cast the string value to a number during the change handler
input [
type="number"
value=number
valueChange(value) {
number = +value;
}
]
The checked=
attribute may be controlled with checkedChange=
<let/checked=false/>
<input type="checkbox" checked:=checked>
<input
type="checkbox"
checked=checked
checkedChange(value) {
checked = value;
}
>
let/checked=false
input type="checkbox" checked:=checked
input [
type="checkbox"
checked=checked
checkedChange(value) {
checked = value;
}
]
The added checkedValue=
attribute also has a change handler.
<let/checked="foo"/>
<input type="radio" value="foo" checkedValue:=checked>
let/checked="foo"
input type="radio" value="foo" checkedValue:=checked
<select>
(valueChange=
)Traditionally, the value of a <select>
is controlled via the selected=
attribute in its <option>
tags. Marko adds an additional way to control the <select>
using a new value=
attribute, which is also controllable with a Change
handler.
<textarea>
(valueChange=
)The <textarea>
tag has a change handler for Marko's added value=
attribute.
<let/text=""/>
<textarea value:=text/>
let/text=""
textarea value:=text
<details>
(openChange=
)The <details>
tag has a change handler for its open=
attribute.
<let/open=false/>
<details open:=open/>
<button onClick() {
open = false;
}>
Collapse
</button>
let/open=false
details open:=open
button onClick() {
open = false;
}
-- Collapse
<dialog>
(openChange=
)The <dialog>
tag has a change handler for its open=
attribute.
<let/open=false/>
<dialog open:=open>
Hello!
</dialog>
<button onClick() {
open = !open;
}>
Toggle
</button>
let/open=false
dialog open:=open -- Hello!
button onClick() {
open = !open;
}
-- Toggle
[!Warning] The
open
attribute of the<dialog>
tag can be used to control a non-modal dialog. However if you need a modal dialog, you should use the.showModal()
method directly. Calling this method will not causeopenChange
to fire as the HTML<dialog>
only fires an event onclose
.
Helpful? You can thank these awesome people! You can also edit this doc if you see any issues or want to improve it.