@livenetworks/ashlar 1.5.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 +92 -2
- 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 -56
- 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 -25
- package/scss/components/_form.scss +7 -0
- package/scss/components/_ln-table.scss +13 -3
- package/scss/components/_sort.scss +10 -0
- package/scss/components/_table.scss +36 -23
- 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 +11 -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 +3 -5
- package/scss/config/mixins/_footer.scss +1 -2
- package/scss/config/mixins/_form.scss +207 -21
- package/scss/config/mixins/_index.scss +2 -0
- package/scss/config/mixins/_ln-table.scss +41 -2
- 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 +85 -60
- 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 +27 -9
- package/scss/ln-ashlar.scss +7 -1
- 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 -1087
- 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 -156
package/js/ln-form/README.md
CHANGED
|
@@ -1,101 +1,264 @@
|
|
|
1
1
|
# ln-form
|
|
2
2
|
|
|
3
|
-
A
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
A minimal **form manipulation** primitive. `ln-form` does exactly two
|
|
4
|
+
things to a native `<form>`: populate it when an `ln-fill` event delivers
|
|
5
|
+
a record, and rewrite `action` / `_method` for RESTful edit routing.
|
|
6
|
+
Submit is entirely native HTML — `ln-form` does not listen for `submit`
|
|
7
|
+
at all. Validation display and the submit gate are owned entirely by
|
|
8
|
+
[`ln-validate`](../ln-validate/README.md).
|
|
6
9
|
|
|
7
10
|
---
|
|
8
11
|
|
|
9
|
-
##
|
|
12
|
+
## 1. Philosophy
|
|
10
13
|
|
|
11
|
-
1. **
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
1. **Populate, don't orchestrate.** `ln-form` writes values into the form
|
|
15
|
+
on `ln-fill` and dispatches synthetic `input`/`change` events so
|
|
16
|
+
reactive consumers (`ln-validate`, `ln-autoresize`) react in sync. It
|
|
17
|
+
keeps no validation state and no submit-button logic.
|
|
18
|
+
2. **Native submit, always.** Forms MUST carry a real `action` and
|
|
19
|
+
`method` in HTML. Without JavaScript they do a normal native submit.
|
|
20
|
+
PUT/DELETE ride on POST via an auto-ensured hidden `_method` input
|
|
21
|
+
(Laravel method spoofing) — `ln-form` never serializes data, never
|
|
22
|
+
dispatches a submit event, and never listens for `submit` at all.
|
|
23
|
+
3. **Transport is someone else's job.** Ajax interception (if wanted) is
|
|
24
|
+
a separate component's concern — it listens to the native `submit`
|
|
25
|
+
event itself. Validation is owned by the browser's constraint
|
|
26
|
+
validation plus `ln-validate` for field error display and the submit
|
|
27
|
+
gate — a form with at least one `data-ln-validate` field gets
|
|
28
|
+
`novalidate` injected automatically and the first such field attaches
|
|
29
|
+
the form's submit gate; a form with none keeps native validation as
|
|
30
|
+
the default.
|
|
14
31
|
|
|
15
32
|
---
|
|
16
33
|
|
|
17
|
-
##
|
|
34
|
+
## 2. Minimal Blueprint
|
|
18
35
|
|
|
19
36
|
```html
|
|
20
|
-
<form id="user-form" data-ln-form>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<li><button type="submit">Save</button></li>
|
|
34
|
-
</ul>
|
|
37
|
+
<form id="user-form" data-ln-form action="/users" method="post">
|
|
38
|
+
<div class="form-element">
|
|
39
|
+
<label for="username">Username</label>
|
|
40
|
+
<input id="username" name="username" required data-ln-validate>
|
|
41
|
+
<ul data-ln-validate-errors>
|
|
42
|
+
<li hidden data-ln-validate-error="required">Username is required</li>
|
|
43
|
+
</ul>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<ul class="form-actions">
|
|
47
|
+
<li><button type="button" data-ln-modal-close>Cancel</button></li>
|
|
48
|
+
<li><button type="submit">Save</button></li>
|
|
49
|
+
</ul>
|
|
35
50
|
</form>
|
|
36
51
|
```
|
|
37
52
|
|
|
38
53
|
> [!WARNING]
|
|
39
|
-
> Always set `type="button"` on Cancel buttons. Otherwise
|
|
54
|
+
> Always set `type="button"` on Cancel buttons. Otherwise the browser
|
|
55
|
+
> defaults to `type="submit"` and triggers a real submit.
|
|
40
56
|
|
|
41
57
|
---
|
|
42
58
|
|
|
43
|
-
##
|
|
59
|
+
## 3. Declarative API Contract
|
|
44
60
|
|
|
45
61
|
### HTML Attributes
|
|
46
62
|
|
|
47
63
|
| Attribute | Elements | Description |
|
|
48
64
|
| :--- | :--- | :--- |
|
|
49
|
-
| `data-ln-form` | `<form>` | Initializes the coordinator.
|
|
50
|
-
| `data-ln-form-
|
|
51
|
-
| `data-ln-form-
|
|
65
|
+
| `data-ln-form` | `<form>` | Initializes the coordinator. |
|
|
66
|
+
| `data-ln-form-action-edit` | `<form>` | Opt-in RESTful action routing. See below. |
|
|
67
|
+
| `data-ln-form-action-method="PUT"` | `<form>` | Override verb for `_method` (default `PUT`). Requires `data-ln-form-action-edit`. |
|
|
68
|
+
| `data-ln-form-scope` | `<form>` | Opt-in local-first write path. Empty = nearest ancestor `[data-ln-data-coordinator]`. Named = explicit coordinator override. Intercepts submit; incompatible with native/ajax submit on the same form. |
|
|
52
69
|
|
|
53
70
|
### JS API
|
|
54
71
|
|
|
55
|
-
Access the
|
|
72
|
+
Access the instance directly via the `lnForm` property on the form
|
|
73
|
+
element:
|
|
56
74
|
|
|
57
75
|
```javascript
|
|
58
76
|
const form = document.getElementById('user-form');
|
|
59
77
|
|
|
60
|
-
//
|
|
78
|
+
// Bulk populate fields by name (fires synthetic input/change events)
|
|
61
79
|
form.lnForm.fill({ username: 'dalibor', role: 'admin' });
|
|
62
80
|
|
|
63
|
-
//
|
|
64
|
-
form.lnForm.submit();
|
|
65
|
-
|
|
66
|
-
// 3. Clear all fields, reset validity states, and re-enable the fresh guard
|
|
67
|
-
form.lnForm.reset();
|
|
68
|
-
|
|
69
|
-
// 4. Check if all data-ln-validate fields are valid (Boolean getter)
|
|
70
|
-
if (form.lnForm.isValid) { ... }
|
|
71
|
-
|
|
72
|
-
// 5. Clean up listeners and destroy the instance
|
|
81
|
+
// Clean up listeners and destroy the instance
|
|
73
82
|
form.lnForm.destroy();
|
|
74
83
|
```
|
|
75
84
|
|
|
76
85
|
---
|
|
77
86
|
|
|
78
|
-
##
|
|
87
|
+
## 4. DOM Events
|
|
79
88
|
|
|
80
89
|
### Emitted
|
|
81
90
|
|
|
82
91
|
| Event | Bubbles | Payload | Description |
|
|
83
92
|
| :--- | :--- | :--- | :--- |
|
|
84
|
-
| `ln-form:submit` | Yes | `{ data: Object }` | Dispatched with serialized form key-values on valid submission. |
|
|
85
|
-
| `ln-form:reset-complete` | Yes | `{ target: HTMLElement }` | Dispatched after a complete reactive reset cycle. |
|
|
86
93
|
| `ln-form:destroyed` | Yes | `{ target: HTMLElement }` | Dispatched when the coordinator is torn down. |
|
|
87
94
|
|
|
88
95
|
### Received
|
|
89
96
|
|
|
90
97
|
| Event | Payload | Description |
|
|
91
98
|
| :--- | :--- | :--- |
|
|
92
|
-
| `ln-
|
|
93
|
-
|
|
99
|
+
| `ln-fill` | `record \| null` | Canonical fan-out fill. `detail` → `this.fill(detail)` + apply action mode; `null` → native `form.reset()`. Guarded: only handled when `e.target === self.dom`. |
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 5. RESTful action routing (create vs. edit)
|
|
104
|
+
|
|
105
|
+
For Laravel REST backends, `ln-form` can rewrite `form.action` and inject
|
|
106
|
+
a hidden `_method` input driven off the fill record — no coordinator JS,
|
|
107
|
+
no second click listener.
|
|
108
|
+
|
|
109
|
+
**Opt-in:** add `data-ln-form-action-edit` to the `<form>`. Forms without
|
|
110
|
+
this attribute are completely inert — this feature does not exist for
|
|
111
|
+
them.
|
|
112
|
+
|
|
113
|
+
### Attributes
|
|
114
|
+
|
|
115
|
+
| Attribute | On | Description |
|
|
116
|
+
| :--- | :--- | :--- |
|
|
117
|
+
| `data-ln-form-action-edit` | `<form>` | Opt-in. Empty value = RESTful default (`baseAction + '/' + id`). Non-empty value = template with `:id` placeholder. |
|
|
118
|
+
| `data-ln-form-action-edit="/path/:id"` | `<form>` | Template variant — `:id` is replaced with `encodeURIComponent(record.id)`. |
|
|
119
|
+
| `data-ln-form-action-method="PUT"` | `<form>` | HTTP verb written into `_method` on edit. Default: `PUT`. |
|
|
120
|
+
|
|
121
|
+
> The `<input name="_method">` is auto-ensured — do not author it
|
|
122
|
+
> manually. `ln-form` creates it on first fill if absent, or reuses the
|
|
123
|
+
> existing one.
|
|
124
|
+
|
|
125
|
+
### Behavior
|
|
126
|
+
|
|
127
|
+
| Fill state | `form.action` | `<input name="_method">` |
|
|
128
|
+
| :--- | :--- | :--- |
|
|
129
|
+
| Edit (record with `id`) | Rewritten to edit URL | `PUT` (or custom verb) |
|
|
130
|
+
| New (`null` detail) | Restored to base action | `''` (empty — Laravel ignores) |
|
|
131
|
+
|
|
132
|
+
### Examples
|
|
133
|
+
|
|
134
|
+
```html
|
|
135
|
+
<!-- Minimal: baseAction + '/' + id -->
|
|
136
|
+
<form data-ln-form action="/packages" data-ln-form-action-edit>
|
|
137
|
+
|
|
138
|
+
<!-- Custom template -->
|
|
139
|
+
<form data-ln-form action="/packages" data-ln-form-action-edit="/packages/:id">
|
|
140
|
+
|
|
141
|
+
<!-- Custom verb -->
|
|
142
|
+
<form data-ln-form action="/packages"
|
|
143
|
+
data-ln-form-action-edit="/packages/:id"
|
|
144
|
+
data-ln-form-action-method="PATCH">
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
> [!NOTE]
|
|
148
|
+
> This feature only rewrites `action`/`_method`. The actual HTTP request
|
|
149
|
+
> (native submit or ajax) is out of scope for `ln-form`.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 6. What ln-form does NOT do
|
|
154
|
+
|
|
155
|
+
- **No transport.** It does not make XHR/fetch requests. Submit is
|
|
156
|
+
native HTML; ajax interception (if desired) belongs to a separate
|
|
157
|
+
component that listens to the native `submit` event.
|
|
158
|
+
- **No serialization, ever.** There is no `serializeForm` call inside
|
|
159
|
+
`ln-form`, no `ln-form:submit-record` event, no JSON payload — scoped
|
|
160
|
+
forms are serialized by the claiming `ln-data-coordinator`, not by
|
|
161
|
+
`ln-form`.
|
|
162
|
+
- **No validation orchestration, no submit listener at all.** `ln-form`
|
|
163
|
+
never dispatches `ln-validate:request-validate`, never calls
|
|
164
|
+
`preventDefault()`, and never touches `novalidate` — all of that is
|
|
165
|
+
entirely [`ln-validate`](../ln-validate/README.md)'s job (see its
|
|
166
|
+
README §Philosophy). Claiming a valid submit for the write pipeline
|
|
167
|
+
(scoped forms only) is `ln-data-coordinator`'s job (native `submit`,
|
|
168
|
+
bubble phase, its own `preventDefault()`) — `ln-form` plays no role in
|
|
169
|
+
that decision either.
|
|
94
170
|
|
|
95
171
|
---
|
|
96
172
|
|
|
97
|
-
##
|
|
173
|
+
## 5b. Local-first write routing (`data-ln-form-scope`)
|
|
174
|
+
|
|
175
|
+
A form carrying `data-ln-form-scope` becomes the universal write entry
|
|
176
|
+
point for local-first/SPA pages. The `submit` event is intercepted at
|
|
177
|
+
three possible rungs, and whichever rung claims it decides the archetype:
|
|
178
|
+
|
|
179
|
+
1. **Nobody** — native browser submit to `action` (+ `_method`), SSR.
|
|
180
|
+
2. **`ln-ajax`** — fetch to `action`, page stays put (progressive
|
|
181
|
+
enhancement). `ln-ajax` explicitly skips forms carrying
|
|
182
|
+
`data-ln-form-scope` (see the `ln-ajax` README).
|
|
183
|
+
3. **`ln-data-coordinator`** — a form with `data-ln-form-scope` has its
|
|
184
|
+
valid submit left native; `ln-form` never intercepts it beyond the
|
|
185
|
+
validation gate (§7 below). The nearest ancestor
|
|
186
|
+
`[data-ln-data-coordinator]` (or the named coordinator, if
|
|
187
|
+
`data-ln-form-scope="name"`) listens for the native `submit` on
|
|
188
|
+
`document` (bubble phase), claims it with `preventDefault()` once its
|
|
189
|
+
own scope/containment matches, serializes the form itself, and routes
|
|
190
|
+
it through the store → queue → connector write pipeline. See the
|
|
191
|
+
`ln-data-coordinator` README for the intake contract.
|
|
192
|
+
|
|
193
|
+
`ln-validate`'s submit gate runs on every method — GET included, exactly
|
|
194
|
+
like the native browser validation it replaces. A search form nested
|
|
195
|
+
inside a coordinator with invalid `data-ln-validate` fields is blocked
|
|
196
|
+
the same as any other form; a form with no `data-ln-validate` fields has
|
|
197
|
+
no gate at all. `ln-data-coordinator` reads the effective method
|
|
198
|
+
(`_method` input if present and non-empty, else the form's `method`
|
|
199
|
+
attribute) independently, purely to decide whether to claim the native
|
|
200
|
+
submit for the write pipeline (`POST` → create, `PUT`/`PATCH` → update) —
|
|
201
|
+
a `GET` form is never claimed, regardless of validity. `ln-form` itself
|
|
202
|
+
stays coordinator-blind and validation-blind: it never serializes, never
|
|
203
|
+
dispatches a custom event, and never listens for `submit`.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## 7. Common Pitfalls
|
|
208
|
+
|
|
209
|
+
- **Setting `input.value` directly:** Doing `input.value = 'new'` is
|
|
210
|
+
silent in the DOM. Neither validation nor layout systems will detect
|
|
211
|
+
the change. **Always** use `form.lnForm.fill()` or manually dispatch an
|
|
212
|
+
`input`/`change` event.
|
|
213
|
+
- **Expecting a submit event:** `ln-form` never dispatches anything on
|
|
214
|
+
submit — there is no `ln-form:submit` and no `ln-form:submit-record`
|
|
215
|
+
either. Listen to the form's native `submit` event, or the resulting
|
|
216
|
+
`ln-data-store:created`/`ln-data-store:updated` outcome, instead.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## 8. Integration & Source Files
|
|
221
|
+
|
|
222
|
+
- **Unified Bundle**: Loaded automatically with the main bundle:
|
|
223
|
+
```html
|
|
224
|
+
<script src="dist/ln-ashlar.iife.js" defer></script>
|
|
225
|
+
```
|
|
226
|
+
- **Standalone IIFE**: For lightweight pages, load the standalone,
|
|
227
|
+
self-registering IIFE version:
|
|
228
|
+
```html
|
|
229
|
+
<script src="js/ln-form/ln-form.js" defer></script>
|
|
230
|
+
```
|
|
231
|
+
- **Active Source (ESM)**: Development source is located at
|
|
232
|
+
[js/ln-form/src/ln-form.js](file:///c:/laragon/www/ln-ashlar/js/ln-form/src/ln-form.js).
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 🔧 Internals
|
|
237
|
+
|
|
238
|
+
Source: `js/ln-form/ln-form.js`. Instance state is minimal: `dom` (the form) and `_baseAction` (the `action` attribute captured once at init, restored on "new" mode). No validation index, no debounce, no submit-button bookkeeping — `ln-form` does not orchestrate submission.
|
|
239
|
+
|
|
240
|
+
### `fill(data)`
|
|
241
|
+
|
|
242
|
+
Iterates `form.elements` writing by `name`: checkboxes get `.checked = Boolean(value)`, radios match `el.value === String(value)`, `select-multiple` sets `options[n].selected`, everything else sets `.value`. Each written element then dispatches exactly one synthetic event — `change` for `SELECT`/checkbox/radio, `input` otherwise (the `isChangeBased` discriminator). This mirrors the same discriminator `ln-validate` and `ln-autoresize` use for their own listener registration, so every field fires the event those consumers actually listen for — no double fires, no missed reactions.
|
|
243
|
+
|
|
244
|
+
### `ln-fill` handling
|
|
245
|
+
|
|
246
|
+
Guarded to `e.target === self.dom` (ignores bubbled `ln-fill` from nested `[data-ln-fillable]` children). A record calls `fill()` then `_applyActionMode(record)`; `null` calls `form.reset()` — the native `reset` event that fires from that call is what actually restores the base action (see below), not the `ln-fill` handler directly.
|
|
247
|
+
|
|
248
|
+
### Reset routing
|
|
249
|
+
|
|
250
|
+
The native `reset` event — whether from `form.reset()` above, a `<button type="reset">`, or any other trigger — always runs `_applyActionMode(null)`, restoring `_baseAction` and clearing `_method`. There is no synthetic input/change replay on reset and no `ln-form:reset-complete` event.
|
|
251
|
+
|
|
252
|
+
### Action-mode guard
|
|
253
|
+
|
|
254
|
+
`_applyActionMode` exits immediately on forms without `data-ln-form-action-edit` — unrelated forms are untouched by the reset/fill listeners' side effects.
|
|
255
|
+
|
|
256
|
+
### Relation to ln-validate
|
|
257
|
+
|
|
258
|
+
Independent siblings, no imports between them. `ln-validate` injects `novalidate` the moment its first field initializes (authors never write it by hand); a form with zero `data-ln-validate` fields keeps native validation as the default. `ln-validate` has its own separate `reset` listener (clears field error classes / `_touched` state) — unrelated to `ln-form`'s reset listener, which only handles action/`_method`.
|
|
259
|
+
|
|
260
|
+
---
|
|
98
261
|
|
|
99
|
-
|
|
100
|
-
- **
|
|
101
|
-
- **
|
|
262
|
+
## Related
|
|
263
|
+
- **[`ln-validate`](../ln-validate/README.md)** — Field-level validation and error display.
|
|
264
|
+
- **[`ln-fill`](../ln-fill/README.md)** — Fan-out fill event source.
|
package/js/ln-form/ln-form.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(){"use strict";function y(s,t,
|
|
1
|
+
(function(){"use strict";if(typeof window<"u"){const t=console.warn;console.warn=function(...e){typeof e[0]=="string"&&(e[0].startsWith("[ln-")||e[0].startsWith("[lnCore"))&&!(document.documentElement.hasAttribute("data-ln-debug")||document.body&&document.body.hasAttribute("data-ln-debug"))||t.apply(console,e)}}const y={};function M(t,e){y[t]||(y[t]=document.querySelector('[data-ln-template="'+t+'"]'));const s=y[t];return s?s.content.cloneNode(!0):(console.warn("["+(e||"ln-core")+'] Template "'+t+'" not found'),null)}function D(t,e,s){t.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:s||{}}))}function v(t,e){if(!t||!e)return t;const s=t.querySelectorAll("[data-ln-field]");for(let n=0;n<s.length;n++){const i=s[n],l=i.getAttribute("data-ln-field");e[l]!=null&&(i.textContent=e[l])}const d=t.querySelectorAll("[data-ln-attr]");for(let n=0;n<d.length;n++){const i=d[n],l=i.getAttribute("data-ln-attr").split(",");for(let r=0;r<l.length;r++){const u=l[r].trim().split(":");if(u.length!==2)continue;const a=u[0].trim(),f=u[1].trim();e[f]!=null&&i.setAttribute(a,e[f])}}const c=t.querySelectorAll("[data-ln-show]");for(let n=0;n<c.length;n++){const i=c[n],l=i.getAttribute("data-ln-show");l in e&&i.classList.toggle("hidden",!e[l])}const o=t.querySelectorAll("[data-ln-class]");for(let n=0;n<o.length;n++){const i=o[n],l=i.getAttribute("data-ln-class").split(",");for(let r=0;r<l.length;r++){const u=l[r].trim().split(":");if(u.length!==2)continue;const a=u[0].trim(),f=u[1].trim();f in e&&i.classList.toggle(a,!!e[f])}}return t}function q(t,e){t.matches&&t.matches("[data-ln-form], [data-ln-fillable]")&&t.dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));const s=t.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let d=0;d<s.length;d++)s[d].dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));return t}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore._fillBound||(window.lnCore._fillBound=!0,document.addEventListener("ln-fill",function(t){if(!(!t.target.matches||!t.target.matches("[data-ln-fillable]")))if(t.detail)v(t.target,t.detail);else{const e=t.target.querySelectorAll("[data-ln-field]");for(let s=0;s<e.length;s++)e[s].textContent=""}})));function x(t,e){if(!t||!e)return t;const s=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;s.nextNode();){const o=s.currentNode;o.textContent.indexOf("{{")!==-1&&(o.textContent=o.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(n,i){return e[i]!==void 0?e[i]:""}))}const d=function(o,n){return e[n]!==void 0?e[n]:""},c=Array.from(t.querySelectorAll("*"));t.nodeType===1&&c.push(t);for(let o=0;o<c.length;o++){const n=c[o],i=n.attributes;for(let l=0;l<i.length;l++){const r=i[l];r.value.indexOf("{{")!==-1&&n.setAttribute(r.name,r.value.replace(/\{\{\s*(\w+)\s*\}\}/g,d))}}return t}function I(t,e,s,d,c,o){const n={};for(let l=0;l<t.children.length;l++){const r=t.children[l],u=r.getAttribute("data-ln-key");u&&(n[u]=r)}const i=document.createDocumentFragment();for(let l=0;l<e.length;l++){const r=e[l],u=String(d(r));let a=n[u];if(a)c(a,r,l);else{const f=M(s,o);if(!f||(x(f,r),a=f.firstElementChild,!a))continue;a.setAttribute("data-ln-key",u),c(a,r,l)}i.appendChild(a)}t.textContent="",t.appendChild(i)}function _(t,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){_(t,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}t()}function g(t,e,s,d){if(t.nodeType!==1)return;const o=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",n=Array.from(t.querySelectorAll(o));t.matches&&t.matches(o)&&n.push(t);for(const i of n)i[s]||(i[s]=new d(i))}function N(t){if(typeof t!="string")return!!t;const e=t.trim().toLowerCase();return e!=="false"&&e!=="0"&&e!==""&&e!=="off"&&e!=="no"}function F(t,e){const s=t.elements,d=[],c={};for(let o=0;o<s.length;o++){const n=s[o];n.name&&n.type==="checkbox"&&(c[n.name]=(c[n.name]||0)+1)}for(let o=0;o<s.length;o++){const n=s[o];if(n.type==="file"||n.type==="submit"||n.type==="button")continue;const i=n.getAttribute("data-ln-fill-as")||n.name;if(!i||!(i in e))continue;const l=e[i];if(n.type==="checkbox"){if(Array.isArray(l))n.checked=l.indexOf(n.value)!==-1;else if(c[n.name]>1){const r=String(l).split(",").map(function(u){return u.trim()});n.checked=r.indexOf(n.value)!==-1}else n.checked=N(l);d.push(n)}else if(n.type==="radio")n.checked=n.value===String(l),d.push(n);else if(n.type==="select-multiple"){if(Array.isArray(l))for(let r=0;r<n.options.length;r++)n.options[r].selected=l.indexOf(n.options[r].value)!==-1;d.push(n)}else n.value=l,d.push(n)}return d}function R(t,e,s,d,c={}){const o=c.extraAttributes||[],n=c.onAttributeChange||null,i=c.onInit||null;function l(r){const u=r||document.body;g(u,t,e,s),i&&i(u)}return _(function(){const r=new MutationObserver(function(a){for(let f=0;f<a.length;f++){const h=a[f];if(h.type==="childList"){for(let m=0;m<h.addedNodes.length;m++){const p=h.addedNodes[m];p.nodeType===1&&(g(p,t,e,s),i&&i(p))}for(let m=0;m<h.removedNodes.length;m++){const p=h.removedNodes[m];if(p.nodeType===1){const O=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",b=Array.from(p.querySelectorAll(O));p.matches&&p.matches(O)&&b.push(p);for(let C=0;C<b.length;C++){const S=b[C];if(!document.contains(S)){const A=S[e];A&&typeof A.destroy=="function"&&A.destroy()}}}}}else h.type==="attributes"&&(n&&h.target[e]?n(h.target,h.attributeName):(g(h.target,t,e,s),i&&i(h.target)))}});let u=[];if(t.indexOf("[")!==-1){const a=/\[([\w-]+)/g;let f;for(;(f=a.exec(t))!==null;)u.push(f[1])}else u.push(t);r.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:u.concat(o)})},d),window[e]=l,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){l(document.body)}):l(document.body),l}const L={};function j(t,e){L[t]=e}function B(t){return L[t]||{ingress:e=>e,egress:e=>e}}const E={};function k(t,e){if(!t||typeof e!="object")return;const s=t.toLowerCase().split("-")[0];E[s]=e}function W(t){if(!t)return null;const e=t.toLowerCase().split("-")[0];return E[e]||null}k("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=j,window.lnCore.getDataMapper=B,window.lnCore.registerLocaleFallback=k,window.lnCore.getLocaleFallback=W,window.lnCore.fillTemplate=x,window.lnCore.fill=v,window.lnCore.lnFill=q,window.lnCore.renderList=I);function T(t){return(t||"").replace(/^#/,"")}function w(t){const e=t===void 0?location.hash:t,s={},d=T(e);if(!d)return s;const c=d.split("&");for(let o=0;o<c.length;o++){const n=c[o];if(!n)continue;const i=n.indexOf(":"),l=i>-1?n.slice(0,i):n,r=i>-1?n.slice(i+1):"";if(l)try{s[l]=decodeURIComponent(r)}catch{s[l]=r}}return s}function U(t){if(!t)return null;const e=w();return t in e?e[t]:null}function K(t,e){if(!t)return;const s=w();e==null?delete s[t]:s[t]=String(e);const c=Object.keys(s).map(function(o){const n=s[o];return n===""?o:o+":"+encodeURIComponent(n)}).join("&");T(location.hash)!==c&&(location.hash=c)}function G(t){return t.button===1||t.ctrlKey||t.metaKey||t.shiftKey?!1:(t.preventDefault(),!0)}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.hashParse=w,window.lnCore.hashGet=U,window.lnCore.hashSet=K,window.lnCore.hashLinkClick=G),(function(){const t="data-ln-form",e="lnForm",s="data-ln-form-action-edit",d="data-ln-form-action-method";if(window[e]!==void 0)return;function c(o){this.dom=o,this._baseAction=o.getAttribute("action")||"";const n=this;return this._onLnFill=function(i){i.target===n.dom&&(i.detail?(n.fill(i.detail),n._applyActionMode(i.detail)):n.dom.reset())},this._onReset=function(){n._applyActionMode(null)},o.addEventListener("ln-fill",this._onLnFill),o.addEventListener("reset",this._onReset),this}c.prototype.fill=function(o){const n=F(this.dom,o);for(let i=0;i<n.length;i++){const l=n[i],r=l.tagName==="SELECT"||l.type==="checkbox"||l.type==="radio";l.dispatchEvent(new Event(r?"change":"input",{bubbles:!0}))}},c.prototype._ensureMethodInput=function(){let o=this.dom.querySelector('input[name="_method"]');return o||(o=document.createElement("input"),o.type="hidden",o.name="_method",o.value="",this.dom.appendChild(o)),o},c.prototype._applyActionMode=function(o){if(!this.dom.hasAttribute(s))return;const n=o&&o.id!=null&&o.id!==""?o.id:null,i=this._ensureMethodInput();if(n!==null){const l=this.dom.getAttribute(s);l?this.dom.setAttribute("action",l.replace(":id",encodeURIComponent(n))):this.dom.setAttribute("action",this._baseAction.replace(/\/$/,"")+"/"+encodeURIComponent(n)),i.value=this.dom.getAttribute(d)||"PUT"}else this.dom.setAttribute("action",this._baseAction),i.value=""},c.prototype.destroy=function(){this.dom[e]&&(this.dom.removeEventListener("ln-fill",this._onLnFill),this.dom.removeEventListener("reset",this._onReset),D(this.dom,"ln-form:destroyed",{target:this.dom}),delete this.dom[e])},R(t,e,c,"ln-form")})()})();
|