@livenetworks/ashlar 1.4.1 → 1.6.0
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/README.md +189 -177
- package/js/COMPONENTS.md +55 -49
- package/js/index.js +22 -5
- package/js/ln-accordion/README.md +186 -137
- package/js/ln-accordion/ln-accordion.js +1 -1
- package/js/ln-ajax/README.md +73 -4
- package/js/ln-ajax/ln-ajax.js +1 -1
- package/js/ln-ajax/src/ln-ajax.js +13 -13
- package/js/ln-api-connector/README.md +261 -150
- package/js/ln-api-connector/ln-api-connector.js +1 -1
- package/js/ln-api-connector/src/ln-api-connector.js +368 -265
- package/js/ln-api-queue/README.md +219 -0
- package/js/ln-api-queue/ln-api-queue.js +1 -0
- package/js/ln-api-queue/src/ln-api-queue.js +320 -0
- package/js/ln-api-queue/src/queue-storage.js +455 -0
- package/js/ln-autoresize/README.md +47 -0
- package/js/ln-autoresize/ln-autoresize.js +1 -1
- package/js/ln-autosave/README.md +33 -1
- package/js/ln-autosave/ln-autosave.js +1 -1
- package/js/ln-autosave/src/ln-autosave.js +4 -4
- package/js/ln-circular-progress/README.md +27 -13
- package/js/ln-circular-progress/ln-circular-progress.js +1 -1
- package/js/ln-circular-progress/src/ln-circular-progress.js +18 -5
- package/js/ln-confirm/README.md +162 -86
- package/js/ln-confirm/_ln-confirm.scss +11 -0
- package/js/ln-confirm/ln-confirm.js +1 -1
- package/js/ln-confirm/src/ln-confirm.js +77 -41
- package/js/ln-core/README.md +896 -0
- package/js/ln-core/hash.js +92 -0
- package/js/ln-core/helpers.js +337 -8
- package/js/ln-core/index.js +4 -2
- package/js/ln-core/persist.js +29 -3
- package/js/ln-core/positioning.js +1 -35
- package/js/ln-core/window-cache.js +213 -0
- package/js/ln-couchdb-connector/README.md +177 -156
- package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
- package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +398 -348
- package/js/ln-data-coordinator/README.md +536 -165
- package/js/ln-data-coordinator/ln-data-coordinator.js +1 -1
- package/js/ln-data-coordinator/src/data-read-policy.js +18 -0
- package/js/ln-data-coordinator/src/ln-data-coordinator.js +1042 -249
- package/js/ln-data-coordinator/src/mutation-receipts.js +46 -0
- package/js/ln-data-store/README.md +241 -94
- package/js/ln-data-store/ln-data-store.js +1 -1
- package/js/ln-data-store/src/ln-data-store.js +694 -699
- package/js/ln-date/README.md +39 -1
- package/js/ln-date/ln-date-dev.scss +14 -0
- package/js/ln-date/ln-date.js +1 -1
- package/js/ln-date/src/ln-date.js +236 -178
- package/js/ln-debug/README.md +58 -0
- package/js/ln-debug/ln-debug.js +1 -0
- package/js/ln-debug/src/ln-debug.js +19 -0
- package/js/ln-dropdown/README.md +31 -8
- package/js/ln-dropdown/ln-dropdown.js +1 -1
- package/js/ln-dropdown/src/ln-dropdown.js +30 -13
- package/js/ln-editor/README.md +274 -0
- package/js/ln-editor/ln-editor.js +1 -0
- package/js/ln-editor/ln-editor.scss +43 -0
- package/js/ln-editor/src/ln-editor.js +619 -0
- package/js/ln-external-links/README.md +44 -5
- package/js/ln-external-links/ln-external-links.js +1 -1
- package/js/ln-fill/README.md +199 -0
- package/js/ln-fill/ln-fill.js +1 -0
- package/js/ln-fill/src/ln-fill.js +87 -0
- package/js/ln-filter/README.md +189 -99
- package/js/ln-filter/ln-filter-dev.scss +8 -0
- package/js/ln-filter/ln-filter.js +1 -1
- package/js/ln-filter/src/ln-filter.js +22 -7
- package/js/ln-form/README.md +212 -49
- package/js/ln-form/ln-form.js +1 -1
- package/js/ln-form/src/ln-form.js +99 -199
- package/js/ln-http/README.md +125 -89
- package/js/ln-http/ln-http.js +1 -1
- package/js/ln-http/src/ln-http.js +32 -13
- package/js/ln-icon/README.md +130 -0
- package/js/ln-icon/ln-icon.js +1 -0
- package/js/{ln-icons/src/ln-icons.js → ln-icon/src/ln-icon.js} +20 -15
- package/js/ln-link/README.md +47 -9
- package/js/ln-link/ln-link.js +1 -1
- package/js/ln-link/src/ln-link.js +22 -6
- package/js/ln-list/README.md +129 -0
- package/js/ln-list/ln-list.js +1 -0
- package/js/ln-list/ln-list.scss +28 -0
- package/js/ln-list/src/ln-list.js +1216 -0
- package/js/ln-modal/README.md +185 -154
- package/js/ln-modal/ln-modal-dev.scss +13 -0
- package/js/ln-modal/ln-modal.js +1 -1
- package/js/ln-modal/ln-modal.scss +28 -4
- package/js/ln-modal/src/ln-modal.js +137 -201
- package/js/ln-modal-coordinator/README.md +99 -0
- package/js/ln-modal-coordinator/ln-modal-coordinator-dev.scss +8 -0
- package/js/ln-modal-coordinator/ln-modal-coordinator.js +1 -0
- package/js/ln-modal-coordinator/src/ln-modal-coordinator.js +345 -0
- package/js/ln-nav/README.md +46 -3
- package/js/ln-nav/ln-nav.js +1 -1
- package/js/ln-nav/src/ln-nav.js +92 -127
- package/js/ln-number/README.md +28 -1
- package/js/ln-number/ln-number-dev.scss +13 -0
- package/js/ln-number/ln-number.js +1 -1
- package/js/ln-number/src/ln-number.js +443 -350
- package/js/ln-options/README.md +66 -0
- package/js/ln-options/ln-options.js +1 -0
- package/js/ln-options/src/ln-options.js +74 -0
- package/js/ln-popover/README.md +40 -5
- package/js/ln-popover/ln-popover.js +1 -1
- package/js/ln-popover/src/ln-popover.js +38 -11
- package/js/ln-progress/README.md +413 -442
- package/js/ln-progress/ln-progress.js +1 -1
- package/js/ln-progress/src/ln-progress.js +98 -150
- package/js/ln-router/README.md +432 -0
- package/js/ln-router/ln-router.js +1 -0
- package/js/ln-router/src/ln-router.js +539 -0
- package/js/ln-search/README.md +158 -83
- package/js/ln-search/ln-search-dev.scss +8 -0
- package/js/ln-search/ln-search.js +1 -1
- package/js/ln-search/src/ln-search.js +12 -9
- package/js/ln-slug/README.md +110 -0
- package/js/ln-slug/ln-slug.js +1 -0
- package/js/ln-slug/src/ln-slug.js +74 -0
- package/js/ln-sort/README.md +170 -0
- package/js/ln-sort/ln-sort-dev.scss +8 -0
- package/js/ln-sort/ln-sort.js +1 -0
- package/js/ln-sort/src/ln-sort.js +178 -0
- package/js/ln-sortable/README.md +133 -95
- package/js/ln-sortable/ln-sortable.js +1 -1
- package/js/ln-stat/README.md +55 -0
- package/js/ln-stat/ln-stat.js +1 -0
- package/js/ln-stat/src/ln-stat.js +55 -0
- package/js/ln-table/README.md +285 -101
- package/js/ln-table/ln-table-dev.scss +17 -0
- package/js/ln-table/ln-table.js +1 -1
- package/js/ln-table/ln-table.scss +1 -1
- package/js/ln-table/src/ln-table.js +1493 -473
- package/js/ln-table-coordinator/README.md +73 -0
- package/js/ln-table-coordinator/ln-table-coordinator.js +1 -0
- package/js/ln-table-coordinator/src/ln-table-coordinator.js +160 -0
- package/js/ln-tabs/README.md +221 -137
- package/js/ln-tabs/ln-tabs-dev.scss +18 -0
- package/js/ln-tabs/ln-tabs.js +1 -1
- package/js/ln-tabs/src/ln-tabs.js +41 -24
- package/js/ln-time/README.md +14 -0
- package/js/ln-time/ln-time.js +1 -1
- package/js/ln-time/src/ln-time.js +223 -192
- package/js/ln-toast/README.md +178 -122
- package/js/ln-toast/ln-toast.js +1 -15
- package/js/ln-toast/src/ln-toast.js +232 -210
- package/js/ln-toast/template.html +16 -12
- package/js/ln-toggle/README.md +61 -2
- package/js/ln-toggle/ln-toggle-dev.scss +13 -0
- package/js/ln-toggle/ln-toggle.js +1 -1
- package/js/ln-toggle/src/ln-toggle.js +56 -43
- package/js/ln-tooltip/README.md +95 -58
- package/js/ln-tooltip/ln-tooltip-dev.scss +9 -0
- package/js/ln-tooltip/ln-tooltip.js +1 -1
- package/js/ln-tooltip/src/ln-tooltip.js +45 -6
- package/js/ln-translations/README.md +118 -96
- package/js/ln-translations/ln-translations.js +1 -1
- package/js/ln-translations/src/ln-translations.js +10 -5
- package/js/ln-upload/README.md +85 -10
- package/js/ln-upload/ln-upload.js +1 -1
- package/js/ln-upload/src/ln-upload.js +26 -10
- package/js/ln-validate/README.md +72 -0
- package/js/ln-validate/ln-validate-dev.scss +15 -0
- package/js/ln-validate/ln-validate.js +1 -1
- package/js/ln-validate/src/ln-validate.js +64 -0
- package/package.json +61 -55
- package/scss/base/_global.scss +2 -2
- package/scss/base/_typography.scss +1 -2
- package/scss/components/_date.scss +1 -0
- package/scss/components/_dropdown.scss +0 -5
- package/scss/components/_editor.scss +5 -0
- package/scss/components/_filter.scss +26 -0
- package/scss/components/_form.scss +7 -0
- package/scss/components/_ln-table.scss +14 -5
- package/scss/components/_sort.scss +10 -0
- package/scss/components/_table.scss +68 -1
- package/scss/components/_toast.scss +68 -41
- package/scss/components/_toggle.scss +16 -0
- package/scss/config/_density.scss +205 -117
- package/scss/config/_icons.scss +10 -9
- package/scss/config/_tokens.scss +69 -47
- package/scss/config/mixins/_accordion.scss +2 -7
- package/scss/config/mixins/_alert.scss +4 -4
- package/scss/config/mixins/_breadcrumbs.scss +2 -2
- package/scss/config/mixins/_btn.scss +1 -1
- package/scss/config/mixins/_card.scss +89 -14
- package/scss/config/mixins/_collapsible.scss +3 -0
- package/scss/config/mixins/_colors.scss +1 -1
- package/scss/config/mixins/_diagnostics.scss +48 -0
- package/scss/config/mixins/_display.scss +1 -0
- package/scss/config/mixins/_dropdown.scss +3 -5
- package/scss/config/mixins/_editor.scss +124 -0
- package/scss/config/mixins/_empty-state.scss +7 -0
- package/scss/config/mixins/_filter.scss +47 -0
- package/scss/config/mixins/_footer.scss +1 -2
- package/scss/config/mixins/_form.scss +207 -21
- package/scss/config/mixins/_index.scss +4 -1
- package/scss/config/mixins/_ln-table.scss +51 -93
- package/scss/config/mixins/_modal.scss +149 -66
- package/scss/config/mixins/_nav.scss +3 -4
- package/scss/config/mixins/_popover.scss +39 -0
- package/scss/config/mixins/_sidebar.scss +1 -1
- package/scss/config/mixins/_sort.scss +42 -0
- package/scss/config/mixins/_table.scss +232 -38
- package/scss/config/mixins/_toast.scss +179 -152
- package/scss/config/mixins/_tooltip.scss +17 -7
- package/scss/config/mixins/_typography.scss +4 -2
- package/scss/ln-ashlar-dev.scss +39 -0
- package/scss/ln-ashlar.scss +8 -2
- package/js/ln-data-table/README.md +0 -110
- package/js/ln-data-table/ln-data-table.js +0 -1
- package/js/ln-data-table/ln-data-table.scss +0 -10
- package/js/ln-data-table/src/ln-data-table.js +0 -1103
- package/js/ln-icons/README.md +0 -88
- package/js/ln-icons/ln-icons.js +0 -1
- package/js/ln-table/ln-table-sort.js +0 -1
- package/js/ln-table/src/ln-table-sort.js +0 -168
- package/scss/components/_data-table.scss +0 -83
- package/scss/config/mixins/_data-table.scss +0 -346
|
@@ -1,165 +1,536 @@
|
|
|
1
|
-
# `data-ln-data-coordinator`
|
|
2
|
-
|
|
3
|
-
A zero-dependency, Local-First **Data Coordinator** component that
|
|
4
|
-
|
|
5
|
-
This component monitors its DOM subtree, intercepts events, and coordinates the
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
##
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
1
|
+
# `data-ln-data-coordinator`
|
|
2
|
+
|
|
3
|
+
A zero-dependency, Local-First **Data Coordinator** component that orchestrates the full 3-Tier Data Layer in `ln-ashlar`: it bridges the local cache store to remote connectors **and** delivers live data to bound view components (tables, lists, selects, stat counters) with zero application JavaScript.
|
|
4
|
+
|
|
5
|
+
This component monitors its DOM subtree, intercepts events, and coordinates the lifecycle between a **Local Storage Cache** (`data-ln-data-store`) and any **Transport Gateway** (`data-ln-*-connector`). It also listens on `document` for view-binding requests and refreshes all bound view elements on every store mutation.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Declarative View Binding
|
|
10
|
+
|
|
11
|
+
Add these attributes to view elements to bind them to this coordinator's child store. No app JS needed.
|
|
12
|
+
|
|
13
|
+
| Attribute | Applied To | Description |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `data-ln-table-store="<storeName>"` | `[data-ln-table]` | Binds a table — receives `ln-table:set-data` automatically. |
|
|
16
|
+
| `data-ln-list-store="<storeName>"` | `[data-ln-list]` | Binds a list — receives `ln-list:set-data` automatically. |
|
|
17
|
+
| `data-ln-options="<storeName>"` | `<select>` | Populated by `ln-options` via `ln-options:set-data`. |
|
|
18
|
+
| `data-ln-stat="<storeName>"` | Inline element | Receives count via `ln-stat:set-count`. |
|
|
19
|
+
|
|
20
|
+
`<storeName>` = the value of `data-ln-data-store` on the coordinator's child store. Multiple coordinators on one page each serve only their own store — isolated by the `_ownsStore(name)` guard.
|
|
21
|
+
|
|
22
|
+
### Presenter / Binder Split
|
|
23
|
+
|
|
24
|
+
- **Presenters** (`store.setPresenters({computed})`) — registered on the store; computed display fields flow through `getAll → set-data` automatically.
|
|
25
|
+
- **Binder** (this coordinator) — delivers already-decorated records without knowing their shape.
|
|
26
|
+
|
|
27
|
+
Use `setPresenters` for fields like `updated_display`, `size_display`, `status_label`. The binder delivers them as-is.
|
|
28
|
+
|
|
29
|
+
### Store-Change Refresh
|
|
30
|
+
|
|
31
|
+
The coordinator listens on `this.dom` for `ln-data-store:ready`, `loaded`, `created`, `updated`, `deleted`, and `synced` (only when `changed`). On any of these, `_refreshAll()` re-queries all bound view elements using their last cached query parameters.
|
|
32
|
+
|
|
33
|
+
### Zero-JS Example
|
|
34
|
+
|
|
35
|
+
```html
|
|
36
|
+
<ul data-ln-data-coordinator hidden>
|
|
37
|
+
<li data-ln-data-store="people"></li>
|
|
38
|
+
<li data-ln-api-connector data-ln-api-endpoint="/api/people"></li>
|
|
39
|
+
</ul>
|
|
40
|
+
|
|
41
|
+
<section data-ln-table="people"
|
|
42
|
+
data-ln-table-source="people"
|
|
43
|
+
data-ln-table-store="people"
|
|
44
|
+
id="people-table">
|
|
45
|
+
<table>
|
|
46
|
+
<thead>
|
|
47
|
+
<tr>
|
|
48
|
+
<th data-ln-table-col="name">
|
|
49
|
+
Name
|
|
50
|
+
<ul data-ln-sort="people-table" data-ln-sort-field="name" data-ln-sort-state="none">
|
|
51
|
+
<li><button type="button" data-ln-sort-dir="asc" aria-label="Sort ascending"><svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-arrows-sort"></use></svg></button></li>
|
|
52
|
+
<li><button type="button" data-ln-sort-dir="desc" aria-label="Sort descending"><svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-arrow-up"></use></svg></button></li>
|
|
53
|
+
<li><button type="button" data-ln-sort-dir="none" aria-label="Remove sort"><svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-arrow-down"></use></svg></button></li>
|
|
54
|
+
</ul>
|
|
55
|
+
</th>
|
|
56
|
+
<th data-ln-table-col="status">Status <button data-ln-table-col-filter …></button></th>
|
|
57
|
+
</tr>
|
|
58
|
+
</thead>
|
|
59
|
+
<tbody data-ln-table-body></tbody>
|
|
60
|
+
</table>
|
|
61
|
+
<template data-ln-template="people-row">
|
|
62
|
+
<tr data-ln-table-row>
|
|
63
|
+
<td>{{ name }}</td>
|
|
64
|
+
<td>{{ status_display }}</td>
|
|
65
|
+
</tr>
|
|
66
|
+
</template>
|
|
67
|
+
</section>
|
|
68
|
+
|
|
69
|
+
<select data-ln-options="people" data-ln-options-value="id" data-ln-options-label="name">
|
|
70
|
+
<option value="">All</option>
|
|
71
|
+
</select>
|
|
72
|
+
|
|
73
|
+
<strong data-ln-stat="people" data-ln-stat-filter="status:active"></strong> active
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
No `<script>` block needed. The coordinator wires everything.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 🧭 The 3-Tier Data Layer Anatomy
|
|
81
|
+
|
|
82
|
+
The coordinator acts as a parent wrapper enclosing the database cache and transport connector:
|
|
83
|
+
|
|
84
|
+
```html
|
|
85
|
+
<ul data-ln-data-coordinator="documents"
|
|
86
|
+
data-ln-data-mapper="documents" hidden>
|
|
87
|
+
|
|
88
|
+
<!-- Tier 1: Local Cache Database (IndexedDB - pure and network-blind) -->
|
|
89
|
+
<li data-ln-data-store
|
|
90
|
+
data-ln-data-store-indexes="status,updated_at">
|
|
91
|
+
</li>
|
|
92
|
+
|
|
93
|
+
<!-- Tier 2: Transport Gateway (API / REST Connector) -->
|
|
94
|
+
<li data-ln-api-connector
|
|
95
|
+
data-ln-api-base-url="https://api.livenetworks.com/v1"
|
|
96
|
+
data-ln-api-path="/documents">
|
|
97
|
+
</li>
|
|
98
|
+
</ul>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## ⚙️ Attributes
|
|
104
|
+
|
|
105
|
+
| Attribute | Category | Description |
|
|
106
|
+
|-----------|----------|-------------|
|
|
107
|
+
| `data-ln-data-coordinator` | Selector | Creates the coordinator instance. The value acts as the domain/scope name (e.g. `documents`). |
|
|
108
|
+
| `data-ln-data-mapper` | Mapping | Reference to an externally registered data mapper name (e.g. `documents`). |
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 🔄 Dynamic Child & Mapper Discovery
|
|
113
|
+
|
|
114
|
+
The coordinator is built to be highly dynamic, reacting to runtime modifications in its DOM subtree.
|
|
115
|
+
|
|
116
|
+
1. **Child Discovery**: The coordinator automatically locates its child components by querying its DOM subtree:
|
|
117
|
+
* **Store Cache**: Looks for `[data-ln-data-store]` and accesses `el.lnDataStore || el.lnStore`.
|
|
118
|
+
* **Transport Connector**: Looks for any connector selector (`[data-ln-api-connector]`, `[data-ln-couchdb-connector]`, `[data-ln-websocket-connector]`, `[data-ln-rest-connector]`) and accesses the universal alias `el.lnConnector`.
|
|
119
|
+
* **Offline Outbox (optional Child 3)**: Looks for `[data-ln-api-queue]` and accesses `el.lnApiQueue`. When present, write routing (below) enqueues instead of calling the connector directly. When absent, behavior is byte-for-byte the direct-connector path described in this document.
|
|
120
|
+
|
|
121
|
+
2. **Mapper Resolution**: The coordinator resolves mapping functions using two strategies:
|
|
122
|
+
* **Inline Script (Highly Encapsulated)**: Looks for a nested `<script type="application/javascript" data-ln-mapper>` tag in its subtree:
|
|
123
|
+
```html
|
|
124
|
+
<script type="application/javascript" data-ln-mapper>
|
|
125
|
+
({
|
|
126
|
+
ingress(serverRaw) {
|
|
127
|
+
return {
|
|
128
|
+
id: serverRaw.id,
|
|
129
|
+
title: serverRaw.title,
|
|
130
|
+
status: serverRaw.status,
|
|
131
|
+
updated_at: Date.parse(serverRaw.updated_at) / 1000
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
egress(localDb) {
|
|
135
|
+
return {
|
|
136
|
+
title: localDb.title,
|
|
137
|
+
status: localDb.status
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
</script>
|
|
142
|
+
```
|
|
143
|
+
* **External Registry (Reusability)**: If no inline script exists, it reads the `data-ln-data-mapper` attribute and looks up the mapper via `window.lnCore.getDataMapper(name)`.
|
|
144
|
+
* **Fallback**: Defaults to a safe no-op mapper: `{ ingress: r => r, egress: r => r }`.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## ⚡ The Event Loop Orchestration
|
|
149
|
+
|
|
150
|
+
Because all events dispatched by the child components bubble up, the coordinator listens directly on its own DOM boundary. It manages the following flows seamlessly:
|
|
151
|
+
|
|
152
|
+
### 0. Form Write Intake (native `submit`, claimed via `preventDefault()`)
|
|
153
|
+
|
|
154
|
+
The coordinator listens for the native `submit` event on `document`
|
|
155
|
+
(bubble phase — never capture, so `ln-validate`'s own validation gate on
|
|
156
|
+
the form itself always runs first). On every submit bubbling through:
|
|
157
|
+
|
|
158
|
+
1. `if (e.defaultPrevented) return` — either `ln-validate`'s validation
|
|
159
|
+
gate blocked an invalid submit, or another coordinator already claimed it.
|
|
160
|
+
2. Reads `data-ln-form-scope` off `e.target` (the form). Absent → the
|
|
161
|
+
form never opted in; leave the native submit alone.
|
|
162
|
+
3. Claims it if either holds: scope value `=== this._name` (named
|
|
163
|
+
override), or the scope is empty and the form is a DOM descendant of
|
|
164
|
+
this coordinator (containment) — identical matching rules as before.
|
|
165
|
+
4. Resolves the effective method itself (hidden `_method` input if
|
|
166
|
+
present and non-empty, else `form.method`) — a literal read, no
|
|
167
|
+
fallback, mirroring `ln-validate`'s own gate exactly.
|
|
168
|
+
5. Methods other than `POST`/`PUT`/`PATCH` are left untouched — the
|
|
169
|
+
native submit proceeds (e.g. a `GET` search form nested in the
|
|
170
|
+
coordinator).
|
|
171
|
+
6. Only now does it call `e.preventDefault()` — this IS the claim, there
|
|
172
|
+
is no separate `claimed` flag to set.
|
|
173
|
+
7. Serializes the form itself (`serializeForm`, stripping `_method`/`_token`)
|
|
174
|
+
and translates the raw `{ action, method, data }` into the same
|
|
175
|
+
fan-out call as before — `POST` → `_fanOutCreate`; `PUT`/`PATCH` →
|
|
176
|
+
`_fanOutUpdate` (reading `id`/`expected_version` off `data`). The
|
|
177
|
+
coordinator generates the `tempId` itself for a create (no correlation
|
|
178
|
+
map needed — see Fan-Out below). The form's current `action` attribute
|
|
179
|
+
(`form.getAttribute('action')`) rides straight through as the
|
|
180
|
+
mutation's transport `url` — no base/resolved distinction.
|
|
181
|
+
|
|
182
|
+
**Unclaimed scoped forms** (wrong scope name, or not contained in any
|
|
183
|
+
coordinator) fall through as an ordinary native submit — no console
|
|
184
|
+
warning, no silent JS interception. This is the progressive-enhancement
|
|
185
|
+
fallback.
|
|
186
|
+
|
|
187
|
+
### Coordinator-Namespaced Intake Events
|
|
188
|
+
|
|
189
|
+
The same fan-out is reachable directly (no form involved) via four
|
|
190
|
+
events dispatched on the coordinator's own element:
|
|
191
|
+
|
|
192
|
+
| Event | `detail` |
|
|
193
|
+
|---|---|
|
|
194
|
+
| `ln-data-coordinator:request-create` | `{ data, action? }` |
|
|
195
|
+
| `ln-data-coordinator:request-update` | `{ id, data, expected_version?, action? }` |
|
|
196
|
+
| `ln-data-coordinator:request-delete` | `{ id }` |
|
|
197
|
+
| `ln-data-coordinator:request-bulk-delete` | `{ ids }` |
|
|
198
|
+
|
|
199
|
+
These are intentionally namespaced under the coordinator (not
|
|
200
|
+
`ln-data-store:request-*`) so there is no naming collision with the events the
|
|
201
|
+
store dispatches — no loop-guard logic is needed.
|
|
202
|
+
|
|
203
|
+
### 1. Parallel Fan-Out — the core write model
|
|
204
|
+
|
|
205
|
+
Every write intake (form or coordinator-namespaced event) reaches one of
|
|
206
|
+
four `_fanOut*` methods. Each one does **two things from the same
|
|
207
|
+
synchronous handler**, independently — the local store write is never
|
|
208
|
+
gated on the remote call succeeding, and vice versa:
|
|
209
|
+
|
|
210
|
+
```mermaid
|
|
211
|
+
sequenceDiagram
|
|
212
|
+
participant Intake as ln-form / ln-data-coordinator:request-*
|
|
213
|
+
participant Coord as data-ln-data-coordinator
|
|
214
|
+
participant Store as data-ln-data-store
|
|
215
|
+
participant Gateway as data-ln-*-connector / ln-api-queue
|
|
216
|
+
|
|
217
|
+
Intake->>Coord: _fanOutCreate(children, data, action)
|
|
218
|
+
par Local (always)
|
|
219
|
+
Coord->>Store: Event: ln-data-store:request-create { tempId, data }
|
|
220
|
+
Store-->>Coord: Event: ln-data-store:created (bubbles, refreshes bound views)
|
|
221
|
+
and Remote (only if connector or queue child present)
|
|
222
|
+
alt queue present
|
|
223
|
+
Coord->>Gateway: Event: ln-api-queue:request-enqueue { chainKey:tempId, op:'create', payload:egress(data), meta:{tempId,action} }
|
|
224
|
+
else connector present, no queue
|
|
225
|
+
Coord->>Gateway: Event: ln-api-connector:request-create { data:egress(data), url:action, meta:{tempId} }
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The only guard on all four write paths is `!children.storeEl` (console
|
|
231
|
+
warning + no-op). There is no `!children.connector` guard — **a
|
|
232
|
+
store-only coordinator (no connector, no queue child) is a valid,
|
|
233
|
+
supported offline-only setup**: the fan-out dispatches the local
|
|
234
|
+
`ln-data-store:request-*` event and stops there. No remote call, no toast; the
|
|
235
|
+
record simply keeps its `_temp_`-prefixed id until a connector is added
|
|
236
|
+
later.
|
|
237
|
+
|
|
238
|
+
### 2. Sync (`ln-data-store:request-remote-sync`) — unchanged
|
|
239
|
+
|
|
240
|
+
Triggered when the store cache boots up or detects stale data. This is
|
|
241
|
+
the one `request-remote-*` event that still exists — sync has no
|
|
242
|
+
per-record reconciliation, so it was never part of the fan-out/no-pending
|
|
243
|
+
refactor:
|
|
244
|
+
|
|
245
|
+
```mermaid
|
|
246
|
+
sequenceDiagram
|
|
247
|
+
participant Store as data-ln-data-store
|
|
248
|
+
participant Coord as data-ln-data-coordinator
|
|
249
|
+
participant Gateway as data-ln-*-connector
|
|
250
|
+
|
|
251
|
+
Store->>Coord: Event: ln-data-store:request-remote-sync (since)
|
|
252
|
+
Coord->>Gateway: Event: ln-api-connector:request-sync { since, meta }
|
|
253
|
+
Gateway-->>Coord: Event: ln-api-connector:fetched { data, meta }
|
|
254
|
+
Note over Coord: Applies Ingress Mapper:<br/>mapper.ingress(serverRawRecord)
|
|
255
|
+
Coord->>Store: JS API: store.applySync(normalizedData, deletedIds, syncedAt)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### 3. Server Confirmation — ordinary update, id-swap on create
|
|
259
|
+
|
|
260
|
+
There is no `confirmMutation`/`revertMutation`/`resolveConflict` anymore.
|
|
261
|
+
A server response is reconciled with **an ordinary
|
|
262
|
+
`ln-data-store:request-update`** — for a create, the target `id` is the
|
|
263
|
+
`tempId` and the incoming `data.id` is the real server id, which the store
|
|
264
|
+
detects as an id-swap (rekey) automatically:
|
|
265
|
+
|
|
266
|
+
```mermaid
|
|
267
|
+
sequenceDiagram
|
|
268
|
+
participant Gateway as data-ln-*-connector
|
|
269
|
+
participant Coord as data-ln-data-coordinator
|
|
270
|
+
participant Store as data-ln-data-store
|
|
271
|
+
|
|
272
|
+
Gateway-->>Coord: Event: ln-api-connector:created { record, message, meta:{tempId} }
|
|
273
|
+
Note over Coord: mapper.ingress(record)
|
|
274
|
+
Coord->>Store: Event: ln-data-store:request-update { id: meta.tempId, data: serverRecord }
|
|
275
|
+
Note over Store: data.id !== tempId -> rekey (id-swap), fires ln-data-store:updated
|
|
276
|
+
Coord->>Coord: _toastFromMessage(message) — success toast if the server sent one
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Update/delete/bulk-delete confirmations follow the same shape (ordinary
|
|
280
|
+
`ln-data-store:request-update`/no reconciliation needed for delete since the
|
|
281
|
+
optimistic delete already applied) — see "Error reconciliation policy"
|
|
282
|
+
below for what happens when the server instead responds with an error.
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## 📥 Offline Outbox Write Routing (optional `ln-api-queue` child)
|
|
287
|
+
|
|
288
|
+
When a `[data-ln-api-queue]` child is present, every fan-out write
|
|
289
|
+
(`create`/`update`/`delete`/`bulk-delete`) is routed through it instead of
|
|
290
|
+
calling the connector directly. When it is absent, the direct-connector
|
|
291
|
+
flow above applies unchanged. Presence is evaluated per-write inside each
|
|
292
|
+
`_fanOut*` method (`children.queue` takes priority over `children.connector`).
|
|
293
|
+
|
|
294
|
+
**Queue-present enqueue payloads:**
|
|
295
|
+
|
|
296
|
+
| op | `ln-api-queue:request-enqueue` detail |
|
|
297
|
+
|---|---|
|
|
298
|
+
| create | `{ chainKey: tempId, op:'create', targetId:null, payload: egress(data), expectedVersion:null, meta:{ tempId, action } }` |
|
|
299
|
+
| update | `{ chainKey: id, op:'update', targetId: id, payload: egress(data), expectedVersion, meta:{ id, action } }` |
|
|
300
|
+
| delete | `{ chainKey: id, op:'delete', targetId: id, payload:null, expectedVersion:null, meta:{ id } }` |
|
|
301
|
+
| bulk-delete | `{ chainKey: bulkKey, op:'bulk-delete', targetId:null, payload:{ ids }, expectedVersion:null, meta:{ bulkKey, ids } }` |
|
|
302
|
+
|
|
303
|
+
The store is **not** reconciled at enqueue time — reconciliation happens
|
|
304
|
+
only when the queue later commands the coordinator to send.
|
|
305
|
+
|
|
306
|
+
### Transport executor — `ln-api-queue:send`
|
|
307
|
+
|
|
308
|
+
The queue entry's opaque `meta.action` (the form's resource URL,
|
|
309
|
+
persisted across sessions) now rides into the connector request's `url`
|
|
310
|
+
field at send time; the connector executes the write against it, joined
|
|
311
|
+
with `data-ln-api-base-url` exactly like the `path` fallback.
|
|
312
|
+
|
|
313
|
+
The coordinator listens for `ln-api-queue:send` on the queue element. It
|
|
314
|
+
maps `op` to the matching connector `:request-*` event (same egress/ingress flow as the
|
|
315
|
+
direct path) and, on resolution, drives both the store and the queue via the
|
|
316
|
+
ordinary `ln-data-store:request-update`/`ln-data-store:request-delete` reconciliation
|
|
317
|
+
described above (no `confirmMutation`):
|
|
318
|
+
|
|
319
|
+
| op | success → store | success → queue command |
|
|
320
|
+
|---|---|---|
|
|
321
|
+
| create | Correlated `ln-data-store:request-update { id: meta.tempId, data: ingress(record), requestId }` (id-swap); wait for matching `updated` | `resolve-create {entryId, oldKey:meta.tempId, newId:record.id}` (atomic remap + ack) |
|
|
322
|
+
| update | Correlated `ln-data-store:request-update { id: meta.id, data: ingress(record), requestId }`; wait for matching `updated` | `ack {entryId}` |
|
|
323
|
+
| delete | — (optimistic delete already applied, no reconciliation) | `ack {entryId}` |
|
|
324
|
+
| bulk-delete | — (optimistic delete already applied, no reconciliation) | `ack {entryId}` |
|
|
325
|
+
|
|
326
|
+
After create remap, the queue entry's `targetId` is the authoritative transport
|
|
327
|
+
identity. The coordinator does not fall back to a possibly stale `meta.id`.
|
|
328
|
+
|
|
329
|
+
> Remap also rewrites `meta.action` if it contains the old key as a
|
|
330
|
+
> substring (string replace) — keeps a persisted per-record URL in sync
|
|
331
|
+
> after a create resolves. See `js/ln-api-queue/README.md`.
|
|
332
|
+
|
|
333
|
+
Every success path also calls `_toastFromMessage(e.detail.message)` — see
|
|
334
|
+
Toasts below.
|
|
335
|
+
|
|
336
|
+
Create resolution waits for the correlated store update before advancing the
|
|
337
|
+
outbox. `resolve-create` then deletes the create entry and remaps all queued
|
|
338
|
+
siblings in one IndexedDB transaction, so no sibling can advance with the
|
|
339
|
+
stale temp id.
|
|
340
|
+
|
|
341
|
+
Correlated mutations use one coordinator-owned receipt registry keyed by
|
|
342
|
+
`requestId`. Store post-events settle the matching receipt through the normal
|
|
343
|
+
bubbling event protocol. There are no per-request listeners or fixed timeouts:
|
|
344
|
+
a slow but successful IndexedDB transaction can still acknowledge its queue
|
|
345
|
+
entry, while coordinator teardown rejects all outstanding receipts.
|
|
346
|
+
|
|
347
|
+
Every queued transport request carries `Idempotency-Key: <entryId>` through
|
|
348
|
+
the connector. Delivery remains at-least-once after a crash; a server that
|
|
349
|
+
honours this key can make the mutation effect exactly-once.
|
|
350
|
+
|
|
351
|
+
**On error**, see "Error reconciliation policy" below — the determinism
|
|
352
|
+
classification (auth / transient / deterministic) applies identically on
|
|
353
|
+
the queued and non-queued paths; only the queue command (`nack` reason)
|
|
354
|
+
differs.
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## 🧭 Local-only mode (no connector child)
|
|
359
|
+
|
|
360
|
+
A coordinator wrapping **only** a `[data-ln-data-store]` child — no
|
|
361
|
+
connector, no queue — is a first-class, supported configuration, not a
|
|
362
|
+
degraded fallback:
|
|
363
|
+
|
|
364
|
+
```html
|
|
365
|
+
<ul data-ln-data-coordinator="drafts" hidden>
|
|
366
|
+
<li data-ln-data-store="drafts"></li>
|
|
367
|
+
</ul>
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Every `_fanOut*` method checks `children.queue` then `children.connector`
|
|
371
|
+
and simply **stops after the local dispatch** if neither exists. Writes
|
|
372
|
+
still go through the coordinator's intake (form or
|
|
373
|
+
`ln-data-coordinator:request-*`), still hit `ln-data-store:request-create`/
|
|
374
|
+
`request-update`/`request-delete`/`request-bulk-delete`, and still refresh
|
|
375
|
+
bound views — there is just no outbound network call and no toast. This is
|
|
376
|
+
the same code path a connector-equipped coordinator uses; adding a
|
|
377
|
+
connector later requires no change to the write intake side.
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## ⚠️ Error reconciliation policy
|
|
382
|
+
|
|
383
|
+
`connError` classifies every non-2xx connector response into exactly one
|
|
384
|
+
of three buckets by `detail.status` (never a fourth "unknown" bucket —
|
|
385
|
+
`status === 0` is treated as transient, the same as a 5xx):
|
|
386
|
+
|
|
387
|
+
| Bucket | Status | Store action | Queue action (if queued) | Toast |
|
|
388
|
+
|---|---|---|---|---|
|
|
389
|
+
| **Auth** | `401` / `419` | none — optimistic write stays | `nack {reason:'auth'}` (pauses that scope) | `auth` dict key |
|
|
390
|
+
| **Transient** | `0` (network) / `5xx` | none — optimistic write stays; never deleted | `nack {reason:'retry'}` (backoff ladder) | none immediately; on **terminal** failure (`ln-api-queue:failed`) → `network` dict key. Non-queued: `network` dict key fires immediately (single attempt already spent). |
|
|
391
|
+
| **Deterministic** | `409` (update) / other `4xx` / `3xx` | `409` update: ordinary `ln-data-store:request-update` with the server's `remote` record (server wins). `create`: `ln-data-store:request-delete` for the temp id (server rejected it outright). Other 4xx update/delete/bulk: left as-is, next sync reconciles. | `nack {reason:'drop'}` (never retried) | `conflict` (409 update) or `rejected` (everything else deterministic) |
|
|
392
|
+
|
|
393
|
+
**Never retried** = deterministic errors always `nack 'drop'` on the queued
|
|
394
|
+
path — a 4xx is never re-attempted, only sync eventually catches up.
|
|
395
|
+
**Never silently discards a local write on a transient/network failure** —
|
|
396
|
+
that is the core "no-pending" invariant: an optimistic write is only ever
|
|
397
|
+
removed by an explicit server rejection (create 4xx) or a genuine local
|
|
398
|
+
`ln-data-store:request-delete`, never by a retry-exhausted network error.
|
|
399
|
+
|
|
400
|
+
There is no `ln-data-store:sync-conflict` event and no `forceSync()` error
|
|
401
|
+
backstop — both were removed as part of this refactor (no grep-verified
|
|
402
|
+
consumer existed for either).
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## 🔔 Toasts
|
|
407
|
+
|
|
408
|
+
The `ln-store-notify` component has been removed. Toasts now come from two
|
|
409
|
+
independent sources, both funneled through the standard `ln-toast:enqueue`
|
|
410
|
+
window event (consumed by `ln-toast` if present on the page, silently
|
|
411
|
+
ignored otherwise):
|
|
412
|
+
|
|
413
|
+
1. **Success — from the server's response envelope.** When a connector
|
|
414
|
+
mutation response includes a `message` (see the
|
|
415
|
+
[ln-api-connector README](../ln-api-connector/README.md) → Mutation
|
|
416
|
+
Response Envelope), the coordinator's `_toastFromMessage(message)`
|
|
417
|
+
enqueues it verbatim (`type`/`title`/`body` from the server, defaulting
|
|
418
|
+
`type` to `'success'`). **No `message` → no toast.** This mirrors
|
|
419
|
+
`ln-ajax`'s existing `data.message` → `ln-toast:enqueue` precedent
|
|
420
|
+
exactly.
|
|
421
|
+
2. **Error — from a coordinator markup dictionary.** Error text is never
|
|
422
|
+
hardcoded in JS. It is authored once per coordinator instance via
|
|
423
|
+
`data-ln-data-coordinator-dict` child elements (parsed with the same
|
|
424
|
+
`buildDict()` helper `ln-upload` uses for its dictionary):
|
|
425
|
+
|
|
426
|
+
```html
|
|
427
|
+
<ul data-ln-data-coordinator="documents" hidden>
|
|
428
|
+
<li data-ln-data-store="documents"></li>
|
|
429
|
+
<li data-ln-api-connector data-ln-api-base-url="/api" data-ln-api-path="/documents"></li>
|
|
430
|
+
|
|
431
|
+
<!-- consumed once at init, then removed from the DOM -->
|
|
432
|
+
<span data-ln-data-coordinator-dict="auth" hidden>Your session expired — please sign in again.</span>
|
|
433
|
+
<span data-ln-data-coordinator-dict="network" hidden>Could not reach the server — your change is saved locally and will retry.</span>
|
|
434
|
+
<span data-ln-data-coordinator-dict="conflict" hidden>Someone else updated this record — showing their version.</span>
|
|
435
|
+
<span data-ln-data-coordinator-dict="rejected" hidden>The server rejected that change.</span>
|
|
436
|
+
</ul>
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Keys are exactly the four buckets from the error reconciliation table:
|
|
440
|
+
`auth`, `network`, `conflict`, `rejected`. **A missing key is silent —
|
|
441
|
+
no fallback string is ever synthesized.** If the coordinator has no
|
|
442
|
+
dictionary at all, `this._dict` is `{}` and every `_toastFromDict` call
|
|
443
|
+
is a no-op.
|
|
444
|
+
|
|
445
|
+
### Sync ownership
|
|
446
|
+
|
|
447
|
+
The coordinator — not the store — decides WHEN to sync:
|
|
448
|
+
|
|
449
|
+
* Listens for `ln-data-store:initialized` on its store child: if
|
|
450
|
+
`!detail.hasCache` → `store.forceSync()` (initial load); else if the
|
|
451
|
+
store is stale → `store.forceSync()`.
|
|
452
|
+
* **Race guard.** `ln-data-store:initialized` can fire before the coordinator
|
|
453
|
+
finishes binding (async `_initStore`, SPA-injected subtree). When
|
|
454
|
+
children resolve, if `store.isLoaded` is already true, the coordinator
|
|
455
|
+
evaluates the same condition directly from instance state instead of
|
|
456
|
+
relying on the event alone.
|
|
457
|
+
* A module-level singleton wires **one shared** `window 'online'`,
|
|
458
|
+
`window 'offline'`, and `document 'visibilitychange'` listener set across
|
|
459
|
+
every coordinator instance on the page (not one per instance). On
|
|
460
|
+
`online`, dispatches `ln-data-store:online` on `document` once, then for each
|
|
461
|
+
coordinator with a loaded, non-syncing store: `store.forceSync()`. On
|
|
462
|
+
`offline`, dispatches `ln-data-store:offline` on `document` once. On
|
|
463
|
+
`visibilitychange` (tab visible again), for each coordinator whose store
|
|
464
|
+
is stale: `store.forceSync()`.
|
|
465
|
+
|
|
466
|
+
If IndexedDB initialization fails, the coordinator does not start a cache sync.
|
|
467
|
+
The store exposes the failure as `initializationError`, and connector-backed
|
|
468
|
+
view reads use the remote source directly.
|
|
469
|
+
|
|
470
|
+
**New attributes:**
|
|
471
|
+
|
|
472
|
+
| Attribute | Description |
|
|
473
|
+
|---|---|
|
|
474
|
+
| `data-ln-data-coordinator-stale` | Seconds threshold before the store is considered stale; falls back to the store's own `data-ln-data-store-stale`; default 300; `-1` / `never` = never stale. |
|
|
475
|
+
| `data-ln-data-coordinator-no-autosync` | Presence opts the coordinator out of online/visibility auto-sync; falls back to the store's own `data-ln-data-store-no-autosync`. |
|
|
476
|
+
|
|
477
|
+
`ln-data-store:online` / `ln-data-store:offline` now require a coordinator on the
|
|
478
|
+
page — the store itself no longer dispatches them (see the
|
|
479
|
+
[ln-data-store README](../ln-data-store/README.md)).
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## 💡 JS API (On the element)
|
|
484
|
+
|
|
485
|
+
Access the coordinator instance programmatically via the `lnDataCoordinator` or `lnCoordinator` properties:
|
|
486
|
+
|
|
487
|
+
```javascript
|
|
488
|
+
const coordinator = document.querySelector('[data-ln-data-coordinator="documents"]').lnDataCoordinator;
|
|
489
|
+
|
|
490
|
+
// Force a remote configuration refresh
|
|
491
|
+
coordinator.refreshConfig();
|
|
492
|
+
|
|
493
|
+
// Retrieve children objects (queue is null when no [data-ln-api-queue] child exists)
|
|
494
|
+
const { store, connector, queue } = coordinator.findChildren();
|
|
495
|
+
|
|
496
|
+
// Fetch currently resolved mapper functions
|
|
497
|
+
const { ingress, egress } = coordinator.mapper;
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
## 🔧 Internals — View-Binding Layer
|
|
503
|
+
|
|
504
|
+
Source: `js/ln-data-coordinator/src/ln-data-coordinator.js`. The write pipeline (fan-out, sync, queue, toasts) is documented above; this section covers the coordinator's second role — serving live data to bound view components with zero application JS.
|
|
505
|
+
|
|
506
|
+
### Binding attributes
|
|
507
|
+
|
|
508
|
+
| Attribute | Applied to | Served by |
|
|
509
|
+
|---|---|---|
|
|
510
|
+
| `data-ln-table-store="<name>"` | `[data-ln-table]` | `ln-table:set-data` |
|
|
511
|
+
| `data-ln-list-store="<name>"` | `[data-ln-list]` | `ln-list:set-data` |
|
|
512
|
+
| `data-ln-options="<name>"` | `<select>` | `ln-options:set-data` |
|
|
513
|
+
| `data-ln-stat="<name>"` | inline element | `ln-stat:set-count` |
|
|
514
|
+
|
|
515
|
+
`<name>` must match the coordinator's own child store's `data-ln-data-store` value. Every document-level handler guards with `_ownsStore(name)` first (`children.store && children.store._name === name`), so multiple coordinators on one page coexist without cross-talk.
|
|
516
|
+
|
|
517
|
+
### Presenter / binder split
|
|
518
|
+
|
|
519
|
+
Presenters (`store.setPresenters({computed})`) are registered once per store; computed display fields (`updated_display`, `status_label`, …) flow through `getAll → set-data` automatically. The coordinator is a pure binder — it transports whatever `getAll` produces to the requesting view element without transforming it.
|
|
520
|
+
|
|
521
|
+
The binder awaits `store.ready` before choosing a source. Loaded non-windowed
|
|
522
|
+
reads use the local store; an empty local-only store is also a valid source and
|
|
523
|
+
returns `[]`. Windowed reads, connector-backed stores without a loaded cache,
|
|
524
|
+
and stores with `initializationError` route remote. When IndexedDB is
|
|
525
|
+
unavailable, table/list/options/stat responses are delivered directly to the
|
|
526
|
+
requesting view instead of being forced through the failed store. Query
|
|
527
|
+
failures clear loading and emit
|
|
528
|
+
`ln-data-coordinator:error {operation:'query', kind, store, target, error}`.
|
|
529
|
+
|
|
530
|
+
### Refresh trigger
|
|
531
|
+
|
|
532
|
+
On `ln-data-store:ready`/`loaded`/`created`/`updated`/`deleted`, and `synced` (only when `e.detail.changed`), `_refreshAll()` re-queries every bound view element using its last cached query parameters (or defaults) — so a filter/sort/search a user set on a table survives a background sync.
|
|
533
|
+
|
|
534
|
+
### `data-ln-table-filter-options` — removed
|
|
535
|
+
|
|
536
|
+
Filter options are now static authored markup inside `[data-ln-popover]` blocks (domain-owned enum/lookup), not accumulated from the dataset. See `docs/architecture/reference.md#column-filter-architecture`.
|