@webjsdev/core 0.7.47 → 0.7.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/webjs-core-browser.js +5 -5
- package/dist/webjs-core-browser.js.map +3 -3
- package/dist/webjs-core-testing.js +21 -21
- package/dist/webjs-core-testing.js.map +2 -2
- package/dist/webjs-core.js +7 -7
- package/dist/webjs-core.js.map +3 -3
- package/index-browser.js +1 -1
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/package.json +1 -1
- package/src/action-seed-client.d.ts +23 -1
- package/src/action-seed-client.js +359 -17
- package/src/component.d.ts +7 -3
- package/src/form-action.js +204 -203
- package/src/render-client.js +94 -96
- package/src/render-server.js +60 -109
- package/src/router-client.js +617 -12
package/src/render-client.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
reconcileFormAction, isBoundFormAction, ABSENT, assertSubmitterHasNoName,
|
|
7
7
|
assertSubmitterType, assertSubmitterHasNoValue, assertSubmitterHasNoStaticFormAction,
|
|
8
8
|
assertSubmitterHasNoFormAttribute, assertSingleSubmitterAction,
|
|
9
|
-
|
|
9
|
+
resolveBoundSubmitterAttrs, applyResolvedAttr, releaseSubmitterAttrs,
|
|
10
10
|
assertConvergentSubmitter, isSubmitterReflectedProp,
|
|
11
11
|
formActionId, assertIdentifiableAction, FORM_ACTION_FIELD,
|
|
12
12
|
} from './form-action.js';
|
|
@@ -104,14 +104,6 @@ function commitInto(node, fn) {
|
|
|
104
104
|
|
|
105
105
|
/** @type {WeakMap<TemplateStringsArray | string[], { templateEl: HTMLTemplateElement, parts: PartDescriptor[], formActions: FormActionRecord[] | null }>} */
|
|
106
106
|
const templateCache = new WeakMap();
|
|
107
|
-
/**
|
|
108
|
-
* Forms whose `action` hole currently holds a function (#1207). Read by a
|
|
109
|
-
* submitter that reconciles before its form does, so it can tell a form that is
|
|
110
|
-
* about to be bound from one that never will be. Keyed weakly so a detached
|
|
111
|
-
* form is collected with its entry.
|
|
112
|
-
* @type {WeakMap<HTMLFormElement, unknown>}
|
|
113
|
-
*/
|
|
114
|
-
const formActionCandidates = new WeakMap();
|
|
115
107
|
/**
|
|
116
108
|
* Submitters this renderer stamped with an identity, so a later release removes
|
|
117
109
|
* only the framework's own `name` / `value` and never an author's.
|
|
@@ -652,6 +644,11 @@ function assignPaths(root, parts) {
|
|
|
652
644
|
function buildFormActionRecord(el, onEl, parts) {
|
|
653
645
|
const isForm = el.localName === 'form';
|
|
654
646
|
const targetAttr = isForm ? 'action' : 'formaction';
|
|
647
|
+
// The submission pair, named per element (#1307). A form declares `method` /
|
|
648
|
+
// `enctype`; a submitter declares `formmethod` / `formenctype`, and a bound
|
|
649
|
+
// one receives them from the renderer exactly as a bound form does.
|
|
650
|
+
const methodAttr = isForm ? 'method' : 'formmethod';
|
|
651
|
+
const enctypeAttr = isForm ? 'enctype' : 'formenctype';
|
|
655
652
|
const actionParts = onEl.filter((p) => p.kind === 'attr' && p.name.toLowerCase() === targetAttr);
|
|
656
653
|
if (!actionParts.length) return null;
|
|
657
654
|
|
|
@@ -697,7 +694,11 @@ function buildFormActionRecord(el, onEl, parts) {
|
|
|
697
694
|
}
|
|
698
695
|
continue;
|
|
699
696
|
}
|
|
700
|
-
|
|
697
|
+
// #1307: a bound submitter carries its OWN submission attributes, so the
|
|
698
|
+
// pair to capture depends on the element. `formmethod` / `formenctype` on a
|
|
699
|
+
// button are exactly what `method` / `enctype` are on a form, and they flow
|
|
700
|
+
// through the same resolution below untouched.
|
|
701
|
+
if (name !== methodAttr && name !== enctypeAttr) continue;
|
|
701
702
|
if (p.kind !== 'attr' && p.kind !== 'attr-mixed' && p.kind !== 'bool') continue;
|
|
702
703
|
const d = /** @type any */ (parts[p.idx]);
|
|
703
704
|
/** @type {FormAttrPart} */
|
|
@@ -706,7 +707,14 @@ function buildFormActionRecord(el, onEl, parts) {
|
|
|
706
707
|
// pieces have to travel with the record; reading the anchor's value alone
|
|
707
708
|
// is right only when the statics are empty.
|
|
708
709
|
if (p.kind === 'attr-mixed') { entry.statics = d.statics || []; entry.group = d.group || []; }
|
|
709
|
-
|
|
710
|
+
// Compared against `methodAttr`, NOT the literal 'method'. On a submitter
|
|
711
|
+
// the pair is `formmethod` / `formenctype`, so a literal comparison sent
|
|
712
|
+
// every hole-provided `formmethod` into `enctypeParts`: the client then
|
|
713
|
+
// resolved it as the enctype, refused `formenctype="post"`, and threw on
|
|
714
|
+
// hydration for a template SSR renders happily. That is the
|
|
715
|
+
// render-on-the-server, crash-in-the-browser direction this module treats
|
|
716
|
+
// as the one unacceptable failure.
|
|
717
|
+
(name === methodAttr ? methodParts : enctypeParts).push(entry);
|
|
710
718
|
}
|
|
711
719
|
|
|
712
720
|
return {
|
|
@@ -728,8 +736,8 @@ function buildFormActionRecord(el, onEl, parts) {
|
|
|
728
736
|
propAttrs,
|
|
729
737
|
nameParts,
|
|
730
738
|
valueParts,
|
|
731
|
-
staticMethod: el.getAttribute(
|
|
732
|
-
staticEnctype: el.getAttribute(
|
|
739
|
+
staticMethod: el.getAttribute(methodAttr),
|
|
740
|
+
staticEnctype: el.getAttribute(enctypeAttr),
|
|
733
741
|
methodParts,
|
|
734
742
|
enctypeParts,
|
|
735
743
|
};
|
|
@@ -754,8 +762,9 @@ function buildFormActionRecord(el, onEl, parts) {
|
|
|
754
762
|
*/
|
|
755
763
|
function reconcileFormActions(formActions, bound, values) {
|
|
756
764
|
if (!formActions) return;
|
|
757
|
-
|
|
758
|
-
|
|
765
|
+
// Forms first, then submitters. The original motivation was a boundness read
|
|
766
|
+
// that is gone with #1307, but the ordering still makes a form's RELEASE run
|
|
767
|
+
// before its submitters reconcile, so it is kept rather than churned.
|
|
759
768
|
for (const pass of [true, false]) {
|
|
760
769
|
for (const rec of formActions) {
|
|
761
770
|
if (rec.isForm !== pass) continue;
|
|
@@ -779,9 +788,6 @@ function reconcileFormActions(formActions, bound, values) {
|
|
|
779
788
|
effectiveFormAttr(rec.enctypeParts, rec.staticEnctype, values),
|
|
780
789
|
rec,
|
|
781
790
|
);
|
|
782
|
-
if (typeof val === 'function' && formActionId(val)) {
|
|
783
|
-
boundForms.push(/** @type any */ (part.el));
|
|
784
|
-
}
|
|
785
791
|
} else {
|
|
786
792
|
// What SSR would have emitted for this pass. An attribute hole always
|
|
787
793
|
// emits (even `name=${null}`, as `name=""`); a boolean hole emits only
|
|
@@ -791,14 +797,14 @@ function reconcileFormActions(formActions, bound, values) {
|
|
|
791
797
|
? !!resolveHoleValue(values[np.i])
|
|
792
798
|
: true));
|
|
793
799
|
reconcileSubmitterAction(
|
|
794
|
-
/** @type any */ (part.el), val, rec,
|
|
800
|
+
/** @type any */ (part.el), val, rec,
|
|
801
|
+
emits(rec.nameParts), emits(rec.valueParts),
|
|
802
|
+
effectiveFormAttr(rec.methodParts, rec.staticMethod, values),
|
|
803
|
+
effectiveFormAttr(rec.enctypeParts, rec.staticEnctype, values),
|
|
795
804
|
);
|
|
796
805
|
}
|
|
797
806
|
}
|
|
798
807
|
}
|
|
799
|
-
// Part B (#1207), last, because it reads the DOM state every reconcile above
|
|
800
|
-
// has finished writing. See `assertBoundFormSubmitters`.
|
|
801
|
-
for (const form of boundForms) assertBoundFormSubmitters(form);
|
|
802
808
|
}
|
|
803
809
|
|
|
804
810
|
/**
|
|
@@ -820,65 +826,47 @@ function releaseSubmitterAction(el) {
|
|
|
820
826
|
submitterActionBindings.delete(el);
|
|
821
827
|
}
|
|
822
828
|
|
|
823
|
-
/**
|
|
824
|
-
* Find the enclosing `<form>` by walking parents.
|
|
825
|
-
*
|
|
826
|
-
* Deliberately a parent walk rather than `closest('form')`: the element may
|
|
827
|
-
* still be inside a detached DocumentFragment when this runs, and the walk
|
|
828
|
-
* gives the same answer there for a form and a button in the same template.
|
|
829
|
-
* Returns null when the fragment's root is reached, which is the honest answer
|
|
830
|
-
* for a nested template whose form lives in the parent.
|
|
831
|
-
*
|
|
832
|
-
* @param {Element} el
|
|
833
|
-
* @returns {HTMLFormElement | null}
|
|
834
|
-
*/
|
|
835
|
-
function enclosingForm(el) {
|
|
836
|
-
let node = el.parentNode;
|
|
837
|
-
while (node) {
|
|
838
|
-
if (node.nodeType === 1 && /** @type {Element} */ (node).localName === 'form') {
|
|
839
|
-
return /** @type {HTMLFormElement} */ (node);
|
|
840
|
-
}
|
|
841
|
-
node = node.parentNode;
|
|
842
|
-
}
|
|
843
|
-
return null;
|
|
844
|
-
}
|
|
845
|
-
|
|
846
829
|
/**
|
|
847
830
|
* Converge a live submitter on what SSR emitted for `<button formaction=${fn}>`
|
|
848
|
-
* (#1207): the `formaction` attribute gone,
|
|
849
|
-
* own `name` / `value` pair
|
|
831
|
+
* (#1207, #1307): the `formaction` attribute gone, the identity in the button's
|
|
832
|
+
* own `name` / `value` pair, and the submission the framework supplies in its
|
|
833
|
+
* own `formmethod` / `formenctype`.
|
|
850
834
|
*
|
|
851
|
-
*
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
*
|
|
857
|
-
*
|
|
858
|
-
* feature exists for, a per-row button in a list, on a page SSR renders
|
|
859
|
-
* perfectly. So an UNRESOLVED form skips the assertion and the binding is
|
|
860
|
-
* applied: the server is the renderer that sees every page, and a genuinely
|
|
861
|
-
* unbound form is still refused there, loudly, before anything ships.
|
|
835
|
+
* There is no enclosing-form question here any more. It used to be the hard
|
|
836
|
+
* part of this function, asked once per element and BEST EFFORT because a
|
|
837
|
+
* fragment reconciling detached (a submitter inside a `repeat()` or an array
|
|
838
|
+
* item, whose form lives in the parent template) has no answer to give. With
|
|
839
|
+
* the submission attributes now on the button itself (#1307) the button needs
|
|
840
|
+
* nothing from the form, so the question stopped mattering and the whole
|
|
841
|
+
* asymmetry with SSR went with it.
|
|
862
842
|
*
|
|
863
843
|
* @param {Element} el
|
|
864
844
|
* @param {unknown} value
|
|
865
845
|
* @param {FormActionRecord} rec
|
|
866
846
|
* @param {boolean} emitsName whether SSR would emit a `name` attribute this pass
|
|
867
847
|
* @param {boolean} emitsValue whether SSR would emit a `value` attribute this pass
|
|
848
|
+
* @param {string | typeof ABSENT} authoredFormMethod what the template supplies for `formmethod`
|
|
849
|
+
* @param {string | typeof ABSENT} authoredFormEnctype what the template supplies for `formenctype`
|
|
868
850
|
*/
|
|
869
|
-
function reconcileSubmitterAction(
|
|
851
|
+
function reconcileSubmitterAction(
|
|
852
|
+
el, value, rec, emitsName, emitsValue, authoredFormMethod, authoredFormEnctype,
|
|
853
|
+
) {
|
|
870
854
|
const id = typeof value === 'function' ? formActionId(value) : null;
|
|
871
855
|
if (!id) {
|
|
872
856
|
// A function that was MEANT as an action still refuses, so a button never
|
|
873
857
|
// silently submits the form's action instead of its own.
|
|
874
858
|
if (typeof value === 'function') assertIdentifiableAction(null, el.localName);
|
|
875
859
|
releaseSubmitterAction(el);
|
|
860
|
+
// The submission attributes the framework supplied go with the identity, or
|
|
861
|
+
// a released button keeps a `formmethod` / `formenctype` SSR does not emit
|
|
862
|
+
// for the same template (#1307).
|
|
863
|
+
releaseSubmitterAttrs(el, authoredFormMethod, authoredFormEnctype, rec.propAttrs);
|
|
876
864
|
return;
|
|
877
865
|
}
|
|
878
866
|
// Template-shaped refusals first, from the compiled record, because the live
|
|
879
867
|
// element may already carry this renderer's own `name` / `value`.
|
|
880
868
|
assertSingleSubmitterAction(rec.duplicateAction, el.localName);
|
|
881
|
-
assertConvergentSubmitter(rec.propAttrs, el.localName
|
|
869
|
+
assertConvergentSubmitter(rec.propAttrs, el.localName);
|
|
882
870
|
if (rec.staticAction) assertSubmitterHasNoStaticFormAction(el.localName);
|
|
883
871
|
if (rec.authoredValue || emitsValue) assertSubmitterHasNoValue(el.localName);
|
|
884
872
|
if (rec.authoredName || emitsName) {
|
|
@@ -892,32 +880,23 @@ function reconcileSubmitterAction(el, value, rec, emitsName, emitsValue) {
|
|
|
892
880
|
if (rec.authoredForm || el.hasAttribute('form')) assertSubmitterHasNoFormAttribute(el.localName);
|
|
893
881
|
assertSubmitterType(el.localName, el.getAttribute('type'));
|
|
894
882
|
|
|
895
|
-
|
|
896
|
-
//
|
|
897
|
-
//
|
|
898
|
-
//
|
|
899
|
-
//
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
// first paint. Skipping for an element already stamped can only REMOVE a
|
|
903
|
-
// throw, never add one, so it cannot refuse anything that used to render.
|
|
904
|
-
if (form && !submitterActionBindings.has(el)) {
|
|
905
|
-
// `formActionCandidates` covers the pass where the form's own action hole
|
|
906
|
-
// has committed but its reconcile has not run yet; the identity field is
|
|
907
|
-
// the settled answer, including for an SSR'd form on first hydration.
|
|
908
|
-
const isFormBound = !!(form.querySelector(`input[name="${FORM_ACTION_FIELD}"]`)
|
|
909
|
-
|| formActionCandidates.has(form));
|
|
910
|
-
assertSubmitterFormIsBound(isFormBound, el.localName);
|
|
911
|
-
}
|
|
912
|
-
assertSubmitterSubmission(
|
|
913
|
-
el.localName,
|
|
914
|
-
el.getAttribute('formmethod'),
|
|
915
|
-
el.getAttribute('formenctype'),
|
|
916
|
-
{ bound: true },
|
|
883
|
+
// The submission decision, made from the TEMPLATE rather than by reading the
|
|
884
|
+
// DOM back, exactly as `reconcileFormAction` makes it for a bound form. The
|
|
885
|
+
// distinction the DOM cannot preserve is the same one: `?formmethod=${false}`
|
|
886
|
+
// and `formmethod=${null}` both leave no attribute, and SSR resolves them to
|
|
887
|
+
// opposite answers.
|
|
888
|
+
const resolved = resolveBoundSubmitterAttrs(
|
|
889
|
+
el.localName, authoredFormMethod, authoredFormEnctype,
|
|
917
890
|
);
|
|
918
891
|
el.removeAttribute('formaction');
|
|
919
892
|
el.setAttribute('name', FORM_ACTION_FIELD);
|
|
920
893
|
el.setAttribute('value', id);
|
|
894
|
+
// AFTER the identity, matching SSR's byte order: the identity replaces the
|
|
895
|
+
// `formaction=` hole in place and the submission pair is appended at the `>`.
|
|
896
|
+
// The differential parity suite compares these two renderers byte for byte,
|
|
897
|
+
// so the order is part of the contract, not a detail.
|
|
898
|
+
applyResolvedAttr(el, 'formmethod', authoredFormMethod, resolved.formMethod);
|
|
899
|
+
applyResolvedAttr(el, 'formenctype', authoredFormEnctype, resolved.formEnctype);
|
|
921
900
|
submitterActionBindings.set(el, id);
|
|
922
901
|
}
|
|
923
902
|
|
|
@@ -1180,17 +1159,6 @@ function applyPart(part, value, _prev, allValues) {
|
|
|
1180
1159
|
applyChild(part, value);
|
|
1181
1160
|
break;
|
|
1182
1161
|
case 'attr': {
|
|
1183
|
-
// #1207: note a form whose action hole holds a function, so a submitter
|
|
1184
|
-
// reconciling BEFORE that form's own reconcile can still tell the form is
|
|
1185
|
-
// going to be bound. Recorded at commit time because that is the only
|
|
1186
|
-
// moment the hole's value is in hand for a form the submitter does not
|
|
1187
|
-
// own. Cleared on a non-function so a form that stops being bound does
|
|
1188
|
-
// not keep vouching for its buttons.
|
|
1189
|
-
if (part.el.localName === 'form' && part.name.toLowerCase() === 'action') {
|
|
1190
|
-
const candidate = resolveHoleValue(value);
|
|
1191
|
-
if (typeof candidate === 'function') formActionCandidates.set(part.el, candidate);
|
|
1192
|
-
else formActionCandidates.delete(part.el);
|
|
1193
|
-
}
|
|
1194
1162
|
if (value == null || value === false) part.el.removeAttribute(part.name);
|
|
1195
1163
|
else if (isBoundFormAction(value, part.name, part.el.localName)) {
|
|
1196
1164
|
// #1155: the ONE supported form-action binding, applied to the live
|
|
@@ -1701,16 +1669,46 @@ function nodesToFrag(nodes) {
|
|
|
1701
1669
|
return frag;
|
|
1702
1670
|
}
|
|
1703
1671
|
|
|
1704
|
-
/**
|
|
1672
|
+
/**
|
|
1673
|
+
* Remove a template instance's whole range, its bookend markers INCLUDED.
|
|
1674
|
+
*
|
|
1675
|
+
* Every caller discards the instance right after: the map entry or slot that
|
|
1676
|
+
* held it is dropped, and a replacement, where there is one, is built with
|
|
1677
|
+
* markers of its own. So this is a REMOVE, never lit's clear-and-reuse. A
|
|
1678
|
+
* caller that wants to keep the bookends and render into them again needs its
|
|
1679
|
+
* OWN function, because the two want opposite answers for the end marker.
|
|
1680
|
+
*
|
|
1681
|
+
* `parent` is read BEFORE the walk because the walk removes `start` on its
|
|
1682
|
+
* first iteration, which nulls `start.parentNode`. Reading it afterwards
|
|
1683
|
+
* compared the end marker's live parent against `null`, so the guard could
|
|
1684
|
+
* never fire and every teardown left one `wjm-e` comment in the document,
|
|
1685
|
+
* unbounded for the life of the region.
|
|
1686
|
+
*
|
|
1687
|
+
* The `end.parentNode === parent` comparison is a refusal, not a formality. A
|
|
1688
|
+
* marker moved under a different parent is not this region's to remove, and
|
|
1689
|
+
* `parent.removeChild(end)` on it throws NotFoundError from inside a teardown
|
|
1690
|
+
* that has to stay total.
|
|
1691
|
+
*
|
|
1692
|
+
* The walk assumes the range is INTACT: it steps `nextSibling` from `start`
|
|
1693
|
+
* and stops on `end`, so a range whose end no longer follows its start runs
|
|
1694
|
+
* off the child list and takes the part's own marker with it. That is not
|
|
1695
|
+
* something this function defends against, before or after the parent capture,
|
|
1696
|
+
* and the consequence is spelled out where it bites, on `reconcileRepeat`'s
|
|
1697
|
+
* catch below. The guard is the narrower promise: whatever the walk did, a
|
|
1698
|
+
* marker that is somewhere else is left alone.
|
|
1699
|
+
*
|
|
1700
|
+
* @param {Node} start @param {Node} end
|
|
1701
|
+
*/
|
|
1705
1702
|
function removeBetween(start, end) {
|
|
1706
|
-
|
|
1703
|
+
const parent = start.parentNode;
|
|
1704
|
+
if (!parent) return;
|
|
1707
1705
|
let n = start;
|
|
1708
1706
|
while (n && n !== end) {
|
|
1709
1707
|
const next = n.nextSibling;
|
|
1710
1708
|
n.parentNode?.removeChild(n);
|
|
1711
1709
|
n = next;
|
|
1712
1710
|
}
|
|
1713
|
-
if (end.parentNode ===
|
|
1711
|
+
if (end.parentNode === parent) parent.removeChild(end);
|
|
1714
1712
|
}
|
|
1715
1713
|
|
|
1716
1714
|
/* ================================================================
|