Repository navigation
Interpolate placeholders in bound props #489
Description
Description
A : prop can read a value from the render data, but it cannot mix fixed text with that value. So a prop such as href="/docs/<slug>/overview" or title="Item <id>" cannot be built from data or frontmatter.
Note
This ports over net-new behavior being introduced in the @nuxtjs/mdc module via nuxt-content/mdc#516
What works today
A : prop binds to one whole data path:
:a[Read more]{:href="data.slug"}With data = { slug: 'getting-started' }, this renders <a href="getting-started">Read more</a>.
The binding plugin builds text from the same data with {{ }}:
Hello {{ data.name }}What does not work
The examples below use data = { slug: 'getting-started', id: 7 }.
-
An inline prop:
:a[Read more]{:href="/docs/{{ data.slug }}/overview"}
Expected:
<a href="/docs/getting-started/overview">Read more</a>Actual: the placeholder stays as literal text.
<a href="/docs/{{ data.slug }}/overview">Read more</a>
-
A YAML props block:
::card --- :title: Item {{ data.id }} --- Body ::
Expected:
<card title="Item 7">Actual:
<card title="Item {{ data.id }}"> -
A default value:
:span{:title="Version {{ data.version || 1.0 }}"}
Expected:
<span title="Version 1.0"></span>, becausedata.versionis not set.Actual:
<span title="Version {{ data.version || 1.0 }}"></span>
The Vue, React and Svelte renderers omit the prop instead. For example 1 they render <a>Read more</a>.
How to reproduce
import { parseMarkdown } from 'comark'
import { renderHtmlFromDocument } from '@comark/html'
const render = async (md: string, data: Record<string, unknown>) =>
renderHtmlFromDocument(await parseMarkdown(md), { data })
const data = { slug: 'getting-started', id: 7 }
await render(':a[Read more]{:href="/docs/{{ data.slug }}/overview"}', data)
// <a href="/docs/{{ data.slug }}/overview">Read more</a>
await render('::card\n---\n:title: Item {{ data.id }}\n---\nBody\n::', data)
// <card title="Item {{ data.id }}">
await render(':span{:title="Version {{ data.version || 1.0 }}"}', data)
// <span title="Version {{ data.version || 1.0 }}"></span>Tested on main at 9bcda64 (comark, @comark/html and @comark/vue 0.7.0 plus unreleased changes).
Motivation
Authors want links and labels that depend on runtime data, such as a slug, an id or an environment host. Today the options are:
- Compute the full string in the app and pass it as one
datavalue. This moves content decisions out of the document. - Write a wrapper component that joins the parts. This needs one component per prop.
- Use
{{ }}in text only.
Proposed solution
This is an open question, not a spec. Should a : prop that mixes fixed text and data be supported? If so, should it work in core, or only when the binding plugin is enabled?
I know the concern in #286 that this edges toward computed logic. #286 is about nested props, and #455 is its fix, which is a separate topic from this one. I also know the reasoning for keeping syntax out of core in #427 (pipe filters) and #437 (two-way binding).
A restricted form could reuse the {{ path || default }} syntax that binding() already ships. It would allow paths and defaults only, with no expressions and no filters.
I have a working prototype. I will open a PR only after we agree on a direction.
Alternatives considered
- A user-land component that takes
slugand builds the href. - A third-party plugin that rewrites the string before render.
- Pre-computing the full value in the host app.
Additional context
- Why this happens: the renderer reads a
:value as JSON or as one whole dot-path. A value such as/docs/{{ data.slug }}/overviewis neither. The HTML and ANSI renderers keep the raw string, and the framework renderers passundefined. Thebindingplugin only handles{{ }}in text nodes. - Plain props without the
:prefix stay literal today. This issue is only about:props. - The resolved string must pass the existing URL safety checks.
Activity