sercrod

*api / n-api

Summary

*api / n-api is the low-level HTTP gateway directive in Sercrod.

It is responsible for:

Higher level helpers such as *download or *upload share the same $pending / $error / $download / $upload convention, but *api is the single general-purpose primitive for ad-hoc HTTP calls on normal elements.

Basic example

A simple GET that populates user and exposes status to children:

<serc-rod id="app" data='{"user": null}'>
  <section
    *api="/api/user.json"
    *into="user">

    <p *if="$pending">Loading user...</p>

    <p *if="$error" class="error">
      <span *print="$error.message"></span>
    </p>

    <pre *if="user" *print="JSON.stringify(user, null, 2)"></pre>
  </section>
</serc-rod>

Key points in this example:

Behavior

Core behavior

When Sercrod finds *api or n-api on an element during rendering:

  1. It clones the element (shallow clone, without children).

  2. It reads the relevant attributes:

    • *api / n-api - URL template string.
    • method - HTTP method, default "GET".
    • body or payload - expression for request body (non-GET only).
    • *into / n-into - optional destination key in the host data.
  3. It detects file uploads:

    • isFile is true when the cloned element is <input type="file">.
  4. It initializes status fields on this._data if they are missing:

    • $pending - false
    • $error - null
    • $download - null
    • $upload - null
    • into key - when provided and not present, it is created and set to null.
  5. It appends the clone to the parent.

  6. It wires up request logic depending on the element type:

    • <input type="file"> - prepare upload on change.
    • Button-like elements - trigger JSON-like request on click.
    • Other elements - schedule a one-shot automatic request.
  7. It renders the original children into the clone with the current scope, so that children can read $pending, $error, $download, $upload, and the *into variable.

Request URL and placeholders

URL source

The URL template is taken exactly from:

The raw string is passed to an internal helper _expand_text(urlRaw, scope, work), which performs placeholder expansion based on the global delimiters.

By default, the delimiters are:

So a URL such as:

<section
  *api="/api/users/%userId%?ts=%Date.now()%"
  *into="user">
</section>

is turned into a concrete URL by evaluating each expression between the delimiters in the current scope, then substituting the result.

Notes:

HTTP method and body

Method

The HTTP method is read from the method attribute on the same element, then uppercased:

For <input type="file" *api>, GET is automatically converted to POST during the upload, but the original method string is still used in the non-file path and in event payloads for JSON-like requests.

JSON body (non-file elements)

For non-file elements, *api can optionally send a JSON body when the method is not GET.

The body expression is taken from:

When bodyExp is non-empty and method !== "GET":

  1. Sercrod evaluates the expression with eval_expr(bodyExp, scope, { el: work, mode: "body" }).
  2. If evaluation succeeds and the result is not null or undefined, it builds:
    • headers: { "Content-Type": "application/json" }
    • body: JSON.stringify(value)
  3. If evaluation of the body expression throws, the error is ignored, and the request is sent without a body.

For method === "GET":

Parsing the response

After the request completes:

Arrays, objects, and primitive values are all stored as-is.

File uploads with <input type="file" *api>

When *api is placed on an <input type="file">:

<serc-rod id="uploader">
  <input
    type="file"
    name="files[]"
    *api="/upload">
</serc-rod>

the behavior is:

The response parsing and placement are the same as for JSON-like requests, with these differences:

There is no automatic request on page load for file inputs. Uploads only happen when the user selects files.

Shared state flags and *into

Status flags

*api ensures that the following properties exist on the host data object:

These flags are created even when *into is not set, so you can always:

*into / n-into

*into and n-into allow you to additionally store the response into a named data property:

Example:

<serc-rod id="app" data='{"profile": null, "logs": []}'>
  <section
    *api="/api/profile"
    *into="profile">

    <p *if="$pending">Loading profile...</p>
    <p *if="$error" *print="$error.message"></p>

    <h2 *if="profile" *print="profile.name"></h2>
  </section>
