Skip to content

Interpolate placeholders in bound props #489

Open

Description

@adamdehaven

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 }.

  1. 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>
  2. A YAML props block:

    ::card
    ---
    :title: Item {{ data.id }}
    ---
    Body
    ::

    Expected: <card title="Item 7">

    Actual: <card title="Item {{ data.id }}">

  3. A default value:

    :span{:title="Version {{ data.version || 1.0 }}"}

    Expected: <span title="Version 1.0"></span>, because data.version is 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 data value. 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 slug and 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 }}/overview is neither. The HTML and ANSI renderers keep the raw string, and the framework renderers pass undefined. The binding plugin 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions