sercrod

*download / n-download

Summary

*download turns any element into an accessible download trigger. It evaluates an expression to a download specification, fetches the resource from the given URL, and starts a browser download using a Blob-backed Object URL.:contentReference[oaicite:0]{index=0}:contentReference[oaicite:1]{index=1}

n-download is a star-less alias for the same directive. Both forms share one implementation and one manual entry.:contentReference[oaicite:2]{index=2}:contentReference[oaicite:3]{index=3}

Basic example

Trigger a CSV export from a server endpoint:

<serc-rod>
  <button type="button" *download="'/api/report.csv'">
    Download report
  </button>
</serc-rod>

Description

*download (and n-download) decorates an element so that activating it (mouse click, keyboard Enter/Space) downloads a file from a server endpoint. The directive:

It is purely side-effecting: it does not write into Sercrod data such as $download or $upload. Those slots are reserved for directives like *api, *fetch, and *post, and are cleared in _finalize independently of *download.:contentReference[oaicite:7]{index=7}

Behavior

At render time:

  1. Read and evaluate the expression

    Sercrod reads the attribute value from *download or n-download on the original template element and evaluates it as a JavaScript expression in the current scope:

    • The evaluation runs via eval_expr(expr, scope, { el: ctx_el, mode: "download" }).
    • The scope is the effective scope after *let and other scope modifiers.
    • ctx_el is the template element that carried the directive.:contentReference[oaicite:8]{index=8}:contentReference[oaicite:9]{index=9}
  2. Normalize options

    The result of the expression must be either:

    • a string, interpreted as { url: "<string>" }, or
    • an object, which is normalized by _normalize_download_opts.:contentReference[oaicite:10]{index=10}

    Normalization guarantees at least:

    • url (required): download URL.
    • method (default "GET").
    • headers (default {}).
    • credentials (default false).
    • filename (default null).
    • transport (default "fetch", or "xhr" for the XHR fallback).:contentReference[oaicite:11]{index=11}

    If the normalized result has no url, _normalize_download_opts throws an error, which is caught and reported as a sercrod-error event with stage: "download-init". The element stays in the DOM but no download handler is bound.:contentReference[oaicite:12]{index=12}:contentReference[oaicite:13]{index=13}

  3. Accessibility and event binding

    When options are valid, Sercrod:

    • ensures the element is keyboard-accessible by setting role="button" and tabIndex=0 when those attributes are missing, and:contentReference[oaicite:14]{index=14}
    • attaches a shared on_click handler to click and keydown (Enter/Space, non-repeating) events.:contentReference[oaicite:15]{index=15}
  4. Activation

    When the element is activated:

    • The handler calls e.preventDefault(). On an <a> element this stops the normal navigation; the download is always driven by the Blob-based URL, not any static href.:contentReference[oaicite:16]{index=16}

    • Sercrod dispatches a CustomEvent("sercrod-download-start", { detail:{ host, el, url }, bubbles:true, composed:true }) from the host.:contentReference[oaicite:17]{index=17}:contentReference[oaicite:18]{index=18}

    • It then performs the network request:

      • If transport === "xhr", it calls _xhr_download(opt):
        • Uses XMLHttpRequest with responseType="blob".
        • Applies opt.method || "GET", opt.url, opt.headers, and opt.credentials.:contentReference[oaicite:19]{index=19}
      • Otherwise, it uses fetch(opt.url, { method, headers, credentials, cache:"no-cache" }):
        • method defaults to "GET".
        • headers defaults to {}.
        • credentials is "include" when opt.credentials is truthy, "same-origin" otherwise.
        • Non-2xx responses throw an error.:contentReference[oaicite:20]{index=20}:contentReference[oaicite:21]{index=21}
    • After a successful response, Sercrod:

      • converts it to a Blob,
      • creates an object URL via URL.createObjectURL(blob),
      • creates a temporary <a> element, sets its href to the object URL and its download attribute to opt.filename || "download",:contentReference[oaicite:22]{index=22}
      • programmatically clicks the <a>, then removes it and revokes the object URL.:contentReference[oaicite:23]{index=23}
    • Finally, Sercrod emits CustomEvent("sercrod-downloaded", { detail:{ host, el, url, filename, status }, bubbles:true, composed:true }) on success, or CustomEvent("sercrod-error", { detail:{ host, el, stage:"download", error }, ... }) on errors during the request.:contentReference[oaicite:24]{index=24}:contentReference[oaicite:25]{index=25}