</serc-rod>

Important notes:

Events

*api dispatches two CustomEvents on the element that owns the directive.

sercrod-api

Dispatched after a successful request, with detail containing:

The event is configured with:

so ancestor elements and outer frameworks can listen for it.

sercrod-error

Dispatched when the request, parsing, or internal processing throws. Details differ slightly:

Evaluation timing and scope

Scope used for URL and body

Inside _renderElement(node, scope, parent), Sercrod maintains:

For *api:

This means:

If you need to prepare derived values for *api, prefer one of these patterns:

Child rendering order

The order around *api is:

  1. Status fields ($pending, $error, $download, $upload, and into key) are created if missing.
  2. The host clone is appended to the DOM.
  3. Event handlers and auto-run logic are registered.
  4. Children are rendered into the clone, using the current effective scope.

This guarantees that:

Execution model and triggers

Non-file elements

For non-file elements, *api chooses the trigger based on the element type:

Deduplication key for auto-run

For non-clickable, non-file elements, *api builds an automatic deduplication key and uses an internal __apiOnce set:

The final once-key is:

At render time:

As a result:

File inputs

For <input type="file" *api>, there is:

Use with conditionals and loops

Showing loading and errors

A typical pattern is:

<serc-rod id="users" data='{"items": [], "selectedId": null}'>
  <section *api="/api/users" *into="items">
    <p *if="$pending">Loading users...</p>

    <p *if="$error" class="error">
      <span *print="$error.message"></span>
    </p>

    <ul *if="!$pending && !$error && items">
      <li *for="user of items">
        <button
          @click="selectedId = user.id"
          *print="user.name">
        </button>
      </li>
    </ul>
  </section>
</serc-rod>

Because *api fires automatically on the non-clickable section, this:

Inside loops

You can place *api inside *for or *each loops, but keep in mind:

For independent state per iteration, consider:

Use with other directives

*into

*into is designed to be used with *api (and some related directives such as *websocket and upload helpers). When combined with *api:

Other network helpers

Although *api, *download, and *upload are related conceptually, they consume the element in different ways:

In the current implementation:

For clarity and future-proofing, it is recommended to:

Event handlers (@click and others)

*api coexists with event directives such as @click, @change, and others:

Be aware that:

Because *post, *fetch, and *api all treat HTTP communication as “JSON in, JSON out” and share the same state flags, it is natural to standardize server-side handlers around this contract.

Recommended approach on the server:

Benefits for server-side code:

Position in Sercrod’s design:

Best practices

Examples

GET with ignored body

Because *api only uses the body expression for non-GET methods, the following does not send a body:

<section
  *api="/api/search"
  method="GET"
  body="{ query: term }"
  *into="results">
</section>

To send JSON, change to method="POST":

<section
  *api="/api/search"
  method="POST"
  body="{ query: term }"
  *into="results">
</section>
Button-triggered POST
<serc-rod id="formHost" data='{"form": {"name": "", "email": ""}, "saved": null}'>
  <input type="text"
         :value="form.name"
         @input="form.name = $event.target.value">

  <input type="email"
         :value="form.email"
         @input="form.email = $event.target.value">

  <button
    *api="/api/submit"
    method="POST"
    body="form"
    *into="saved">
    Save
  </button>

  <p *if="$pending">Saving...</p>
  <p *if="$error" *print="$error.message"></p>
  <p *if="saved" *print="'Saved as id ' + saved.id"></p>
</serc-rod>
Simple file upload with preview
<serc-rod id="avatarHost" data='{"avatarResult": null}'>
  <input
    type="file"
    name="avatar"
    accept="image/*"
    *api="/api/avatar"
    *into="avatarResult">

  <p *if="$pending">Uploading...</p>
  <p *if="$error" *print="$error.message"></p>

  <p *if="avatarResult && avatarResult.url">
    <img :src="avatarResult.url" alt="Avatar">
  </p>
</serc-rod>

Notes