Jelajahi Sumber

Final features for the night

Billy Barrow 3 minggu lalu
induk
melakukan
320d2aaa19
3 mengubah file dengan 93 tambahan dan 19 penghapusan
  1. 12 9
      frontend-demo/index.html
  2. 71 10
      js/statum.js
  3. 10 0
      model.md

+ 12 - 9
frontend-demo/index.html

@@ -210,17 +210,20 @@
       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-* -->
+      <!-- Counter: stm-text, stm-class.x, stm-if, stm-action, stm-data-*, stm-attribute, stm-disabled -->
       <section>
-        <h2>Counter <span class="muted">(stm-text · stm-class · stm-if · stm-data)</span></h2>
+        <h2>Counter <span class="muted">(stm-text · stm-class · stm-if · stm-data · stm-attribute · stm-disabled)</span></h2>
         <div class="counter">
-          <button stm-action="counter.decrement">−</button>
-          <!-- stm-class.high toggles when value > 5 -->
-          <span class="value" stm-text="counter.value" stm-class.high="counter.value > 5">0</span>
+          <!-- stm-disabled disables the element + descendants when the predicate is true -->
+          <button stm-action="counter.decrement" stm-disabled="counter.value &lt;= 0">−</button>
+          <!-- stm-class.high toggles when value > 5; stm-attribute.data-value sets a data-* attr -->
+          <span class="value" stm-text="counter.value" stm-class.high="counter.value > 5" stm-attribute.data-value="counter.value">0</span>
           <button stm-action="counter.increment">+</button>
           <button stm-action="counter.reset">Reset</button>
           <!-- stm-data-amount sends a literal value with the request -->
           <button stm-action="counter.add" stm-data-amount="5">+5</button>
+          <!-- stm-attribute.checked toggles a boolean attribute from a boolean result -->
+          <label class="pill"><input type="checkbox" stm-attribute.checked="counter.high"> high flag</label>
         </div>
         <!-- stm-if on a derived boolean -->
         <p stm-if="counter.high">That is a high number!</p>
@@ -269,7 +272,7 @@
               ($<span stm-text="line.price"></span>)
             </li>
           </ul>
-          <button stm-action="cart.checkoutAction">Checkout</button>
+          <button stm-action="cart.checkoutAction" stm-disabled="cart.empty">Checkout</button>
           <button stm-action="cart.clearAction">Clear cart</button>
         </div>
       </section>
@@ -287,10 +290,10 @@
       </section>
 
       <section>
-        <h2>Session</h2>
+        <h2>Session <span class="muted">(stm-confirm)</span></h2>
         <p>
-          <!-- Demonstrates a `navigate` directive (full reload) + `clear` directives. -->
-          <button stm-action="user.resetAllAction">Reset everything</button>
+          <!-- stm-confirm shows a confirm() dialog before the action runs. -->
+          <button stm-action="user.resetAllAction" stm-confirm="Reset all session state?">Reset everything</button>
         </p>
       </section>
     </div>

+ 71 - 10
js/statum.js

@@ -721,6 +721,53 @@
     }
   }
 
+  /**
+   * Bind `stm-attribute.{name}`: a boolean result toggles the attribute, any
+   * other defined value sets it, and `undefined` removes it.
+   *
+   * For booleans, prefer the element's IDL property when one exists (e.g.
+   * `checked`, `disabled`, `hidden`) — setting the content attribute is
+   * unreliable for these once the control is "dirty".
+   */
+  function handleAttribute(el, scope) {
+    var attrs = el.attributes;
+    for (var i = 0; i < attrs.length; i++) {
+      var attr = attrs[i];
+      if (attr.name.indexOf('stm-attribute.') === 0) {
+        var name = attr.name.slice('stm-attribute.'.length);
+        if (!name) continue;
+        var value = evalExpr(attr.value, scope);
+        if (value === undefined) {
+          el.removeAttribute(name);
+        } else if (typeof value === 'boolean') {
+          if (name in el && typeof el[name] === 'boolean') el[name] = value;
+          else if (value) el.setAttribute(name, '');
+          else el.removeAttribute(name);
+        } else {
+          el.setAttribute(name, value === null ? '' : String(value));
+        }
+      }
+    }
+  }
+
+  /**
+   * Apply `stm-disabled`: when the predicate is truthy, disable the element and
+   * its descendant form controls; otherwise enable them.
+   */
+  function handleDisabled(el, scope) {
+    if (!el.hasAttribute('stm-disabled')) return;
+    setDisabledCascade(el, !!evalExpr(el.getAttribute('stm-disabled'), scope));
+  }
+
+  /** Set the `disabled` attribute on an element and its descendant controls. */
+  function setDisabledCascade(el, on) {
+    var nodes = [el].concat(Array.prototype.slice.call(
+      el.querySelectorAll('button,input,select,textarea,fieldset')));
+    nodes.forEach(function (n) {
+      if (on) n.setAttribute('disabled', ''); else n.removeAttribute('disabled');
+    });
+  }
+
   /**
    * Bind `stm-text` / `stm-html`. Per the "leave original DOM content" policy,
    * an unresolved value (`undefined`) leaves the element untouched; otherwise
@@ -838,6 +885,8 @@
   function processInner(el, scope) {
     el.__stmScope = scope; // latest scope, used when an action fires
     handleClass(el, scope);
+    handleAttribute(el, scope);
+    handleDisabled(el, scope);
     handleTextHtml(el, scope);
     wireAction(el);
     render(el, scope);
@@ -893,28 +942,37 @@
 
   /**
    * Toggle the busy state of an action element: disables the element and its
-   * descendant form controls and applies `stm-busy-class` classes while busy,
-   * restoring prior state when not.
+   * descendant form controls and applies `stm-busy-class` classes while busy.
+   *
+   * On un-busy the disabled state is not "restored" to a captured snapshot
+   * (that would clobber a `stm-disabled` value that `renderAll` applied during
+   * the response); instead the nodes are re-enabled and `stm-disabled` is
+   * re-evaluated, which re-disables any that should remain disabled.
    */
   function setBusyState(el, busy) {
     var nodes = [el].concat(Array.prototype.slice.call(
       el.querySelectorAll('button,input,select,textarea,fieldset')));
     var busyClasses = (el.getAttribute('stm-busy-class') || '').split(/\s+/).filter(Boolean);
     if (busy) {
-      el.__stmDisabled = [];
-      nodes.forEach(function (n) {
-        if (!('disabled' in n)) return;
-        el.__stmDisabled.push([n, n.disabled]);
-        n.disabled = true;
-      });
+      nodes.forEach(function (n) { n.setAttribute('disabled', ''); });
       busyClasses.forEach(function (c) { el.classList.add(c); });
     } else {
-      (el.__stmDisabled || []).forEach(function (pair) { pair[0].disabled = pair[1]; });
-      el.__stmDisabled = [];
+      nodes.forEach(function (n) { n.removeAttribute('disabled'); });
       busyClasses.forEach(function (c) { el.classList.remove(c); });
+      reapplyDisabled(el);
     }
   }
 
