Billy Barrow 3 tuần trước cách đây
mục cha
commit
90a9ca411d
4 tập tin đã thay đổi với 45 bổ sung38 xóa
  1. 5 1
      frontend-demo/index.html
  2. 20 7
      frontend-demo/server.php
  3. 18 30
      js/statum.js
  4. 2 0
      model.md

+ 5 - 1
frontend-demo/index.html

@@ -205,7 +205,11 @@
     <!-- stm-preloader is shown until the entrypoint resolves; stm-content after. -->
     <div stm-preloader>Loading application state…</div>
 
-    <div stm-content>
+    <!--
+      stm-content is pre-hidden in the HTML so the un-bound template never
+      flashes before the script runs; statum removes `hidden` once ready.
+    -->
+    <div stm-content hidden>
       <!-- Counter: stm-text, stm-class.x, stm-if, stm-action, stm-data-* -->
       <section>
         <h2>Counter <span class="muted">(stm-text · stm-class · stm-if · stm-data)</span></h2>

+ 20 - 7
frontend-demo/server.php

@@ -55,17 +55,30 @@ function key_for(string $type): string
 }
 
 /**
- * Build a snapshot for a type. `always` makes transmit_after == as_at so the
- * client sends the base64 frame on every request (demonstrating that path).
+ * Build a snapshot for a type. Options:
+ *  - `always`: transmit_after == as_at, so the client sends the base64 frame on
+ *    every request (the always-send path).
+ *  - `stale`: transmit_after is set 10s in the future (with a far-future
+ *    invalid_after). The slot is fresh for 10s, after which the client must
+ *    pre-flight via /_statum/slots before each request. Load the page, wait ~10s,
+ *    then trigger an action to see the pre-flight request.
  */
 function make_snapshot(string $type, string $scope, array $public, array $opts = []): array
 {
-    $now = gmdate('Y-m-d\TH:i:s\Z');
+    $now = time();
+    $asAt = gmdate('Y-m-d\TH:i:s\Z', $now);
+    if (!empty($opts['always'])) {
+        $transmit = $asAt;
+    } elseif (!empty($opts['stale'])) {
+        $transmit = gmdate('Y-m-d\TH:i:s\Z', $now + 10);
+    } else {
+        $transmit = gmdate('Y-m-d\TH:i:s\Z', $now + 60);
+    }
     return [
-        'as_at' => $now,
+        'as_at' => $asAt,
         'slot' => ['key' => key_for($type), 'scope' => $scope, 'type' => $type],
-        'transmit_after' => !empty($opts['always']) ? $now : gmdate('Y-m-d\TH:i:s\Z', time() + 60),
-        'invalid_after' => gmdate('Y-m-d\TH:i:s\Z', time() + 600),
+        'transmit_after' => $transmit,
+        'invalid_after' => gmdate('Y-m-d\TH:i:s\Z', $now + 600),
         'private' => $opts['private'] ?? '',
         'public' => $public,
     ];
@@ -182,7 +195,7 @@ function snap(string $type): ?array
         case 'counter':  return make_snapshot('counter', 'session', build_counter());
         case 'products': return make_snapshot('products', 'session', build_products());
         case 'cart':     return make_snapshot('cart', 'session', build_cart(), ['always' => true]);
-        case 'user':     return make_snapshot('user', 'session', build_user());
+        case 'user':     return make_snapshot('user', 'session', build_user(), ['stale' => true]);
         case 'notice':   return make_snapshot('notice', 'flow', build_notice());
     }
     return null;

+ 18 - 30
js/statum.js

@@ -646,8 +646,6 @@
   var instancesByHolder = new WeakMap();
   /** Loop instance nodes; skipped by the render walk (owned by their holder). */
   var instanceNodes = new WeakSet();
-  /** If-chain anchors for detached branches. @type {WeakMap<Element, Comment>} */
-  var ifAnchors = new WeakMap();
   /** Elements whose `stm-action` has been wired, to avoid double-binding. */
   var wired = new WeakSet();
 
