|
@@ -33,11 +33,14 @@
|
|
|
* binding logs '[statum] variable cycle: a → b → a' (variable names) and
|
|
* binding logs '[statum] variable cycle: a → b → a' (variable names) and
|
|
|
* the variable reads as undefined for that pass. Distinct bindings that
|
|
* the variable reads as undefined for that pass. Distinct bindings that
|
|
|
* shadow a name are not cycles (an inner `x` may reference an outer `x`).
|
|
* shadow a name are not cycles (an inner `x` may reference an outer `x`).
|
|
|
- * - `from-value` / `from-checked` on input/select/textarea read the live
|
|
|
|
|
- * PROPERTY (not the attribute): an input/change listener updates the
|
|
|
|
|
- * variable and calls renderAll() directly, per keystroke. Every other
|
|
|
|
|
- * attribute name, and every non-form-control element, uses a
|
|
|
|
|
- * MutationObserver.
|
|
|
|
|
|
|
+ * - OBSERVABLE PROPERTIES: `-from-{name}` where {name} is a registered
|
|
|
|
|
+ * observable live property (defaults: `value`, `checked` — the events a
|
|
|
|
|
+ * form control fires when they change) reads the PROPERTY and listens to
|
|
|
|
|
+ * those events: each event updates the bound variables and calls
|
|
|
|
|
+ * renderAll() directly. Further properties are registered with
|
|
|
|
|
+ * `statum.binding.observe(name, events)` — no module changes needed.
|
|
|
|
|
+ * Every other name (and any name the element lacks as a property) falls
|
|
|
|
|
+ * back to ATTRIBUTE semantics via MutationObserver.
|
|
|
* - Redraw guard: an observed attribute change only redraws when the
|
|
* - Redraw guard: an observed attribute change only redraws when the
|
|
|
* variable's derived value actually changed — the walk itself writes
|
|
* variable's derived value actually changed — the walk itself writes
|
|
|
* attributes (stm-attribute.*, hidden, data-stm-active) and a naive
|
|
* attributes (stm-attribute.*, hidden, data-stm-active) and a naive
|
|
@@ -149,22 +152,43 @@
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** Live-property form: from-value/from-checked on a form control. */
|
|
|
|
|
- function isPropSource(el, source) {
|
|
|
|
|
- if (source !== 'value' && source !== 'checked') return false;
|
|
|
|
|
- var tag = el.tagName;
|
|
|
|
|
- return tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA';
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * OBSERVABLE live properties: property name → the standard events that
|
|
|
|
|
+ * signal its change. This table — not hard-coded `if` checks — is what
|
|
|
|
|
+ * makes `-from-{property}` generic: `value` and `checked` ship by default
|
|
|
|
|
+ * (input/select/textarea fire `input`/`change`), and page code registers
|
|
|
|
|
+ * further event-driven properties via `statum.binding.observe()` instead
|
|
|
|
|
+ * of the module growing special cases.
|
|
|
|
|
+ * @type {Object<string, string[]>}
|
|
|
|
|
+ */
|
|
|
|
|
+ var observables = {
|
|
|
|
|
+ value: ['input', 'change'],
|
|
|
|
|
+ checked: ['input', 'change']
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * The events that make `name` observable on `el`, or null when `el` has
|
|
|
|
|
+ * no such property (fall back to attribute semantics — a `value` binding
|
|
|
|
|
+ * on a `<div>` observes the attribute, not a property the element lacks).
|
|
|
|
|
+ */
|
|
|
|
|
+ function observableFor(el, name) {
|
|
|
|
|
+ if (!Object.prototype.hasOwnProperty.call(observables, name)) return null;
|
|
|
|
|
+ if (!(name in el)) return null;
|
|
|
|
|
+ return observables[name];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Attach the redraw plumbing for one -from- binding. The listener/observer
|
|
|
|
|
- * is attached ONCE per (element, source); the VARIABLE registration
|
|
|
|
|
- * (propNames + seed) happens for every binding — several variables may
|
|
|
|
|
- * bind the same source, and each must see updates (review finding V1).
|
|
|
|
|
|
|
+ * Attach the redraw plumbing for one -from- binding. For an observable
|
|
|
|
|
+ * property (see `observables`) the listeners are attached ONCE per
|
|
|
|
|
+ * (element, property); the VARIABLE registration (propNames + seed)
|
|
|
|
|
+ * happens for every binding — several variables may bind the same
|
|
|
|
|
+ * property, and each must see updates (review finding V1). Everything
|
|
|
|
|
+ * else observes the attribute via MutationObserver.
|
|
|
*/
|
|
*/
|
|
|
function bindSource(el, binding) {
|
|
function bindSource(el, binding) {
|
|
|
var propKey = binding.source;
|
|
var propKey = binding.source;
|
|
|
- if (isPropSource(el, propKey)) {
|
|
|
|
|
|
|
+ var events = observableFor(el, propKey);
|
|
|
|
|
+ if (events) {
|
|
|
// Register this variable against the property (always).
|
|
// Register this variable against the property (always).
|
|
|
var names = propNames.get(el) || {};
|
|
var names = propNames.get(el) || {};
|
|
|
names[binding.name] = propKey;
|
|
names[binding.name] = propKey;
|
|
@@ -174,7 +198,7 @@
|
|
|
seed[binding.name] = el[propKey];
|
|
seed[binding.name] = el[propKey];
|
|
|
liveProps.set(el, seed);
|
|
liveProps.set(el, seed);
|
|
|
}
|
|
}
|
|
|
- // Attach the shared listener once per (element, property).
|
|
|
|
|
|
|
+ // Attach the shared listeners once per (element, property).
|
|
|
var bound = propBound.get(el);
|
|
var bound = propBound.get(el);
|
|
|
if (bound && bound[propKey]) return;
|
|
if (bound && bound[propKey]) return;
|
|
|
if (!bound) bound = {};
|
|
if (!bound) bound = {};
|
|
@@ -195,8 +219,9 @@
|
|
|
if (changed) liveProps.set(el, store);
|
|
if (changed) liveProps.set(el, store);
|
|
|
if (changed) renderAll();
|
|
if (changed) renderAll();
|
|
|
};
|
|
};
|
|
|
- el.addEventListener('input', onChange);
|
|
|
|
|
- el.addEventListener('change', onChange);
|
|
|
|
|
|
|
+ for (var i = 0; i < events.length; i++) {
|
|
|
|
|
+ el.addEventListener(events[i], onChange);
|
|
|
|
|
+ }
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
var seen = observed.get(el);
|
|
var seen = observed.get(el);
|
|
@@ -228,12 +253,10 @@
|
|
|
}
|
|
}
|
|
|
return function () {
|
|
return function () {
|
|
|
bindSource(el, binding);
|
|
bindSource(el, binding);
|
|
|
- if (isPropSource(el, binding.source)) {
|
|
|
|
|
- var store = liveProps.get(el);
|
|
|
|
|
- if (store && Object.prototype.hasOwnProperty.call(store, binding.name)) {
|
|
|
|
|
- return store[binding.name];
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // Observable properties read the live property; everything else reads
|
|
|
|
|
+ // the attribute. Reading fresh keeps the value correct even between
|
|
|
|
|
+ // the last event and the next render.
|
|
|
|
|
+ if (observableFor(el, binding.source)) return el[binding.source];
|
|
|
return el.getAttribute(binding.source);
|
|
return el.getAttribute(binding.source);
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
@@ -355,4 +378,28 @@
|
|
|
// --- installation -------------------------------------------------------
|
|
// --- installation -------------------------------------------------------
|
|
|
bridge.buildScope(buildScope);
|
|
bridge.buildScope(buildScope);
|
|
|
bridge.onPassStart(passStart);
|
|
bridge.onPassStart(passStart);
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Public surface: register (or replace) an OBSERVABLE PROPERTY — a live
|
|
|
|
|
+ * element property that fires standard DOM events when it changes, so
|
|
|
|
|
+ * `-from-{name}` bindings read it as a property and listen to those
|
|
|
|
|
+ * events instead of observing the attribute. `value` and `checked` are
|
|
|
|
|
+ * registered by default; page code can extend the set:
|
|
|
|
|
+ *
|
|
|
|
|
+ * statum.binding.observe('selectedIndex', ['change']);
|
|
|
|
|
+ * statum.binding.observe('open', ['toggle']);
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param {string} name property name
|
|
|
|
|
+ * @param {string[]} events events that signal the property changed
|
|
|
|
|
+ */
|
|
|
|
|
+ window.statum.binding = {
|
|
|
|
|
+ observe: function (name, events) {
|
|
|
|
|
+ if (typeof name !== 'string' || !name ||
|
|
|
|
|
+ !Array.isArray(events) || !events.length) {
|
|
|
|
|
+ console.warn('[statum] statum.binding.observe: expected a property name and a non-empty event list');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ observables[name] = events.slice();
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
})();
|
|
})();
|