Errors during option evaluation or normalization are surfaced as sercrod-error with stage: "download-init", while errors during the actual network request use stage: "download".:contentReference[oaicite:26]{index=26}

Download options

The directive expression must evaluate to either:

Any extra keys in the object are currently ignored by the *download implementation.

Evaluation timing

If you need the URL or headers to reflect changing data, ensure that changes cause the host to call update() (usually via normal Sercrod data mutations), so the directive is re-evaluated.

Execution model

To react to downloads (for example, to set a “lastDownloadedAt” field), listen to the host events and update data in your own handlers.

Variable creation

*download does not create or modify any Sercrod variables. In particular:

All state and progress information is exposed via DOM events, not via the data model.

Scope layering

The directive expression is evaluated with the same scope rules as other expression-based directives:

This means a *download expression can freely use the same variables, helpers, and $parent access patterns as any other Sercrod expression.

Parent access

Inside a *download expression you can:

For example, in a nested host you might compute the URL from a parent configuration:

<serc-rod id="outer" data="{ apiBase: '/api' }">
  <serc-rod id="inner" data="{ reportId: 42 }">
    <button
      type="button"
      *download="{ url: $parent.apiBase + '/report/' + reportId + '.csv',
                  filename: 'report-' + reportId + '.csv' }">
      Download report
    </button>
  </serc-rod>
</serc-rod>

Use with conditionals and loops

*download belongs to the group of “own-element” directives in renderNode that:

In particular:

Example: combine *for and *download by putting *for on a parent and *download on a child:

<serc-rod data="{ files: [
  { name: 'a.csv', url: '/api/a.csv' },
  { name: 'b.csv', url: '/api/b.csv' }
] }">
  <ul>
    <li *for="file of files">
      <span *print="file.name"></span>
      <button
        type="button"
        *download="{ url: file.url, filename: file.name }">
        Download
      </button>
    </li>
  </ul>
</serc-rod>

Similarly, you can gate the presence of a download button with *if on an ancestor:

<div *if="user.canDownload">
  <button
    type="button"
    *download="{ url: reportUrl, filename: 'report.csv' }">
    Download report
  </button>
</div>

Best practices

Examples

1. Simple config in data

Move configuration into data and reference it from the directive:

<serc-rod data="{
  downloadSpec: {
    url: '/api/report.csv',
    filename: 'report.csv'
  }
}">
  <button type="button" *download="downloadSpec">
    Download CSV
  </button>
</serc-rod>
2. Secure download with credentials and headers
<serc-rod data="{
  reportUrl: '/api/secure/report',
  csrfToken: '...'
}">
  <button
    type="button"
    *download="{
      url: reportUrl,
      method: 'POST',
      headers: { 'X-CSRF-Token': csrfToken },
      credentials: true,
      filename: 'secure-report.pdf'
    }">
    Download secure PDF
  </button>
</serc-rod>
3. XHR transport fallback

Use XHR when fetch is problematic in the target environment:

<serc-rod data="{ url: '/api/proxy/report.csv' }">
  <button
    type="button"
    *download="{ url, transport: 'xhr', filename: 'report.csv' }">
    Download via XHR
  </button>
</serc-rod>
4. Listening to download events

Use DOM events on the host to update your own state:

<serc-rod id="app" data="{ url: '/api/report.csv' }">
  <button type="button" *download="{ url, filename: 'report.csv' }">
    Download report
  </button>
</serc-rod>

<script>
  const host = document.getElementById('app');

  host.addEventListener('sercrod-download-start', (e) => {
    console.log('Download started:', e.detail.url);
  });

  host.addEventListener('sercrod-downloaded', (e) => {
    console.log('Download finished:', e.detail.filename, e.detail.status);
  });

  host.addEventListener('sercrod-error', (e) => {
    if (e.detail.stage === 'download' || e.detail.stage === 'download-init') {
      console.error('Download error:', e.detail.error);
    }
  });
</script>

Notes