@@ -674,30 +672,14 @@
     toRemove.forEach(function (n) { el.removeAttribute(n); });
   }
 
-  /** Ensure an element is in the DOM (reattaching at its remembered anchor). */
-  function ensureAttached(el) {
-    var anchor = ifAnchors.get(el);
-    if (anchor && anchor.parentNode) {
-      anchor.parentNode.insertBefore(el, anchor);
-      anchor.parentNode.removeChild(anchor);
-      ifAnchors.delete(el);
-    }
-  }
-
-  /** Detach an element, remembering its position with a comment anchor. */
-  function ensureDetached(el) {
-    if (!el.parentNode) return;
-    if (!ifAnchors.has(el)) {
-      var anchor = document.createComment('#stm-if');
-      el.parentNode.insertBefore(anchor, el);
-      ifAnchors.set(el, anchor);
-    }
-    el.parentNode.removeChild(el);
-  }
-
   /**
    * Evaluate an `stm-if` / `stm-else-if` / `stm-else` chain beginning at `el`,
-   * attaching the matching branch and detaching the rest.
+   * showing the matching branch and hiding the rest.
+   *
+   * Branches are toggled with the `hidden` attribute rather than detached, so
+   * every branch stays in the DOM and is re-evaluated on each render. (Detaching
+   * the `stm-if` element would remove it from the render walk, leaving an
+   * already-attached `stm-else` branch skipped and never refreshed.)
    * @returns {Element|null} The active branch element (or `null`).
    */
   function handleIfChain(el, scope) {
@@ -720,8 +702,8 @@
       if (b.expr === null || !!evalExpr(b.expr, scope)) { activeEl = b.el; break; }
     }
     branches.forEach(function (b) {
-      if (b.el === activeEl) ensureAttached(b.el);
-      else ensureDetached(b.el);
+      if (b.el === activeEl) b.el.removeAttribute('hidden');
+      else b.el.setAttribute('hidden', '');
     });
     return activeEl;
   }
@@ -747,7 +729,7 @@
   function handleTextHtml(el, scope) {
     if (el.hasAttribute('stm-text')) {
       var text = evalExpr(el.getAttribute('stm-text'), scope);
-      if (text !== undefined) el.innerText = text === null ? '' : String(text);
+      if (text !== undefined) el.textContent = text === null ? '' : String(text);
     }
     if (el.hasAttribute('stm-html')) {
       var html = evalExpr(el.getAttribute('stm-html'), scope);
@@ -984,9 +966,15 @@
   // ---------------------------------------------------------------------------
 
   /**
-   * Toggle pre/post-entrypoint visibility. While loading, `stm-preloader`
-   * elements are shown and `stm-content` elements hidden; once ready, the
-   * reverse. Uses the `hidden` attribute so it does not fight inline styles.
+   * Toggle pre/post-entrypoint visibility with the `hidden` attribute. While
+   * loading, `stm-preloader` elements are shown and `stm-content` elements
+   * hidden; once ready, the reverse.
+   *
+   * To avoid a flash of un-bound template before the script runs, authors may
+   * pre-set `hidden` directly on `stm-content` in the HTML (e.g.
+   * `<div stm-content hidden>`); the library removes it once ready. The library
+   * also sets `hidden` itself during loading as a fallback for authors who do
+   * not pre-set it.
    * @param {boolean} ready
    */
   function setBootState(ready) {

+ 2 - 0
model.md

@@ -181,6 +181,8 @@ The library fetches the entrypoint on load before the template is fully usable.
 
 These allow a page to display a loading state while state is fetched, then reveal the bound content once it is available.
 
+To prevent a flash of the un-bound template before the script runs, authors may pre-set the `hidden` attribute directly on `stm-content` in the HTML (e.g. `<div stm-content hidden>`). The library removes it once the entrypoint has been processed, and also sets `hidden` itself during loading as a fallback for authors who do not pre-set it.
+
 ## Text binding
 
 The attribute `stm-text` binds the element's `innerText` to the specified state value, e.g. `<span stm-text="typeName.label">`.