Web Components #
mdlib-pure/element defines two custom elements that work in plain HTML and
in any framework:
<mdlib-markdown>: renders Markdown from itssourceattribute;<mdlib-editor>: a Markdown editor with toolbar and live preview.
They are the base of the Blazor wrapper and of the Angular wrapper.
Quick start #
Core and every plugin are included: ~20 KB gzip. KaTeX and Mermaid are not: pass loaders, and they are fetched only when a document uses them.
import { defineElements } from 'mdlib-pure/element'
import 'mdlib-pure/theme.css' // with a bundler that handles CSS imports;
import 'mdlib-pure/editor.css' // otherwise <link> them (see below)
defineElements({
loaders: {
mermaid: () => import('mermaid'),
katex: () => import('katex/contrib/auto-render').then((m) => m.default),
katexStylesheet: '/css/katex.min.css',
},
configs: { docs: { highlight: (code, lang) => myHighlighter(code, lang) } },
})
<mdlib-markdown class="md" source="# Hello" plugins="gfm math"></mdlib-markdown>
<mdlib-editor value="# Hello" plugins="all" style="block-size: 30rem"></mdlib-editor>
Attributes
| Element | Attribute | |
|---|---|---|
| both | plugins |
space-separated names or all: gfm headings math mermaid container deflist mark frontmatter |
| both | config |
name of options registered with defineElements({ configs }) |
<mdlib-markdown> |
source |
the Markdown |
<mdlib-editor> |
value |
initial (or externally set) Markdown |
<mdlib-editor> |
view |
split (default), write, preview |
<mdlib-editor> |
delay |
ms before input is dispatched (default 200) |
<mdlib-editor> |
label, name |
accessible name of the text area; form field name |
<mdlib-editor> |
disabled |
no typing or formatting, value left out of the form (as inside <fieldset disabled>) |
Options that are functions (highlight, allowUrl, sanitize) and html
can only be set through named configs, never from attributes: content cannot
switch raw HTML on.
Editor behavior. The preview renders on every keystroke, in the browser.
Listeners get a debounced input event and change on blur, with the text
in event.target.value (the text area's own events do not escape). While the
user types, an attribute set to a value the editor already sent is treated as
a late echo and ignored, so a slow server never overwrites typing; a value
set while the editor is not focused (a Reset button) is applied. The element
is form-associated: with name, its value is submitted with the form;
disabled (or an enclosing <fieldset disabled>) makes it behave like a
disabled <textarea>, while the view buttons keep working.
Toolbar: bold, italic, heading, link, lists, quote, code (Ctrl+B, Ctrl+I,
Ctrl+K). Math and diagrams load KaTeX and Mermaid only when a document uses
them; diagrams already drawn are reused while typing.
For an editor with application-specific tags and toolbar actions, see the
podcast sample. It builds a dedicated split editor with the public
parser/render APIs; custom plugins and commands are not registration options of
defineElements().
getMarkdown(plugins, config) returns the shared Markdown instance behind
an attribute combination (e.g. to parse() the editor's text).
Without a bundler #
dist/element.js is an ES module. Load it with an import map or a relative
path, then define the elements:
<link rel="stylesheet" href="/node_modules/mdlib-pure/styles/theme.css" />
<link rel="stylesheet" href="/node_modules/mdlib-pure/styles/editor.css" />
<script type="module">
import { defineElements } from '/node_modules/mdlib-pure/dist/element.js'
defineElements()
</script>
<mdlib-editor name="body" value="# Draft" plugins="gfm" style="block-size: 24rem"></mdlib-editor>
Reading the editor's value #
const editor = document.querySelector('mdlib-editor')
editor.addEventListener('input', (e) => save(e.target.value)) // debounced (delay)
editor.addEventListener('change', (e) => save(e.target.value)) // on blur
editor.value = '# Replaced' // set it from code
value works as a property or as an attribute, before or after the element
is connected, and even before defineElements() runs: frameworks that set
properties on custom elements (React 19, Vue) need nothing special. Inside a
<form>, an editor with a name submits its text like a <textarea> would.
Frameworks that rewrite the page #
Blazor enhanced navigation, Turbo and htmx may empty or morph elements they
did not create. Both elements repair themselves: the viewer renders again,
the editor puts back its toolbar and panes and keeps the typed text. Call
element.refresh() to render again by hand; every defineElements() call
refreshes the elements already in the page (e.g. loaders registered late).
Frameworks #
- Blazor: use MdlibPure.Blazor, which wraps these elements for every render mode and adds a C# AST service.
- Angular: use ngx-mdlib-pure: standalone
components with
[(value)], a form control forngModeland reactive forms, SSR with hydration, and a service for the AST. - React 19, Vue, Svelte: custom elements work as-is:
<mdlib-editor value={text} onInput={(e) => setText(e.target.value)} />in React 19 (which setsvalueas a property),:valuein Vue. Passsourceandpluginsas attributes.
Testing #
Unit tests in jsdom (Vitest, Jest) work without polyfills: the editor skips
the form value where ElementInternals.setFormValue() is missing. In jsdom,
value and the preview are updated at once; the input event comes after
delay ms (use fake timers), change on blur. KaTeX and Mermaid do not run
there: test math and diagrams in a real browser.
Security #
Raw HTML and functions cannot be enabled from attributes: html,
highlight, allowUrl and sanitize only come from named configs written
in JavaScript. URLs follow the default allowlist;
Mermaid runs with securityLevel: 'strict'. More in
Security: Web Components.