recyclrjs 1.0.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.
@@ -0,0 +1,403 @@
1
+ # GX (RecyclrJS) — HTML Swaps + Stimulus “Singleton” Controller
2
+
3
+ GX is a tiny “server-driven UI” helper: it **fetches HTML**, **parses the response**, **picks out fragments**, and **swaps them into the current DOM** based on `data-gx-*` attributes.
4
+ Think “HTMX-ish”, but intentionally lightweight and designed to work cleanly with Stimulus.
5
+
6
+ This documentation matches the current implementation in:
7
+
8
+ - `index.js` (GX core)
9
+ - `gx-controller.js` (Stimulus controller that owns a single GX instance)
10
+ - `index.blade.php`, `side.blade.php` (example markup patterns)
11
+
12
+ ---
13
+
14
+ ## 1) Mental Model
15
+
16
+ 1. User clicks a link / button, or submits a form.
17
+ 2. Stimulus controller `gx#handleRequest` reads `data-gx-*` attributes.
18
+ 3. The `GX` instance is configured (URL/method/form/select rules) and then `gx.request()` runs.
19
+ 4. `GX.request()`:
20
+ - `fetch()`es the URL
21
+ - reads the HTML as text
22
+ - picks **swap rules** (either from the element’s `data-gx-select` or via presets)
23
+ - extracts fragments from the response using `DOMParser`
24
+ - performs DOM swaps
25
+ - optionally pushes browser history
26
+ - optionally dispatches a custom event (default: `gx:updated`)
27
+
28
+ ---
29
+
30
+ ## 2) Why the GX Stimulus Controller Must Be a Singleton
31
+
32
+ ### The core rule
33
+
34
+ **Only one `GX` instance should exist per page.**
35
+
36
+ Creating a new `GX` instance per click can stack global listeners (especially `popstate`) and can burn CPU/memory (Firefox is extra sensitive here). The patched controller intentionally creates **exactly one** `GX` instance inside `connect()` and reuses it for every request.
37
+
38
+ ✅ Good:
39
+
40
+ - `connect()` creates `this.gx = new GX(...)`
41
+ - `handleRequest()` only updates properties on `this.gx` and calls `this.gx.request()`
42
+
43
+ ❌ Bad:
44
+
45
+ - `handleRequest()` creates `new GX(...)` every time
46
+
47
+ ---
48
+
49
+ ## 3) Quick Start
50
+
51
+ ### 3.1 Add the controller once (usually on `<body>`)
52
+
53
+ ```html
54
+ <body data-controller="gx">
55
+ ...
56
+ </body>
57
+ ```
58
+
59
+ ### 3.2 Trigger a swap with a click
60
+
61
+ ```html
62
+ <a
63
+ href="/patients"
64
+ data-action="click->gx#handleRequest"
65
+ data-gx-select="#content"
66
+ >
67
+ Patients
68
+ </a>
69
+ ```
70
+
71
+ ### 3.3 Trigger a swap with a form submit
72
+
73
+ ```html
74
+ <form
75
+ action="/login"
76
+ method="post"
77
+ data-action="submit->gx#handleRequest"
78
+ data-gx-select="#content #sidebar"
79
+ >
80
+ ...
81
+ </form>
82
+ ```
83
+
84
+ > Tip: Always include the event in `data-action` (`click->` / `submit->`).
85
+ > The shorthand `data-action="gx#handleRequest"` is easy to forget and harder to reason about.
86
+ > By default, the selector for the input is the output, and the default location is outerHTML, so if you find yourself writing #content@outerHTML->#content@outerHTML, you can just write #content
87
+ > Also by default, data-gx-method is get, so you can really slim it down to something like <a href="http://foo.bar" data-gx-select="#content" data-action="click->gx#handleRequest">Hello World</a>
88
+
89
+ ---
90
+
91
+ ## 4) `data-gx-*` Attribute Reference
92
+
93
+ GX reads these attributes from the clicked/submitted element:
94
+
95
+ ### Required
96
+
97
+ - **`data-gx-select`**
98
+ Swap rule string describing what to extract from the response and where to place it in the current DOM.
99
+
100
+ ### Optional
101
+
102
+ - **`data-gx-url`**
103
+ Overrides the URL. If missing, GX will fall back to:
104
+ - `href` for anchors/buttons
105
+ - `action` for forms
106
+
107
+ - **`data-gx-method`**
108
+ `get | post | put` (lowercase preferred). If missing, GX falls back to the element’s `method` attribute or defaults to `get`.
109
+
110
+ - **`data-gx-form`**
111
+ CSS selector for the form whose fields should be submitted. Useful when a button is outside the `<form>`.
112
+
113
+ - **`data-gx-history`**
114
+ `on | off` (defaults to `on` in the controller unless overridden).
115
+ When enabled, GX will `history.pushState()` after a successful request.
116
+
117
+ - **`data-gx-debug`**
118
+ `on | off` (defaults to `off`). Enables verbose debug logging.
119
+
120
+ - **`data-gx-loading`** _(implemented)_
121
+ CSS selector for a spinner element. GX sets `display: block` while the request runs and hides it afterwards.
122
+
123
+ - **`data-gx-presets`** _(advanced)_
124
+ Overrides which presets are available for the request.
125
+
126
+ > Notes on incomplete/placeholder attributes:
127
+ >
128
+ > - `data-gx-disable` and `data-gx-error` currently exist in the controller plumbing, but the GX core does not yet apply them (they’re stored but not acted on).
129
+
130
+ ---
131
+
132
+ ## 5) Swap Rules (`data-gx-select`) Syntax
133
+
134
+ A swap rule has the general form:
135
+
136
+ ```
137
+ <sourceSelector>@<sourceLocation> -> <targetSelector>@<targetLocation>
138
+ ```
139
+
140
+ Rules are separated by spaces.
141
+
142
+ ### 5.1 Minimal form (location defaults to `outerHTML`)
143
+
144
+ ```
145
+ #content
146
+ ```
147
+
148
+ ### 5.2 Full form
149
+
150
+ ```
151
+ #content@outerHTML->#content@outerHTML
152
+ ```
153
+
154
+ ### 5.3 Multiple swaps in one request
155
+
156
+ ```
157
+ #content@outerHTML->#content@outerHTML #sidebar@outerHTML->#sidebar@outerHTML
158
+ ```
159
+
160
+ ### 5.4 Supported target locations
161
+
162
+ These target locations are currently supported by `GX.render()`:
163
+
164
+ - `innerHTML`
165
+ - `outerHTML`
166
+ - `beforebegin`
167
+ - `afterbegin`
168
+ - `beforeend`
169
+ - `afterend`
170
+
171
+ Target insertion uses `insertAdjacentHTML()` for the `before*/after*` variants.
172
+
173
+ ### 5.5 Source “locations” (what you extract from the response)
174
+
175
+ For the **source**, GX reads a property off the response element:
176
+
177
+ ```js
178
+ doc.querySelector(sourceSelector)[sourceLocation];
179
+ ```
180
+
181
+ So you can technically use any string property on an Element (e.g., `innerHTML`, `outerHTML`, `textContent`, `value`), **but** keep in mind:
182
+
183
+ - If your target location is not one of the supported ones (section 5.4), `render()` will throw.
184
+ - In practice, stick to `innerHTML` and `outerHTML`.
185
+
186
+ ### 5.6 Conditional rules (reserved)
187
+
188
+ Rules may be prefixed with:
189
+
190
+ ```
191
+ condition:...
192
+ ```
193
+
194
+ Example:
195
+
196
+ ```
197
+ redirect:#content@outerHTML->#content@outerHTML
198
+ ```
199
+
200
+ Right now, conditions are only used in a limited way internally (ex: redirect/error paths). Treat this as reserved unless you’re extending GX.
201
+
202
+ ### 5.7 Property list syntax (parsed but **not implemented**)
203
+
204
+ The `Location` helper parses bracket syntax like:
205
+
206
+ ```
207
+ #avatar@outerHTML[src,alt]->#avatar@outerHTML
208
+ ```
209
+
210
+ …but the actual “update only these props” logic is not implemented yet. Currently, if brackets are present, GX will short-circuit that rule.
211
+
212
+ ---
213
+
214
+ ## 6) Presets System (Optional, but Powerful)
215
+
216
+ GX can choose swap rules automatically via a “presets” configuration.
217
+
218
+ ### 6.1 How presets are selected
219
+
220
+ `GX.evaluateWithPresets()` will use presets if any of these are true:
221
+
222
+ - Server returns a `Recyclr-Use-Presets` header (comma/semicolon separated list)
223
+ - A trigger is matched:
224
+ - `response.redirected` → `config.triggers.redirect`
225
+ - `response.status` → `config.triggers["status:<code>"]`
226
+
227
+ If no preset is chosen, GX falls back to the request’s `data-gx-select`.
228
+
229
+ ### 6.2 Presets config shape
230
+
231
+ ```js
232
+ {
233
+ presets: {
234
+ refreshContent: [
235
+ "#content@outerHTML->#content@outerHTML",
236
+ "#sidebar@outerHTML->#sidebar@outerHTML"
237
+ ],
238
+
239
+ // You can also provide a rule object:
240
+ toast: { literal: "<div>Saved</div>", dst: "#toasts", dstLoc: "beforeend" }
241
+ },
242
+
243
+ fallback: "refreshContent",
244
+
245
+ triggers: {
246
+ redirect: ["refreshContent"],
247
+ "status:401": ["refreshContent"]
248
+ }
249
+ }
250
+ ```
251
+
252
+ ### 6.3 Loading presets externally + TTL cache
253
+
254
+ If `config.presetsUrl` is set, GX loads presets once and caches them in `localStorage` using `config.presetsTTLMs`.
255
+
256
+ This is meant for “drop-in behavior changes” without redeploying JS.
257
+
258
+ ---
259
+
260
+ ## 7) Browser History / Back-Forward Behavior
261
+
262
+ When history is enabled for a request:
263
+
264
+ - GX calls `history.pushState(null, 'Page Title', this.url)`
265
+
266
+ The Stimulus controller installs **one** `popstate` listener to re-run a GET request and refresh `#content`.
267
+
268
+ If you would rather force a full page reload on back/forward, comment in the `location.reload()` line in the controller.
269
+
270
+ ---
271
+
272
+ ## 8) Custom Events
273
+
274
+ When `dispatch` is enabled:
275
+
276
+ - GX dispatches a `CustomEvent` on `document` (or the provided target)
277
+ - Event name format is:
278
+
279
+ ```
280
+ <identifier>:<eventName>
281
+ ```
282
+
283
+ With the default config, that’s typically:
284
+
285
+ - `gx:updated`
286
+
287
+ You can override the event name from the server via `Recyclr-Event` response header.
288
+
289
+ ### Example listener
290
+
291
+ ```js
292
+ document.addEventListener("gx:updated", (e) => {
293
+ console.log("GX updated:", e.detail);
294
+ });
295
+ ```
296
+
297
+ ---
298
+
299
+ ## 9) Performance & Memory Safety Checklist (Firefox-Friendly)
300
+
301
+ If you hear your laptop fans rev up after lots of navigation/modals, check these first:
302
+
303
+ ### 9.1 Ensure you aren’t appending forever
304
+
305
+ Using target locations like `beforeend` / `afterend` **adds nodes** without removing old ones. That’s fine for “toasts”, but not for “pages”.
306
+
307
+ For pages/modals, prefer:
308
+
309
+ - A fixed container, swapped via `innerHTML` or `outerHTML`
310
+
311
+ ### 9.2 Avoid global listeners in swapped components
312
+
313
+ Stimulus controllers **should** clean up on `disconnect()`, but only if:
314
+
315
+ - you attach listeners in `connect()` and remove them in `disconnect()`
316
+ - you don’t attach listeners to `window/document` without cleanup
317
+
318
+ ### 9.3 Do not create a new GX per request
319
+
320
+ Again: **singleton GX instance**. Creating a new instance per click can stack listeners and retain references.
321
+
322
+ ### 9.4 Cancel in-flight requests (nice-to-have)
323
+
324
+ GX currently doesn’t abort previous requests. If a user clicks rapidly, you can end up doing extra work.
325
+ If this becomes a real problem, add `AbortController` support in `GX.request()`.
326
+
327
+ ---
328
+
329
+ ## 10) Known Limitations / TODOs
330
+
331
+ - `data-gx-disable` is not applied yet (busy-state helper exists, but GX never targets a real element selector).
332
+ - `data-gx-error` is not displayed yet (stored but unused).
333
+ - Bracket property lists (`@outerHTML[src,alt]`) are parsed but not implemented.
334
+ - Target locations are limited to the list in section 5.4.
335
+
336
+ ---
337
+
338
+ ## 11) Troubleshooting
339
+
340
+ ### “Nothing updated”
341
+
342
+ - Check that `data-gx-select` points at selectors that exist in the **response HTML**.
343
+ - Open DevTools → Network → verify response contains the fragment you expect.
344
+
345
+ ### “Target selector not found”
346
+
347
+ - The _target_ selector must exist in the current DOM at the time of swap.
348
+ - If you are swapping the container that contains the trigger, avoid self-destructing the click target mid-flight.
349
+
350
+ ### “Firefox CPU spikes”
351
+
352
+ - Look for repeated `popstate` listeners or duplicated controllers.
353
+ - Ensure modals are swapped/reused, not appended repeatedly.
354
+
355
+ ---
356
+
357
+ ## 12) Examples From Our Blade Templates (Patterns)
358
+
359
+ ### “Swap main content + sidebar”
360
+
361
+ ```html
362
+ <a
363
+ href="/some/page"
364
+ data-action="click->gx#handleRequest"
365
+ data-gx-method="get"
366
+ data-gx-select="#content #side-nav"
367
+ >
368
+ Go
369
+ </a>
370
+ ```
371
+
372
+ ### “Submit a form and refresh key regions”
373
+
374
+ ```html
375
+ <form
376
+ action="/save"
377
+ method="post"
378
+ data-action="submit->gx#handleRequest"
379
+ data-gx-select="#content@outerHTML->#content@outerHTML #alerts@innerHTML->#alerts@innerHTML"
380
+ data-gx-loading="#global-spinner"
381
+ >
382
+ ...
383
+ </form>
384
+ ```
385
+
386
+ ---
387
+
388
+ ## 13) Extending GX (Recommended Next Steps)
389
+
390
+ If you want to tighten up UX and reduce bugs:
391
+
392
+ 1. **Wire `data-gx-disable` into `applyBusyState()`**
393
+ - Use `this.disable` as a selector for the container you want to lock during requests.
394
+
395
+ 2. **Implement bracket property updates**
396
+ - In `GX.evaluate()`, when `Location.properties()` returns a list, update only those properties on the _target element_.
397
+
398
+ 3. **Add AbortController**
399
+ - Store `this._abort` in GX and abort previous in-flight requests on a new request.
400
+
401
+ That combo makes GX feel much closer to “production HTMX” without losing the simplicity.
402
+
403
+ ---
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Christopher Cordine
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # RecyclrJS
2
+
3
+ RecyclrJS exports `GX`, a small browser-side helper for fetching HTML and swapping fragments into the current DOM.
4
+
5
+ Project site: https://rgx.cordine.site
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install recyclrjs
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```js
16
+ const GX = require('recyclrjs');
17
+
18
+ const gx = new GX({
19
+ url: '/patients',
20
+ method: 'get',
21
+ selection: '#content@outerHTML->#content@outerHTML'
22
+ });
23
+
24
+ gx.request();
25
+ ```
26
+
27
+ You can also import the named property:
28
+
29
+ ```js
30
+ const { GX } = require('recyclrjs');
31
+ ```
32
+
33
+ ## Vanilla DOM
34
+
35
+ If you do not want Stimulus, mount the built-in delegated listeners once:
36
+
37
+ ```js
38
+ const Recyclr = require('recyclrjs');
39
+
40
+ Recyclr.mount(document);
41
+ ```
42
+
43
+ Example markup:
44
+
45
+ ```html
46
+ <a href="/patients" data-gx-select="#content" recyclr="click">Patients</a>
47
+
48
+ <form action="/login" data-gx-select="#content" recyclr="submit">
49
+ ...
50
+ </form>
51
+ ```
52
+
53
+ ## Realtime
54
+
55
+ If you already have a `GX` instance, you can feed it updates from WebSocket or SSE messages:
56
+
57
+ ```js
58
+ const Recyclr = require('recyclrjs');
59
+
60
+ const gx = new Recyclr({
61
+ url: '/patients',
62
+ selection: '#content@innerHTML->#content@innerHTML'
63
+ });
64
+
65
+ const stream = Recyclr.createRecyclrStream({
66
+ wsUrl: '/realtime',
67
+ sseUrl: '/realtime',
68
+ gx
69
+ });
70
+
71
+ stream.start();
72
+ ```
73
+
74
+ The realtime payload should include `html`, and can optionally include `presets`, `eventName`, or `rules`.
75
+
76
+ ## Versioning
77
+
78
+ RecyclrJS follows Semantic Versioning:
79
+
80
+ - `major.minor.patch`
81
+ - `patch` for small fixes
82
+ - `minor` for backward-compatible features
83
+ - `major` for breaking changes
84
+
85
+ The repo starts at `1.0.0`. For releases, keep GitHub tags in sync with npm publishes, for example `v1.0.1` or `v1.1.0`.
86
+ Using `npm version patch|minor|major` is the simplest path because it updates `package.json` and creates the matching git tag.
87
+
88
+ ## Notes
89
+
90
+ - This package is CommonJS and targets the browser.
91
+ - `mount()` and `createRecyclrStream()` are the no-Stimulus runtime APIs. The `controllers/` folder remains as an adapter/example, not a runtime requirement.
92
+ - The full implementation notes live in [`GX_DOCUMENTATION.md`](./GX_DOCUMENTATION.md).