Эх сурвалжийг харах

Keep stm-def-{name}-as as an obsolescent alias for older templates

Per review feedback: the binding module now accepts stm-def-{name}-as="expr" as an exact alias of stm-local-{name} (identical lazy-cached subtree semantics; still requires the binding module) and logs a one-time-per-element obsolescence warning naming the modern spelling. Scenario 34 now asserts the legacy attribute binds and warns exactly once (not per pass); module header and the TEMPLATES/model/WHATS_NEW migration notes updated. Battery green: dom 35/35, split 8/8, markdown 96.4%.
saddle-ClankerSaddle 2 долоо хоног өмнө
parent
commit
85b675120d
5 өөрчлөгдсөн 45 нэмэгдсэн , 17 устгасан
  1. 5 3
      TEMPLATES.md
  2. 9 7
      WHATS_NEW.md
  3. 19 2
      js/statum-binding.js
  4. 1 1
      model.md
  5. 11 4
      tests/dom/scenarios.js

+ 5 - 3
TEMPLATES.md

@@ -248,8 +248,9 @@ Walk bindings:
   read) against the parent scope; the variable is visible to this element
   and its descendants (a child scope) and shadows slot types/outer
   variables. Multiple locals per element; same-element locals cannot
-  reference each other. Replaces the removed `stm-def-{name}-as`
-  (mkpstm's generated fragment-parameter attribute included).
+  reference each other. The legacy `stm-def-{name}-as` spelling is an exact
+  obsolescent alias (one console.warn per element); mkpstm emits the modern
+  form for `pstm-input` fragment parameters.
 - **`stm-text`** / **`stm-html`**: set text content / inner HTML from an
   expression. `undefined` leaves content untouched; otherwise stringified
   (`null` → `''`). Note (known issue C1): on editable controls `stm-text`
@@ -380,7 +381,8 @@ Canonical: core → document → markdown → binding (markdown and binding are
 order-independent of each other and need not co-exist). Cherry-pick trailing
 modules only. Without markdown: no `stm-markdown`, no `stm-h-demotion`.
 Without binding: no `stm-global-*` / `stm-local-*` / `stm-declare-*`
-variables (and the removed `stm-def-*` is inert everywhere). Core alone is
+variables — and note `stm-def-{name}-as` (their obsolescent alias) is inert
+without the binding module too. Core alone is
 HEADLESS — it applies state and directives with no DOM at all (point
 `statum.config` at your endpoints before init), so a non-document templating
 framework can use it as its state layer. Without document there is no render

+ 9 - 7
WHATS_NEW.md

@@ -185,9 +185,11 @@ own attribute writes never loop). On form controls `from-value` and
 variable immediately. `stm-declare-{name}` reserves a name for a subtree so
 a descendant can bind it and siblings of the binder can read it.
 
-**Migration**: `stm-def-{name}-as` is removed; write `stm-local-{name}`
-(same subtree semantics, now lazy). `statum-mkpstm` emits `stm-local-{name}`
-for `pstm-input` fragment parameters.
+**Migration**: `stm-def-{name}-as` still works as an exact obsolescent
+alias of `stm-local-{name}` (one console.warn per element), so older
+templates keep running; it requires the binding module like the modern
+form. `statum-mkpstm` emits `stm-local-{name}` for `pstm-input` fragment
+parameters.
 
 ## The reenter directive
 
@@ -207,7 +209,7 @@ and swallowed.
 
 ## Retired
 
-The former `stm-def-{name}-as` attribute is gone — replaced by
-`stm-local-{name}` (see the binding section). The single-file client
-(`statum.js`) remains gone: legacy references still expand at build time
-into the four module tags above.
+The former `stm-def-{name}-as` attribute is an obsolescent alias of
+`stm-local-{name}` (still works, warns once) — see the binding section. The
+single-file client (`statum.js`) remains gone: legacy references still expand
+at build time into the four module tags above.

+ 19 - 2
js/statum-binding.js

@@ -12,14 +12,19 @@
  *                                         element attribute; changes to that
  *                                         attribute trigger a redraw
  *   stm-local-{name}="expr"               subtree-scoped variable (element +
- *                                         descendants); replaces the removed
- *                                         stm-def-{name}-as
+ *                                         descendants); supersedes
+ *                                         stm-def-{name}-as (kept as an
+ *                                         obsolescent alias, see below)
  *   stm-local-{name}-from-{attribute}     subtree-scoped attribute-sourced
  *                                         variable; changes trigger a redraw
  *   stm-declare-{name}                    declares {name} for a subtree so a
  *                                         descendant can bind it and SIBLINGS
  *                                         of that binder can read it
  *
+ * The legacy `stm-def-{name}-as="expr"` spelling (the pre-binding-module
+ * form, still emitted by older templates) is accepted as an exact alias of
+ * `stm-local-{name}` and logs a one-time obsolescence warning per element.
+ *
  * Semantics (operator-confirmed):
  * - LAZY-CACHED: a binding's expression evaluates at most once per render
  *   pass, only when something reads the variable; the result is cached for
@@ -83,6 +88,8 @@
   /** Stable per-element identity for cache keys. @type {WeakMap} */
   var elIds = new WeakMap();
   var elSeq = 0;
+  /** Elements already warned for legacy stm-def usage. @type {WeakSet} */
+  var legacyWarned = new WeakSet();
 
   function elId(el) {
     var id = elIds.get(el);
@@ -122,6 +129,9 @@
   /**
    * Parse a binding attribute name into {kind, name, source, attr};
    * source is the attribute/property name for -from- forms, else null.
+   * `stm-def-{name}-as` (the pre-binding spelling) parses as a LOCAL
+   * expression binding with legacy=true: identical semantics, plus an
+   * obsolescence warning at bind time.
    * @param {string} attr
    */
   function parseBinding(attr) {
@@ -134,6 +144,8 @@
       return { kind: 'local', name: m[1], source: m[2], attr: attr };
     if ((m = attr.match(/^stm-local-([A-Za-z_$][\w$]*)$/)))
       return { kind: 'local', name: m[1], source: null, attr: attr };
+    if ((m = attr.match(/^stm-def-([A-Za-z_$][\w$]*)-as$/)))
+      return { kind: 'local', name: m[1], source: null, attr: attr, legacy: true };
     return null;
   }
 
@@ -279,6 +291,11 @@
       var binding = parseBinding(attrName);
       if (binding && binding.kind === 'local') {
         if (!child) child = Object.create(scope || null);
+        if (binding.legacy && !legacyWarned.has(el)) {
+          legacyWarned.add(el);
+          console.warn('[statum] stm-def-' + binding.name + '-as is obsolescent; ' +
+            'use stm-local-' + binding.name + ' (same semantics)');
+        }
         // If an ancestor declared this name, bind into its slot so the
         // declarer's whole subtree (including siblings of this element)
         // reads this binding.

+ 1 - 1
model.md

@@ -557,7 +557,7 @@ The `stm-local-{name}` attribute (statum-binding.js) defines a variable bound to
 </div>
 ```
 
-`stm-global-{name}` declares a page-wide variable usable in any expression on the page (regardless of document order; duplicates resolve last-in-document-order with a warning). The `-from-{attribute}` forms source the variable from an element attribute instead of an expression — for `value`/`checked` on form controls the live PROPERTY is read (typing updates the variable and triggers an immediate redraw); other attributes use a MutationObserver and redraw only when the derived value actually changes. `stm-declare-{name}` declares a name for a subtree without a value so a descendant can bind it and siblings of that binder can read it. Referencing a variable that is part of an evaluation cycle logs an error naming the cycle and reads as `undefined` for that pass. The former `stm-def-{name}-as` attribute has been removed; `statum-mkpstm` now emits `stm-local-{name}` for fragment parameters.
+`stm-global-{name}` declares a page-wide variable usable in any expression on the page (regardless of document order; duplicates resolve last-in-document-order with a warning). The `-from-{attribute}` forms source the variable from an element attribute instead of an expression — for `value`/`checked` on form controls the live PROPERTY is read (typing updates the variable and triggers an immediate redraw); other attributes use a MutationObserver and redraw only when the derived value actually changes. `stm-declare-{name}` declares a name for a subtree without a value so a descendant can bind it and siblings of that binder can read it. Referencing a variable that is part of an evaluation cycle logs an error naming the cycle and reads as `undefined` for that pass. The former `stm-def-{name}-as` attribute is kept as an exact obsolescent alias of `stm-local-{name}` (same semantics, one console.warn per element) so older templates keep working; it still requires the binding module. `statum-mkpstm` emits the modern `stm-local-{name}` form for fragment parameters.
 
 ## Server-side attribute warnings
 

+ 11 - 4
tests/dom/scenarios.js

@@ -985,18 +985,25 @@ async function main() {
     check(plain.textContent === '5', 'slot type must still read outside the shadow');
 
   } else if (scenario === '34') {
-    // stm-local replaces stm-def: the demo's stamp pattern, plus confirm the
-    // old attribute is inert (documented removal).
+    // stm-local replaces stm-def: the demo's stamp pattern, and the legacy
+    // stm-def-{name}-as spelling works as an obsolescent alias (warns once
+    // per element, binds identically).
     var box34 = el('div'); box34.setAttribute('stm-local-stamp', "'updated '" + ' + counter.n');
     var out34 = el('span'); out34.setAttribute('stm-text', 'stamp');
     box34.appendChild(out34); body.appendChild(box34);
     await setState('counter', { n: 7 });
     check(out34.textContent === 'updated 7', 'stamp local wrong: ' + out34.textContent);
     var legacy = el('div'); legacy.setAttribute('stm-def-old-as', '1');
-    var legacyOut = el('span'); legacyOut.setAttribute('stm-text', 'typeof old');
+    var legacyOut = el('span'); legacyOut.setAttribute('stm-text', 'old');
     legacy.appendChild(legacyOut); body.appendChild(legacy);
+    log.warns.length = 0;
+    await s._bridge.renderAll(); await spin();
+    check(legacyOut.textContent === '1', 'legacy stm-def must still bind: ' + legacyOut.textContent);
+    check(log.warns.length === 1 && /obsolescent/.test(log.warns[0]),
+      'legacy stm-def must warn once: ' + JSON.stringify(log.warns));
+    var warnsAfter = log.warns.length;
     await s._bridge.renderAll(); await spin();
-    check(legacyOut.textContent === 'undefined', 'stm-def must be inert after removal');
+    check(log.warns.length === warnsAfter, 'obsolescence warn must fire once per element, not per pass');
 
   } else if (scenario === '35') {
     // Several -from- bindings on ONE element (review finding: the plumbing