MDL has two small behavior paths:
MDL behavior attributes -> htmx hx-* output
MDL event attributes -> exported JavaScript or TypeScript handlers
Enter fullscreen mode Exit fullscreen mode
That means the source can stay compact:
form@api(post /api/profile)@result(profileResult)@swap(replace)@trigger(submit)@loading(profileBusy):
.input@id(profileName)@name(name)@required
.btn-primary@type(submit)(Save)
status@id(profileBusy):
Saving.
Enter fullscreen mode Exit fullscreen mode
With the htmx adapter enabled, MDL emits validated htmx attributes:
<form
class="mdl-form"
hx-post="/api/profile"
hx-target="#profileResult"
hx-swap="outerHTML"
hx-trigger="submit"
hx-indicator="#profileBusy">
</form>
Enter fullscreen mode Exit fullscreen mode
And with configured JavaScript or TypeScript modules, event attributes call
exported functions by name:
card@id(plannerPanel)@mount(mountPlanner):
.btn-primary@click(addTask)(Add task)
Enter fullscreen mode Exit fullscreen mode
export function mountPlanner(element: HTMLElement) {
element.dataset.ready = "true";
}
export function addTask(event: MouseEvent) {
event.preventDefault();
}
Enter fullscreen mode Exit fullscreen mode
Enable the htmx adapter
Configure htmx as the behavior adapter in mdl.json:
{
"head_scripts": [
"https://cdn.jsdelivr.net/npm/[email protected]/dist/htmx.min.js"
],
"behavior": {
"adapter": "htmx",
"version": "2"
}
}
Enter fullscreen mode Exit fullscreen mode
MDL source should use MDL behavior attributes such as @api(...),
@result(...), and @swap(...). Raw @hx-post(...), @hx-target(...), and
other raw htmx attributes are not emitted.
htmx behavior attribute map
These are the MDL behavior attributes supported by the htmx adapter.
MDL attribute htmx output Example@api(get /api/search)
hx-get="/api/search"
get, post, put, patch, delete
@result(searchResults)
hx-target="#searchResults"
Targets this, id, #id, or .class
@swap(inner)
hx-swap="innerHTML"
See swap values below
@trigger(input)
hx-trigger="input"
Uses a known trigger event
@loading(searchBusy)
hx-indicator="#searchBusy"
Also accepts this or #id
@confirm(Save changes?)
hx-confirm="Save changes?"
Plain text prompt, max 200 chars
@include(csrfToken)
hx-include="#csrfToken"
Also accepts this or #id
@select(resultFragment)
hx-select="#resultFragment"
Selects one response fragment
@select-oob(toast)
hx-select-oob="#toast"
Selects one out-of-band fragment
@swap-oob(true)
hx-swap-oob="true"
Also accepts known swap values
@push(false)
hx-push-url="false"
true, false, or safe root path
@replace(/profile)
hx-replace-url="/profile"
true, false, or safe root path
@history(false)
hx-history="false"
true or false
@history-elt(true)
hx-history-elt="true"
Emits only for true
@boost(true)
hx-boost="true"
Only on safe local links/forms
@disabled(saveButton)
hx-disabled-elt="#saveButton"
Also accepts this or #id
@disinherit(target swap)
hx-disinherit="hx-target hx-swap"
Allows known htmx attrs or *
@encoding(multipart)
hx-encoding="multipart/form-data"
Also supports urlencoded form encoding
@inherit(trigger)
hx-inherit="hx-trigger"
Allows known htmx attrs
@params(name email)
hx-params="name,email"
Explicit names or none
@preserve(true)
hx-preserve="true"
Requires a safe @id(...)
@prompt(Security code?)
hx-prompt="Security code?"
Plain text prompt, max 200 chars
@request(timeout=5000)
hx-request="{"timeout":5000}"
Timeout from 1 to 60000 ms
@sync(profileForm:queue-last)
hx-sync="#profileForm:queue last"
Coordinates concurrent requests
@validate(true)
hx-validate="true"
true or false
Supported @api(...) methods
get
post
put
patch
delete
Enter fullscreen mode Exit fullscreen mode
@api(...) only emits same-origin-safe paths. External API URLs are not
emitted from this attribute.
Supported @swap(...) values
MDL gives short names to the htmx swap values:
MDL htmxinner
innerHTML
inner-html
innerHTML
innerhtml
innerHTML
replace
outerHTML
outer
outerHTML
outer-html
outerHTML
outerhtml
outerHTML
append
beforeend
prepend
afterbegin
before
beforebegin
after
afterend
none
none
@swap-oob(...) supports true plus the same known swap values. false is not
emitted for @swap-oob(...).
Supported @trigger(...) values
submit
click
change
input
load
revealed
intersect
keyup
keydown
focus
blur
mouseenter
mouseleave
Enter fullscreen mode Exit fullscreen mode
MDL intentionally does not emit htmx trigger filters or JavaScript-like trigger
expressions from @trigger(...).
Supported @encoding(...) values
multipart
multipart/form-data
form
urlencoded
application/x-www-form-urlencoded
Enter fullscreen mode Exit fullscreen mode
These normalize to either multipart/form-data or
application/x-www-form-urlencoded.
Supported @sync(...) strategies
drop
abort
replace
queue
queue-first
queue-last
queue-all
Enter fullscreen mode Exit fullscreen mode
You can pass the strategy by itself:
form@sync(queue-last):
Enter fullscreen mode Exit fullscreen mode
Or scope it to one explicit target:
form@sync(profileForm:queue-last):
Enter fullscreen mode Exit fullscreen mode
Supported @inherit(...) and @disinherit(...) names
These attributes accept MDL names or htmx names. MDL normalizes them to htmx
attribute names.
target / result -> hx-target
swap -> hx-swap
trigger -> hx-trigger
indicator / loading -> hx-indicator
confirm -> hx-confirm
include -> hx-include
select -> hx-select
select-oob -> hx-select-oob
swap-oob -> hx-swap-oob
push / push-url -> hx-push-url
replace / replace-url -> hx-replace-url
history -> hx-history
history-elt -> hx-history-elt
boost -> hx-boost
disabled / disabled-elt -> hx-disabled-elt
disinherit -> hx-disinherit
encoding -> hx-encoding
inherit -> hx-inherit
params -> hx-params
preserve -> hx-preserve
prompt -> hx-prompt
request -> hx-request
sync -> hx-sync
validate -> hx-validate
get -> hx-get
post -> hx-post
put -> hx-put
patch -> hx-patch
delete -> hx-delete
Enter fullscreen mode Exit fullscreen mode
@disinherit(*) is also supported. @inherit(*) is not.
Configure TypeScript handlers
MDL scripts can point at JavaScript or TypeScript module entries:
{
"scripts": [
"scripts/app.ts"
]
}
Enter fullscreen mode Exit fullscreen mode
During mdl build, MDL compiles local .ts module scripts to browser-ready
JavaScript:
scripts/app.ts -> dist/scripts/app.js
Enter fullscreen mode Exit fullscreen mode
The generated HTML imports the JavaScript URL:
<script type="module">
import * as mdlModule0 from "./scripts/app.js";
</script>
Enter fullscreen mode Exit fullscreen mode
During mdl serve, the browser still asks for ./scripts/app.js, and the dev
server serves JavaScript compiled from scripts/app.ts.
Configured TypeScript entries can import local TypeScript trees:
scripts/app.ts
scripts/state/store.ts
scripts/dom/render.ts
scripts/utils/format.ts
Enter fullscreen mode Exit fullscreen mode
MDL preserves the folder tree in output:
dist/scripts/app.js
dist/scripts/state/store.js
dist/scripts/dom/render.js
dist/scripts/utils/format.js
Enter fullscreen mode Exit fullscreen mode
Local TypeScript imports may use .ts, omit the extension, or use the
browser-facing .js extension:
import { snapshot } from "./state/store.ts";
import { renderTaskList } from "./dom/render.js";
import { formatCount } from "./utils/format";
import type { Task } from "./state/model.ts";
Enter fullscreen mode Exit fullscreen mode
MDL does not create package.json, node_modules, or tsconfig.json for this.
It is transpile-only behavior support, not a bundler or project type checker.
Inline TypeScript blocks are not supported yet:
script ts:
// not supported yet
Enter fullscreen mode Exit fullscreen mode
Use inline JavaScript:
script js:
document.body.dataset.ready = "true"
Enter fullscreen mode Exit fullscreen mode
Or an external configured .ts module.
TypeScript event attributes
Event attributes compile to data-mdl-on-* markers, and MDL’s generated module
runtime calls exported handlers by name.
form@submit(createTaskFromForm):
.input@input(updateDraftPreview)
.btn-primary@click(saveTask)(Save)
canvas@id(scene)@mount(drawScene):
Enter fullscreen mode Exit fullscreen mode
<form class="mdl-form" data-mdl-on-submit="createTaskFromForm">
<input class="mdl-input" data-mdl-on-input="updateDraftPreview">
<button class="mdl-btn-primary" data-mdl-on-click="saveTask">Save</button>
</form>
<canvas class="mdl-canvas" id="scene" data-mdl-on-mount="drawScene"></canvas>
Enter fullscreen mode Exit fullscreen mode
@mount(handler) is not a browser event. It runs once after configured modules
are imported and receives the mounted element.
All other handlers receive the browser event.
Complete event alias list
These event attributes can be handled from JavaScript or TypeScript modules:
@click @dblclick @auxclick @contextmenu
@command
@submit @reset @formdata @beforeinput
@input @change @invalid @search
@select
@compositionstart @compositionupdate @compositionend
@focus @blur @focusin @focusout
@keydown @keypress @keyup
@mousedown @mouseup @mousemove @mouseover
@mouseout @mouseenter @mouseleave
@pointerdown @pointerup @pointermove @pointerover
@pointerout @pointerenter @pointerleave @pointercancel
@pointerrawupdate @gotpointercapture @lostpointercapture
@touchstart @touchmove @touchend @touchcancel
@wheel @scroll @scrollend
@load @error @abort @resize
@canplay @canplaythrough @play @playing
@pause @ended @durationchange @emptied
@loadeddata @loadedmetadata @loadstart @progress
@ratechange @seeked @seeking @stalled
@suspend @timeupdate @volumechange @waiting
@dragstart @drag @dragenter @dragover
@dragleave @drop @dragend
@copy @cut @paste
@cuechange @slotchange
@toggle @beforetoggle @beforematch @close
@cancel @contextlost @contextrestored
@securitypolicyviolation
@fullscreenchange @fullscreenerror
@animationstart @animationiteration @animationend @animationcancel
@transitionrun @transitionstart @transitioncancel @transitionend
Enter fullscreen mode Exit fullscreen mode
TypeScript handler example
form@id(taskForm)@submit(createTaskFromForm):
.input@id(taskTitle)@name(title)@type(text)@required@input(updateDraftPreview)
.btn-primary@type(submit)(Add task)
card@id(plannerPanel)@mount(mountPlanner):
status@id(boardStatus)@aria-live(polite):
Waiting for TypeScript.
Enter fullscreen mode Exit fullscreen mode
export function mountPlanner(element: HTMLElement) {
element.dataset.ready = "true";
}
export function createTaskFromForm(event: SubmitEvent) {
event.preventDefault();
const form = event.currentTarget;
if (!(form instanceof HTMLFormElement)) return;
const data = new FormData(form);
const title = String(data.get("title") ?? "").trim();
if (!title) return;
// Update app state, then render.
}
export function updateDraftPreview(event: Event) {
const input = event.currentTarget;
if (!(input instanceof HTMLInputElement)) return;
const preview = document.querySelector("#draftPreview");
if (preview) preview.textContent = input.value;
}
Enter fullscreen mode Exit fullscreen mode
General MDL attribute rules
Normal HTML attributes are still direct:
.input@id(email)@name(email)@type(email)@required@autocomplete(email)
Enter fullscreen mode Exit fullscreen mode
That emits:
<input class="mdl-input" id="email" name="email" type="email" required autocomplete="email">
Enter fullscreen mode Exit fullscreen mode
The core rules are:
-
@attr(value)emitsattr="value". -
@attremits a boolean attribute. -
@class(...)appends author classes after the generatedmdl-*class. - Raw browser event attributes such as
@onclick(...)are not emitted. - Raw htmx attributes such as
@hx-post(...)are not emitted. - Inline
@style(...)and iframe@srcdoc(...)are not emitted. - URL attributes are filtered to safe URL shapes.
The reason is boring in the best way: MDL keeps authoring short, but the output
should remain inspectable and hard to accidentally turn into a script sink.
Try the examples
The htmx example app:
node examples/htmx/server.mjs
target/debug/mdl serve examples/htmx
Enter fullscreen mode Exit fullscreen mode
Open:
http://127.0.0.1:4010
Enter fullscreen mode Exit fullscreen mode
The TypeScript example:
cd examples/typescript
../../bin/mdl serve
Enter fullscreen mode Exit fullscreen mode
Open:
http://127.0.0.1:3996
Enter fullscreen mode Exit fullscreen mode
Links
- Website: https://getmdl.site
- Repository: https://github.com/tosiiko/mdl-code
- npm package: https://www.npmjs.com/package/@tosiiko/mdl
- htmx example:
examples/htmx - TypeScript example:
examples/typescript - Attribute spec:
docs/spec/ATTRIBUTES.md - Script spec:
docs/spec/SCRIPTS.md
MDL is still early, but the shape is already useful: htmx gets validated
hypermedia attributes, TypeScript gets typed behavior modules, and the browser
still receives plain HTML, CSS, and JavaScript.
답글 남기기