@jsenv/navi 0.29.141 → 0.29.143
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/jsenv_navi.js +282 -164
- package/dist/jsenv_navi.js.map +12 -20
- package/docs/AI_INSTRUCTIONS.md +17 -3
- package/docs/actions.md +3 -0
- package/docs/control_value.md +7 -1
- package/docs/css_architecture.md +1 -1
- package/docs/navigation.md +117 -35
- package/docs/popup_open.md +18 -5
- package/docs/state_binding.md +142 -0
- package/package.json +1 -1
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -153,6 +153,16 @@ consistency across the app, not from any single call site.
|
|
|
153
153
|
given a `ui` draws no placeholder of its own. Read it before putting a
|
|
154
154
|
`BadgeList` in a picker's `ui`, before wrapping its fallback in a `Badge`,
|
|
155
155
|
and before setting `maxLines` on the list rather than on the picker.
|
|
156
|
+
- `docs/state_binding.md` — the rule the whole API rests on: state navi shows is
|
|
157
|
+
BOUND (`signal`), not copied back by a callback. The three shapes an
|
|
158
|
+
application ever has to say (`signal` / `action` / `command` + `commandFor`),
|
|
159
|
+
the four ways the same mistake is written (`onCurrentChange`, `uiAction`,
|
|
160
|
+
`onClick` and `onOpen`/`onClose` all writing a signal), why a binding is not
|
|
161
|
+
merely shorter (it is written by every gesture, it moves the component when
|
|
162
|
+
written, and it goes through the interaction gate), what is left for the
|
|
163
|
+
callback, and what to conclude when the state being wired has no prop for it —
|
|
164
|
+
a parallel copy, or a binding navi is missing. Read it before writing any
|
|
165
|
+
handler whose body only assigns state.
|
|
156
166
|
- `docs/control_value.md` — who holds a control's value: nobody, a bound
|
|
157
167
|
`signal` (two-way, in both directions), or you (`value`/`checked`). What
|
|
158
168
|
`signal` + `defaultValue` says, what a signal holds for each kind of control,
|
|
@@ -282,9 +292,13 @@ consistency across the app, not from any single call site.
|
|
|
282
292
|
is given back the offset it was read at, and only when the row owns the
|
|
283
293
|
document scroll), a back arrow that stays inside the app (`useCanNavBack()` /
|
|
284
294
|
`navBack({ fallback })`), and the few cases where tabs are legitimately not
|
|
285
|
-
URLs.
|
|
286
|
-
|
|
287
|
-
|
|
295
|
+
URLs. It also holds the middle answer — a position READ from the URL and
|
|
296
|
+
restored on reload without a route and without an entry per step: a search
|
|
297
|
+
param signal handed to a `SlideContainer`, walked to rather than jumped to,
|
|
298
|
+
and `history: "push"` for the states whose values ARE places (with the write
|
|
299
|
+
that says otherwise, `signal.set(v, { history })`). Read it before writing any
|
|
300
|
+
routing code — the position of the user belongs in the URL by default, and
|
|
301
|
+
that decision is not retrofittable.
|
|
288
302
|
- Source code and demos on GitHub:
|
|
289
303
|
https://github.com/jsenv/core/tree/main/packages/frontend/navi — where to go
|
|
290
304
|
when the built export's JSDoc doesn't answer the question (see "Where the
|
package/docs/actions.md
CHANGED
|
@@ -268,6 +268,9 @@ changes do exactly that).
|
|
|
268
268
|
|
|
269
269
|
## See also
|
|
270
270
|
|
|
271
|
+
- [state_binding.md](./state_binding.md) — `signal`, `action` or
|
|
272
|
+
`command` + `commandFor`: the three shapes, and the callback that only
|
|
273
|
+
remembers
|
|
271
274
|
- [resource.md](./resource.md) — actions created from REST callbacks, and the
|
|
272
275
|
store behind them
|
|
273
276
|
- [resource_with_params.md](./resource_with_params.md) — `withParams()` and
|
package/docs/control_value.md
CHANGED
|
@@ -243,7 +243,10 @@ that is a navi control goes through the same state controller, and the same
|
|
|
243
243
|
|
|
244
244
|
`SlideContainer` takes one too, though it is layout rather than a control: the
|
|
245
245
|
area it shows is a piece of state like any other, and binding it is what lets
|
|
246
|
-
something else read where the slides are — or move them by writing it.
|
|
246
|
+
something else read where the slides are — or move them by writing it. Bind it
|
|
247
|
+
to a `stateSignal` a route declares as a search param and the area is in the
|
|
248
|
+
address, with no route per slide — see
|
|
249
|
+
[navigation.md](./navigation.md#a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from).
|
|
247
250
|
|
|
248
251
|
Inside a `List selectable` you can bind the list, or give each `List.Item` its
|
|
249
252
|
own `selected` — but not expect the two to arbitrate. An item that declares
|
|
@@ -348,6 +351,9 @@ directions; it just has nothing extra to say.
|
|
|
348
351
|
|
|
349
352
|
## See also
|
|
350
353
|
|
|
354
|
+
- [state_binding.md](./state_binding.md) — the same rule beyond controls:
|
|
355
|
+
a `SlideContainer`'s area, a popup being open, the position in the app — all
|
|
356
|
+
bound rather than copied back by a callback
|
|
351
357
|
- [form_changed.md](./form_changed.md) — what a form makes of each of these:
|
|
352
358
|
which fields it counts as already answered, and when it sends nothing
|
|
353
359
|
- [control_object.md](./control_object.md) — several controls reading as one
|
package/docs/css_architecture.md
CHANGED
|
@@ -41,7 +41,7 @@ This means an app can override a Navi default from its own unlayered CSS, withou
|
|
|
41
41
|
}
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
Targeting the same element is not a detail — see [`--navi-*` vs `--component-*`](#--navi
|
|
44
|
+
Targeting the same element is not a detail — see [`--navi-*` vs `--component-*`](#--navi--vs---component--where-the-override-has-to-go) below.
|
|
45
45
|
|
|
46
46
|
### Why actual rules stay outside any layer
|
|
47
47
|
|
package/docs/navigation.md
CHANGED
|
@@ -3,6 +3,23 @@
|
|
|
3
3
|
How to build navigation with `@jsenv/navi`: declaring routes, rendering them,
|
|
4
4
|
linking to them, and turning them into tabs.
|
|
5
5
|
|
|
6
|
+
- [The rule that decides everything else: the position belongs in the URL](#the-rule-that-decides-everything-else-the-position-belongs-in-the-url)
|
|
7
|
+
- [Declaring routes](#declaring-routes)
|
|
8
|
+
- [A section is allowed to be a route of its own](#a-section-is-allowed-to-be-a-route-of-its-own)
|
|
9
|
+
- [Which values a param accepts](#which-values-a-param-accepts)
|
|
10
|
+
- [An address that only sends elsewhere](#an-address-that-only-sends-elsewhere)
|
|
11
|
+
- [Search params](#search-params)
|
|
12
|
+
- [Rendering routes](#rendering-routes)
|
|
13
|
+
- [Loading data](#loading-data)
|
|
14
|
+
- [Links and tab rows](#links-and-tab-rows)
|
|
15
|
+
- [The back arrow: `navBack`](#the-back-arrow-navback)
|
|
16
|
+
- [Tabs that travel: `RouteTravel`](#tabs-that-travel-routetravel)
|
|
17
|
+
- [Where a navigation lands: the scroll](#where-a-navigation-lands-the-scroll)
|
|
18
|
+
- [Creating something, then editing it](#creating-something-then-editing-it)
|
|
19
|
+
- [Tabs that are not routes](#tabs-that-are-not-routes)
|
|
20
|
+
- [A `SlideContainer` in the URL: a position that is not a place one came from](#a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from)
|
|
21
|
+
- [A state whose values ARE places: `history: "push"`](#a-state-whose-values-are-places-history-push)
|
|
22
|
+
|
|
6
23
|
## The rule that decides everything else: the position belongs in the URL
|
|
7
24
|
|
|
8
25
|
Where the user is — which section, which tab, which sub-page — is state. Put it
|
|
@@ -21,8 +38,10 @@ retrofitted later:
|
|
|
21
38
|
So the default shape of a tab row is routes: `<Nav>` + `<Link route>` +
|
|
22
39
|
`<RouteTravel>`. `SlideContainer` is the exception, not the starting point — see
|
|
23
40
|
[Tabs that are not routes](#tabs-that-are-not-routes) for the cases that
|
|
24
|
-
genuinely are one, and
|
|
25
|
-
|
|
41
|
+
genuinely are one, and
|
|
42
|
+
[A `SlideContainer` in the URL](#a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from)
|
|
43
|
+
for the middle answer: a position READ from the URL and restored on reload,
|
|
44
|
+
without a route and without a history entry per step.
|
|
26
45
|
|
|
27
46
|
## Declaring routes
|
|
28
47
|
|
|
@@ -289,6 +308,11 @@ Declared on the **root route**, a search param is a position that holds wherever
|
|
|
289
308
|
one is in the application — a view mode that survives moving from page to page.
|
|
290
309
|
Declared on one route, it exists only there.
|
|
291
310
|
|
|
311
|
+
Writing it AMENDS the history entry one is on rather than stacking a new one: a
|
|
312
|
+
param that qualifies a screen is not a place, and one entry per write turns a
|
|
313
|
+
single back-press into as many as the user moved. A state whose values ARE
|
|
314
|
+
places says so — see [`history: "push"`](#a-state-whose-values-are-places-history-push).
|
|
315
|
+
|
|
292
316
|
## Rendering routes
|
|
293
317
|
|
|
294
318
|
`<Route>` is the only primitive. With `children` it is a container that renders
|
|
@@ -575,9 +599,9 @@ match at once; the first matching branch wins).
|
|
|
575
599
|
## Tabs that are not routes
|
|
576
600
|
|
|
577
601
|
`SlideContainer` holds slides that replace one another in one box, with the same
|
|
578
|
-
gestures and the same travelling bar, and — unless it is
|
|
579
|
-
below — nothing written to the URL. Use it when the
|
|
580
|
-
place one should be able to link to:
|
|
602
|
+
gestures and the same travelling bar, and — unless the signal it is bound to is
|
|
603
|
+
one the URL holds, see below — nothing written to the URL. Use it when the
|
|
604
|
+
position genuinely is not a place one should be able to link to:
|
|
581
605
|
|
|
582
606
|
- the steps of a wizard, or the screens of a picker, inside a dialog or a popover
|
|
583
607
|
— a popup is promoted to the browser's top layer, so no container can hold two
|
|
@@ -604,7 +628,7 @@ page. It reads which slide is on screen from the container itself, and its bar
|
|
|
604
628
|
follows the slides, a finger dragging them included. `<Link slide>` has no href
|
|
605
629
|
and behaves like a button: this is not a link to anywhere.
|
|
606
630
|
|
|
607
|
-
###
|
|
631
|
+
### A `SlideContainer` in the URL: a position that is not a place one came from
|
|
608
632
|
|
|
609
633
|
"Should a link be able to open the app on this?" has a third answer, and a wizard
|
|
610
634
|
is exactly it: **yes for reading and for reloading, no for history.** The step one
|
|
@@ -615,48 +639,106 @@ and it should NOT stack an entry per step, because the back arrow of a form mean
|
|
|
615
639
|
back-press into four, and walk the reader backwards through a form they thought
|
|
616
640
|
they had left.
|
|
617
641
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
guards. A plain `SlideContainer` writes nothing at all.
|
|
642
|
+
There is nothing to invent for it: a search param already IS a position in the
|
|
643
|
+
URL that replaces rather than pushes ([Search params](#search-params)). Declare
|
|
644
|
+
the step as one, and hand its signal to the container:
|
|
622
645
|
|
|
623
|
-
|
|
624
|
-
|
|
646
|
+
```js
|
|
647
|
+
const stepSignal = stateSignal("when", {
|
|
648
|
+
id: "step",
|
|
649
|
+
oneOf: ["when", "where", "who", "recap", "done"],
|
|
650
|
+
// the step qualifies THIS visit, not the screen: a link built to the editor
|
|
651
|
+
// does not inherit the step one happens to be on, and it goes back to the
|
|
652
|
+
// default when the route stops matching
|
|
653
|
+
weak: true,
|
|
654
|
+
});
|
|
655
|
+
export const ALERT_EDIT_ROUTE = route("/alerts/:alertId/edit", {
|
|
656
|
+
searchParams: { step: stepSignal },
|
|
657
|
+
});
|
|
658
|
+
```
|
|
625
659
|
|
|
626
660
|
```jsx
|
|
627
|
-
<SlideContainer
|
|
661
|
+
<SlideContainer signal={stepSignal}>
|
|
628
662
|
```
|
|
629
663
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
- it
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
the
|
|
641
|
-
|
|
664
|
+
That is the whole wiring, and every half of it is the piece that already
|
|
665
|
+
existed. Worth naming, because each answers a question a wizard actually has:
|
|
666
|
+
|
|
667
|
+
- **where it opens is the state's own default** — the first argument of
|
|
668
|
+
`stateSignal`. The container has a `defaultCurrent` for when it owns its
|
|
669
|
+
position, but a bound container does not own it: one place says where the step
|
|
670
|
+
starts, and it is the same place a reset goes back to;
|
|
671
|
+
- **the param is absent while the step IS that default** (route.js prunes it), so
|
|
672
|
+
`/alerts/W-123/edit` is clean on the first step, gains `?step=where` on the
|
|
673
|
+
second, and loses it again coming back. The address carries what differs from
|
|
674
|
+
the usual answer and nothing else;
|
|
675
|
+
- **the container walks to the step rather than jumping to it.** The address comes
|
|
676
|
+
from outside the box (typed, shared, kept from a session that has moved on), so
|
|
677
|
+
every slide between here and there is asked to let go the way a key going that
|
|
678
|
+
way would ask it, and the first one that holds is where one stops. `?step=done`
|
|
679
|
+
cannot open a confirmation screen for something nobody sent. The signal is then
|
|
680
|
+
written with the area actually shown, so the address never says one is
|
|
681
|
+
somewhere one is not;
|
|
682
|
+
- **only the travels that HAPPENED are written.** A travel a lock refused, or one
|
|
683
|
+
`onCurrentChange` refused late, never reaches the signal — or is written back
|
|
684
|
+
when it does.
|
|
642
685
|
|
|
643
686
|
A container remembers nothing across a reload, so a step whose `required` the app
|
|
644
687
|
knows is already satisfied says so itself (`required={!alreadyFilled}`); the same
|
|
645
688
|
holds for a hold that a finished job lifts (`preventNavNext={!published}`).
|
|
646
689
|
|
|
647
|
-
|
|
648
|
-
|
|
690
|
+
**A start that depends on where one is** — a wizard creating something opens on
|
|
691
|
+
its first question, the same wizard editing something opens on its summary — is
|
|
692
|
+
not a prop on the container either: a `stateSignal` takes a SIGNAL as its default
|
|
693
|
+
and follows it as long as nobody has answered otherwise.
|
|
649
694
|
|
|
650
|
-
```
|
|
651
|
-
|
|
695
|
+
```js
|
|
696
|
+
const stepDefaultSignal = computed(() =>
|
|
697
|
+
// `matchingSignal`, not `matching`: the plain property is a mirror, the
|
|
698
|
+
// signal is what a computed can follow
|
|
699
|
+
ALERT_EDIT_ROUTE.matchingSignal.value ? "recap" : "when",
|
|
700
|
+
);
|
|
701
|
+
const stepSignal = stateSignal(stepDefaultSignal, { id: "step", weak: true });
|
|
652
702
|
```
|
|
653
703
|
|
|
654
|
-
|
|
655
|
-
|
|
704
|
+
The pruning follows it too, so both addresses stay clean on the step they open
|
|
705
|
+
on — `/alerts/create` on "when", `/alerts/W-123/edit` on "recap" — and a step
|
|
706
|
+
someone actually chose survives the default moving under it.
|
|
656
707
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
708
|
+
Two containers on one screen are two signals, and that is the whole answer to
|
|
709
|
+
"which one owns the param": the one holding the route's signal. A gallery of
|
|
710
|
+
seven wizards side by side hands each of them a `useSignal` of its own, and
|
|
711
|
+
nothing goes into the address.
|
|
712
|
+
|
|
713
|
+
### A state whose values ARE places: `history: "push"`
|
|
714
|
+
|
|
715
|
+
Replacement is the default because most URL-held state qualifies the screen one
|
|
716
|
+
is on. A state whose values are places one came from — the photo being looked at
|
|
717
|
+
in a gallery — says so where the state is declared:
|
|
718
|
+
|
|
719
|
+
```js
|
|
720
|
+
const photoSignal = stateSignal(undefined, { id: "photo", history: "push" });
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
Every write of it then stacks an entry, wherever the write comes from. Except
|
|
724
|
+
where the writer knows this particular move is not one: a slide reached by
|
|
725
|
+
DRAGGING replaces even in a container that pushes, because swiping back and forth
|
|
726
|
+
with a thumb is browsing, not a trail one wants to walk home along. That is said
|
|
727
|
+
at the write rather than declared:
|
|
728
|
+
|
|
729
|
+
```js
|
|
730
|
+
photoSignal.set(nextPhoto, { history: "replace" });
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
`SlideContainer` already does exactly that for its own drags, so a gallery gets
|
|
734
|
+
it by declaring the state and nothing else.
|
|
735
|
+
|
|
736
|
+
### What this is not
|
|
737
|
+
|
|
738
|
+
It is not a route. Nothing is declared for the steps themselves, nothing matches
|
|
739
|
+
on them, and no page transition plays — a transition needs the page to change
|
|
740
|
+
([route_transitions.md](./route_transitions.md)), and a search param moving is
|
|
741
|
+
not one. What travels is the box, and the address is a label on where the box
|
|
742
|
+
stands. A position several parts of the app must react to is still a route.
|
|
661
743
|
|
|
662
744
|
Demo: [../src/layout/demos/8_slide_container_demo.html](../src/layout/demos/8_slide_container_demo.html).
|
package/docs/popup_open.md
CHANGED
|
@@ -97,11 +97,24 @@ This is the same entry point the attributes go through: same target resolution,
|
|
|
97
97
|
same command proxies, same events. The popup stays uncontrolled, and keeps its
|
|
98
98
|
say over closing.
|
|
99
99
|
|
|
100
|
-
`event` is
|
|
101
|
-
|
|
102
|
-
handle focus correctly (which element to give focus back to,
|
|
103
|
-
mousedown's click must be swallowed)
|
|
104
|
-
|
|
100
|
+
`event` is what caused the decision, and it is mandatory — triggering a command
|
|
101
|
+
without one throws. It is chained into the request event, and that chain is what
|
|
102
|
+
lets the popup handle focus correctly (which element to give focus back to,
|
|
103
|
+
whether a mousedown's click must be swallowed) and what the debug panel groups
|
|
104
|
+
the whole sequence under.
|
|
105
|
+
|
|
106
|
+
Almost always there is a DOM event to hand over, because almost always something
|
|
107
|
+
was done to the page. When there genuinely was not — a timer firing, an action
|
|
108
|
+
settling, a signal changing — say so with a `CustomEvent` named after what
|
|
109
|
+
happened rather than leaving the origin unsaid:
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
import { chainEvent, triggerNaviCommand } from "@jsenv/navi";
|
|
113
|
+
|
|
114
|
+
const expiredEvent = new CustomEvent("session_expired");
|
|
115
|
+
chainEvent(expiredEvent, causeEvent); // when something did precede it
|
|
116
|
+
triggerNaviCommand(dialogRef.current, "--navi-open", expiredEvent);
|
|
117
|
+
```
|
|
105
118
|
|
|
106
119
|
## Which element receives the command
|
|
107
120
|
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Bound, not wired
|
|
2
|
+
|
|
3
|
+
Every piece of state navi shows already has a way in and a way out: the value in
|
|
4
|
+
a control, the area a `SlideContainer` stands on, whether a popup is open, where
|
|
5
|
+
the user is in the application. A callback whose whole body copies that state
|
|
6
|
+
into a variable of your own is a second road beside one that exists — and from
|
|
7
|
+
then on there are two owners for one truth.
|
|
8
|
+
|
|
9
|
+
- [The three shapes](#the-three-shapes)
|
|
10
|
+
- [The callback that only remembers](#the-callback-that-only-remembers)
|
|
11
|
+
- [Why it is not merely shorter](#why-it-is-not-merely-shorter)
|
|
12
|
+
- [What is left for the callback](#what-is-left-for-the-callback)
|
|
13
|
+
- [What binds itself](#what-binds-itself)
|
|
14
|
+
- [When there is no binding](#when-there-is-no-binding)
|
|
15
|
+
|
|
16
|
+
## The three shapes
|
|
17
|
+
|
|
18
|
+
Almost everything an application has to say about a component is one of three
|
|
19
|
+
things, and each has its own prop:
|
|
20
|
+
|
|
21
|
+
| what the code has to say | how it is said |
|
|
22
|
+
| --------------------------------------------- | ------------------------------------------ |
|
|
23
|
+
| this state is the app's, both ways | `signal` |
|
|
24
|
+
| something HAPPENS when it changes | `action` (`uiAction` for what cannot fail) |
|
|
25
|
+
| a press asks something of a component near it | `command` + `commandFor` |
|
|
26
|
+
|
|
27
|
+
Reaching for a handler that is none of the three — an `onClick`, an
|
|
28
|
+
`onCurrentChange`, a `uiAction` writing a signal — is the sign that one of them
|
|
29
|
+
was missed.
|
|
30
|
+
|
|
31
|
+
## The callback that only remembers
|
|
32
|
+
|
|
33
|
+
The same mistake, in four places:
|
|
34
|
+
|
|
35
|
+
```jsx
|
|
36
|
+
// ✗ the app is told where the slides went, and writes it back down
|
|
37
|
+
<SlideContainer
|
|
38
|
+
current={step}
|
|
39
|
+
onCurrentChange={(area) => {
|
|
40
|
+
stepSignal.value = area;
|
|
41
|
+
}}
|
|
42
|
+
/>
|
|
43
|
+
// ✓
|
|
44
|
+
<SlideContainer signal={stepSignal} />
|
|
45
|
+
|
|
46
|
+
// ✗ the control is told what it now holds, and the app writes it back down
|
|
47
|
+
<Picker value={side} uiAction={(value) => (sideSignal.value = value)} />
|
|
48
|
+
// ✓
|
|
49
|
+
<Picker signal={sideSignal} />
|
|
50
|
+
|
|
51
|
+
// ✗ a button proposing a value to a control, straight into the signal
|
|
52
|
+
<Button onClick={() => (durationSignal.value = ninetyMinutes)}>1h30</Button>
|
|
53
|
+
// ✓
|
|
54
|
+
<Button command="--navi-update" commandFor="duration" value={ninetyMinutes}>
|
|
55
|
+
1h30
|
|
56
|
+
</Button>
|
|
57
|
+
|
|
58
|
+
// ✗ the popup is told it opened, and the app writes it back down
|
|
59
|
+
<Dialog
|
|
60
|
+
open={isOpen}
|
|
61
|
+
onOpen={() => (openSignal.value = true)}
|
|
62
|
+
onClose={() => (openSignal.value = false)}
|
|
63
|
+
/>
|
|
64
|
+
// ✓
|
|
65
|
+
<Dialog signal={openSignal} />
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
In each pair the second line is not a tidier spelling of the first: it is the
|
|
69
|
+
binding the component already implements, and the first is a partial
|
|
70
|
+
re-implementation of it.
|
|
71
|
+
|
|
72
|
+
## Why it is not merely shorter
|
|
73
|
+
|
|
74
|
+
A binding is written by **everything that moves the state**, not only by the
|
|
75
|
+
gesture the caller thought of. A `SlideContainer`'s signal is written by a drag,
|
|
76
|
+
by an arrow key, by a `--navi-right` command sent from a chevron outside the
|
|
77
|
+
box, by the address when the signal is a route's own, by a step refusing to be
|
|
78
|
+
left. A control's
|
|
79
|
+
signal is written by the user typing, by a group placing its children, by a
|
|
80
|
+
picker committing on close, by a `--navi-update` from a shortcut button. A
|
|
81
|
+
hand-written callback catches the cases it was written for and silently misses
|
|
82
|
+
the rest — and the drift only shows on the screen where a second gesture was
|
|
83
|
+
added.
|
|
84
|
+
|
|
85
|
+
It reads in the other direction too, which no callback gives at all: writing the
|
|
86
|
+
signal MOVES the component. The wheels roll, the slides travel, the popup opens.
|
|
87
|
+
That is what replaces the `key` or the `value` prop pushed down to force a
|
|
88
|
+
component into a state.
|
|
89
|
+
|
|
90
|
+
And it goes through navi's interaction gate. A `command` on a read-only,
|
|
91
|
+
disabled or busy control is refused and says why; an `onClick` doing the same
|
|
92
|
+
write is plain DOM, fires on a greyed-out button and rewrites a value nobody was
|
|
93
|
+
allowed to change.
|
|
94
|
+
|
|
95
|
+
## What is left for the callback
|
|
96
|
+
|
|
97
|
+
Binding the state does not remove the callback — it removes one job from it:
|
|
98
|
+
|
|
99
|
+
- **`onCurrentChange` with a `signal` bound still fires**, and is still worth
|
|
100
|
+
writing: `cause` says whether the slide was pressed, dragged, or asked for by
|
|
101
|
+
the address — which is what tells a history push from a replace — and
|
|
102
|
+
returning `false` REFUSES the travel. Neither of those is state.
|
|
103
|
+
- **`uiAction`** is for what moves with the value without being it: logging,
|
|
104
|
+
something else on screen following along. Not "remember it".
|
|
105
|
+
- **`action`** is for what can fail or take time — it carries the busy state and
|
|
106
|
+
the error, which a `uiAction` does not (see
|
|
107
|
+
[actions.md](./actions.md#action-or-uiaction)).
|
|
108
|
+
- **`onClick`** is for imperative work with nothing to send, nothing to open and
|
|
109
|
+
nothing to propose.
|
|
110
|
+
|
|
111
|
+
## What binds itself
|
|
112
|
+
|
|
113
|
+
| the state | how it binds | where it is written up |
|
|
114
|
+
| --------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
115
|
+
| a control's value | `signal` (every control, every group) | [control_value.md](./control_value.md) |
|
|
116
|
+
| the area a `SlideContainer` is on | `signal` (a route's search param signal puts it in the URL) | its JSDoc, [navigation.md](./navigation.md#a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from) |
|
|
117
|
+
| a popup being open | `signal`, `open`, `navState` | [popup_open.md](./popup_open.md) |
|
|
118
|
+
| where the user is | route + search-param signals | [navigation.md](./navigation.md) |
|
|
119
|
+
| a value proposed to a control | `command="--navi-update"` | [control_value.md](./control_value.md#a-button-that-proposes-a-value-is---navi-update) |
|
|
120
|
+
| the state of an async run | an `action`, read by `useAsyncData` | [actions.md](./actions.md) |
|
|
121
|
+
|
|
122
|
+
## When there is no binding
|
|
123
|
+
|
|
124
|
+
Sometimes the state being wired by hand has no prop for it. That is not a
|
|
125
|
+
licence to wire it — it is a finding, and it is one of two:
|
|
126
|
+
|
|
127
|
+
- the state is already held somewhere and the app is keeping a parallel copy of
|
|
128
|
+
it (the URL, the control itself, an action's own state) — the fix is to read
|
|
129
|
+
it where it lives;
|
|
130
|
+
- or navi is genuinely missing a binding, and should gain one.
|
|
131
|
+
|
|
132
|
+
Say which, rather than leaving a hand-wired callback behind: it is the version
|
|
133
|
+
the next screen copies, and by then it is the app's pattern.
|
|
134
|
+
|
|
135
|
+
## See also
|
|
136
|
+
|
|
137
|
+
- [control_value.md](./control_value.md) — who holds a control's value, and what
|
|
138
|
+
a bound signal holds control by control
|
|
139
|
+
- [actions.md](./actions.md#action-or-uiaction) — `action` or `uiAction`
|
|
140
|
+
- [popup_open.md](./popup_open.md) — who owns the fact that a popup is open
|
|
141
|
+
- [interactions.md](./interactions.md) — a gesture is named, not read by hand:
|
|
142
|
+
the same rule, one layer down
|