+  /** Re-apply `stm-disabled` to an element and any descendants that carry it. */
+  function reapplyDisabled(el) {
+    var scope = el.__stmScope || stateScope();
+    if (el.hasAttribute('stm-disabled')) handleDisabled(el, scope);
+    var scoped = el.querySelectorAll('[stm-disabled]');
+    Array.prototype.forEach.call(scoped, function (n) {
+      handleDisabled(n, n.__stmScope || scope);
+    });
+  }
+
   /** Apply or remove `stm-on-error` classes on an action element. */
   function applyErrorClasses(el, on) {
     if (!el || !el.hasAttribute || !el.hasAttribute('stm-on-error')) return;
@@ -947,6 +1005,9 @@
     var action = evalExpr(el.getAttribute('stm-action'), scope);
     if (!action || typeof action.uri !== 'string') return;
 
+    // stm-confirm: gate the action behind a confirm() dialog (literal message).
+    if (el.hasAttribute('stm-confirm') && !window.confirm(el.getAttribute('stm-confirm'))) return;
+
     applyErrorClasses(el, false); // clear error state on a new attempt
     var data = collectData(el);
     setBusyState(el, true);

+ 10 - 0
model.md

@@ -191,6 +191,10 @@ The attribute `stm-text` binds the element's `innerText` to the specified state
 
 The attribute `stm-html` binds the element's `innerHTML` to the specified state value.
 
+## Attribute binding
+
+The attribute `stm-attribute.{name}` (where `{name}` is any attribute name) binds an attribute of the element to an expression. When the expression evaluates to a boolean, the attribute is toggled by presence (`true` adds it as a boolean attribute, `false` removes it); when it evaluates to any other value, the attribute is set to that value (coerced to a string). An `undefined` result removes the attribute. For example `<a stm-attribute.href="user.profileUrl">` sets the `href`, while `<input type="checkbox" stm-attribute.checked="user.agreed">` toggles `checked`.
+
 ## Conditional display
 
 The attribute `stm-if` adds or removes the element from the DOM depending on the expression inside the statement, e.g. `<div stm-if="typeName.visable">` or `<div stm-if="typeName.level > 5">`
@@ -203,12 +207,18 @@ The attribute `stm-else` can exist only as the next sibling of an element with `
 
 The attribute `stm-class.x` where `x` is any class name can be used to toggle classes on or off with a predicate, e.g. `<div stm-class.alert="typeName.waterLevel > 50">`.
 
+## Disabled state
+
+The attribute `stm-disabled` takes a predicate expression. When truthy, the element and all of its descendant form controls are disabled; when falsy they are enabled, e.g. `<button stm-action="typeName.submit" stm-disabled="!user.canSubmit">Submit</button>`.
+
 ## Actions
 
 The attribute `stm-action` binds the element's primary action (`click` for buttons and links, `submit` for forms), overriding the default behaviour to instead perform the Statum action referenced by the given path into state. For example `<button stm-action="typeName.deleteItem">Delete</button>`.
 
 Additional data can be attached to the action using one or more `stm-data-{key}` attributes, where `{key}` is the data key and the attribute value is its value, e.g. `<button stm-action="typeName.deleteItem" stm-data-confirm="true">Delete</button>`. These values are merged with any form input values when the request is sent.
 
+The attribute `stm-confirm` may be added to an action element to require confirmation before it runs: its literal value is shown in a browser `confirm()` dialog, and the action is only performed if the user accepts, e.g. `<button stm-action="typeName.deleteItem" stm-confirm="Delete this item?">Delete</button>`.
+
 The attribute will also disable the element (and any children) while the action is being processed, and restore the original state once completed (or on error). In addition to this, the attribute `stm-busy-class` can be used to specify one or more css classes to apply during this time, and `stm-on-error` can specify one or more css classes to apply while the element is in an error state (cleared the next time the action is attempted). The `statum.act` promise for the action also rejects on failure.
 
 When used on a form, any form inputs are sent in the JSON or query string of the action, bound by their `name`, as they would for a regular native form submission.