litewire 1.0.1 → 1.0.3

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.
Files changed (4) hide show
  1. package/LICENSE +2 -2
  2. package/README.MD +383 -217
  3. package/package.json +11 -10
  4. package/src/litewire.js +0 -261
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Your Name
3
+ Copyright (c) 2026 Zainurrahman
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
21
+ SOFTWARE.
package/README.MD CHANGED
@@ -1,305 +1,471 @@
1
- # Litewire Framework
1
+ # Litewire 1.0.3
2
2
 
3
- ```bash
4
- npm i litewire
5
- ```
3
+ Litewire is a small browser-side HTML-over-the-wire library. It binds HTTP
4
+ requests, reactive state, and event expressions to HTML attributes, swaps
5
+ server-rendered HTML into the page, and can load ES module components on
6
+ demand.
6
7
 
7
- ## Complete Architecture Manual & Reference Guide
8
+ Version 1.0.3 adds reactive `lw:class` bindings, iterable lists with
9
+ `template[lw:for]`, and event-triggered component mounting. It also improves
10
+ radio model initialization and browser-history markup restoration.
8
11
 
9
- A comprehensive technical breakdown of the HTML-driven AJAX engine, dynamic component loader, DOM swapping, state management, and lifecycle events.
12
+ ## Contents
10
13
 
11
- ---
14
+ - [Install and start](#install-and-start)
15
+ - [Configuration](#configuration)
16
+ - [AJAX requests](#ajax-requests)
17
+ - [Reactive state and bindings](#reactive-state-and-bindings)
18
+ - [Lists](#lists)
19
+ - [Dynamic components](#dynamic-components)
20
+ - [HTML swaps and DOM updates](#html-swaps-and-dom-updates)
21
+ - [Browser history](#browser-history)
22
+ - [Lifecycle events](#lifecycle-events)
23
+ - [CSRF](#csrf)
24
+ - [Security considerations](#security-considerations)
25
+ - [API reference](#api-reference)
12
26
 
13
- ## Table of Contents
27
+ ## Install and start
14
28
 
15
- 1. [Architecture Overview & Lifecycle](#1-architecture-overview--lifecycle)
16
- 2. [Core Configuration & Initialization](#2-core-configuration--initialization)
17
- 3. [Attribute Reference API](#3-attribute-reference-api)
18
- 4. [Dynamic ES Module Loader (`lw-component`)](#4-dynamic-es-module-loader-lw-component)
19
- 5. [DOM Swapping & Navigation Mechanisms](#5-dom-swapping--navigation-mechanisms)
20
- 6. [History & State Caching](#6-history--state-caching)
21
- 7. [Event System & Lifecycle Hooks](#7-event-system--lifecycle-hooks)
22
- 8. [CSRF & Security Protocols](#8-csrf--security-protocols)
23
- 9. [Practical Implementation Patterns](#9-practical-implementation-patterns)
29
+ ### Use the browser file
24
30
 
25
- ---
31
+ Download `litewire.js` and load it as an ES module:
26
32
 
27
- ## 1. Architecture Overview & Lifecycle
33
+ ```html
34
+ <script type="module" src="/js/litewire.js"></script>
35
+ ```
28
36
 
29
- **Litewire** is an HTML-first AJAX engine and dynamic component loader designed to build modern, interactive single-page-like applications with minimal client-side JavaScript configuration. By extending standard HTML elements with custom `lw-*` attributes, Litewire automatically intercepts user interactions, initiates asynchronous HTTP requests, updates specific DOM regions, and manages dynamic ES Module lifecycles.
37
+ Litewire creates `window.litewire` when the document is ready, unless that
38
+ global already exists. It scans the page, binds supported attributes, and
39
+ continues scanning elements added later.
30
40
 
31
- > **Core Principles**
32
- > Litewire eliminates the need for full-page reloads and complex SPA frameworks by leveraging declarative markup, dynamic MutationObservers, and native browser features like `fetch()` and `history.pushState()`.
41
+ ### Use the npm package
33
42
 
34
- ### Lifecycle Flow
43
+ Install version 1.0.3 from your configured npm registry:
35
44
 
36
- 1. **Initialization:** The class resolves the application's base URL and attaches DOM observers.
37
- 2. **Document Scanning:** Existing DOM nodes are scanned for request triggers and dynamic components.
38
- 3. **Event Binding:** Intercepts clicks, form submissions, and page load triggers automatically.
39
- 4. **Execution:** Sends non-blocking background HTTP requests with built-in CSRF security headers.
40
- 5. **Swap & Mount:** Replaces target DOM regions with backend HTML and automatically boots dynamic JS components on newly introduced elements.
45
+ ```sh
46
+ npm install litewire@1.0.3
47
+ ```
41
48
 
42
- ---
49
+ Import the package from your application's JavaScript entry point:
43
50
 
44
- ## 2. Core Configuration & Initialization
51
+ ```js
52
+ import "litewire";
53
+ ```
45
54
 
46
- Litewire auto-instantiates as soon as the DOM is ready, exposing its instance globally on `window.litewire`.
55
+ The package module auto-initializes in a browser environment. Do not create a
56
+ second instance unless you intentionally want an additional set of listeners
57
+ and observers.
47
58
 
48
- ### Base URL Resolution Priority
59
+ ### First request
49
60
 
50
- To support deployment in deeply nested directories or custom app sub-paths, Litewire resolves its root URL using a four-tier fallback mechanism:
61
+ ```html
62
+ <button
63
+ lw-get="/hello"
64
+ lw-target="#result"
65
+ >
66
+ Load greeting
67
+ </button>
51
68
 
52
- ```text
53
- 1. Constructor Option: new Litewire({ baseUrl: '[https://example.com/api](https://example.com/api)' })
54
- 2. Meta Tag Element: <meta name="base-url" content="[https://example.com](https://example.com)">
55
- 3. Global App Config: window.AppConfig.baseUrl
56
- 4. Window Location: window.location.origin (Default)
69
+ <div id="result">The response will appear here.</div>
57
70
  ```
58
71
 
59
- ### Instantiation Example
72
+ Litewire sends a GET request when the button is clicked and replaces the
73
+ contents of `#result` with the response HTML.
60
74
 
61
- ```javascript
62
- // Custom manual instantiation with options
63
- const customLitewire = new Litewire({
64
- baseUrl: "[https://api.myproject.com](https://api.myproject.com)",
65
- loadingClass: "is-loading", // Class applied to active triggers and indicators
66
- });
75
+ ## Configuration
76
+
77
+ Litewire resolves its base URL in this order:
78
+
79
+ 1. `baseUrl` constructor option
80
+ 2. `<meta name="base-url" content="...">`
81
+ 3. `window.AppConfig.baseUrl`
82
+ 4. `window.location.origin`
83
+
84
+ The default loading class is `litewire-request`. Configure these values before
85
+ Litewire auto-initializes by setting a meta tag or `window.AppConfig` before
86
+ loading the module:
87
+
88
+ ```html
89
+ <meta name="base-url" content="https://example.com/app">
90
+ <script>
91
+ window.AppConfig = { baseUrl: "https://example.com/app" };
92
+ </script>
93
+ <script type="module" src="/js/litewire.js"></script>
67
94
  ```
68
95
 
69
- ---
96
+ The constructor also accepts options when you instantiate the exported class
97
+ yourself:
70
98
 
71
- ## 3. Attribute Reference API
99
+ ```js
100
+ window.litewire = {};
101
+ const { default: Litewire } = await import("./litewire.js");
102
+
103
+ window.litewire = new Litewire({
104
+ baseUrl: "https://example.com/app",
105
+ loadingClass: "is-loading",
106
+ });
107
+ ```
72
108
 
73
- All core functionalities are driven by attaching `lw-*` attributes directly to standard HTML tags.
109
+ The placeholder prevents the module's browser auto-initializer from creating
110
+ another instance before your configured instance is constructed.
74
111
 
75
- | Attribute | Accepted Values | Default | Functional Description |
76
- | -------------- | --------------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------- |
77
- | `lw-get` | Relative / Absolute Path | None | Executes an HTTP GET request to the specified endpoint. |
78
- | `lw-post` | Relative / Absolute Path | None | Executes an HTTP POST request. Submits form payload if bound to or inside a `<form>`. |
79
- | `lw-put` | Relative / Absolute Path | None | Executes an HTTP PUT request with FormData payload attachment. |
80
- | `lw-delete` | Relative / Absolute Path | None | Executes an HTTP DELETE request. |
81
- | `lw-trigger` | `click`, `submit`, `load`, custom | Contextual | Overrides trigger event. Defaults to `submit` for forms, `click` for interactive tags. `load` fires instantly. |
82
- | `lw-target` | CSS Selector | Self Element | Specifies the container element where the returned HTML response will be rendered. |
83
- | `lw-swap` | `innerHTML`, `outerHTML`, `prepend`, `append` | `innerHTML` | Controls how the received HTML response replaces or integrates into the target element. |
84
- | `lw-indicator` | CSS Selector | `.litewire-indicator` | Specifies target loading elements. Automatically toggles the active `loadingClass` during request execution. |
85
- | `lw-push-url` | `true`, `false`, Custom Path | `false` | Pushes a new state to the browser history, caching DOM contents for native back/forward navigation. |
86
- | `lw-component` | JS File Path | None | Asynchronously imports a JavaScript module and mounts it directly onto the host DOM element. |
112
+ Request and component paths are joined to the resolved base URL. Leading
113
+ slashes are removed from the supplied path before joining.
87
114
 
88
- ---
115
+ ## AJAX requests
89
116
 
90
- ## 4. Dynamic ES Module Loader (`lw-component`)
117
+ Litewire recognizes `lw-get`, `lw-post`, `lw-put`, and `lw-delete`. It binds
118
+ forms to `submit` by default and other elements to `click`. Set `lw-trigger` to
119
+ choose a different event or to run a request when the element is scanned:
91
120
 
92
- Litewire features a client-side component loader capable of fetching and initializing JavaScript ES modules on demand without requiring heavy bundle configurations or static page imports.
121
+ ```html
122
+ <form lw-post="/messages" lw-target="#messages">
123
+ <label>
124
+ Message
125
+ <input name="message" required>
126
+ </label>
127
+ <button type="submit">Send</button>
128
+ </form>
93
129
 
94
- ### Component Mounting Process
130
+ <div id="messages"></div>
131
+ ```
95
132
 
96
- When an element with `lw-component="path/to/module.js"` is injected into the DOM:
133
+ For GET requests, form fields are encoded as URL query parameters. For POST,
134
+ PUT, and DELETE requests, a containing form is sent as `FormData`. If the
135
+ trigger is not in a form, no request body is added.
97
136
 
98
- 1. Litewire resolves the full script path against `this.baseUrl`.
99
- 2. Executes a native dynamic `import(fullPath)` statement.
100
- 3. Extracts component constructors or functions via fallback export resolution:
101
- `module.default` → `module.imageUploader` → `Object.values(module)[0]`
102
- 4. Instantiates the module using `new componentFn(el, params)` or direct function execution.
103
- 5. Passes all `data-*` HTML attributes as an object to the component's `params` argument.
137
+ ```html
138
+ <input
139
+ name="q"
140
+ lw-get="/search"
141
+ lw-trigger="keyup"
142
+ lw-target="#results"
143
+ lw-indicator="#search-status"
144
+ >
145
+ <span id="search-status" class="litewire-indicator">Searching…</span>
146
+ <div id="results"></div>
147
+ ```
104
148
 
105
- ### Component Implementation Example
149
+ When a request starts, Litewire adds the configured loading class to the
150
+ trigger and its indicators. Indicators default to `.litewire-indicator`
151
+ descendants of the trigger; `lw-indicator` can instead specify a document-wide
152
+ CSS selector. Litewire removes the class when the request finishes, whether it
153
+ succeeds or fails.
106
154
 
107
- #### HTML Markup
155
+ Use `lw-trigger="load"` with a request attribute to start a request as soon as
156
+ the element is scanned:
108
157
 
109
158
  ```html
110
- <!-- Target element containing component directives and dataset parameters -->
111
- <div
112
- lw-component="assets/js/uploader.js"
113
- data-max-files="5"
114
- data-allowed-types="image/png,image/jpeg"
115
- ></div>
159
+ <section
160
+ lw-get="/account/summary"
161
+ lw-trigger="load"
162
+ lw-target="#account-summary"
163
+ >
164
+ Loading account summary…
165
+ </section>
166
+ <div id="account-summary"></div>
116
167
  ```
117
168
 
118
- #### JavaScript Module (`assets/js/uploader.js`)
169
+ ## Reactive state and bindings
119
170
 
120
- ```javascript
121
- export default class ImageUploader {
122
- constructor(element, params) {
123
- this.container = element;
124
- this.maxFiles = params.maxFiles || 1;
125
- this.allowedTypes = params.allowedTypes || "*";
171
+ Litewire provides a small reactive layer using `lw:state`, `lw:model`,
172
+ `lw:text`, `lw:show`, `lw:class`, `lw:for`, and event-expression attributes.
173
+ The colon in these attribute names is intentional.
126
174
 
127
- this.render();
128
- }
175
+ ### Local state
129
176
 
130
- render() {
131
- this.container.innerHTML = `
132
- <div class="upload-box">
133
- <p>Upload up to ${this.maxFiles} files (${this.allowedTypes})</p>
134
- <input type="file" multiple accept="${this.allowedTypes}">
135
- </div>
136
- `;
137
- }
138
- }
177
+ Put `lw:state` on an element to create state for that element and its
178
+ descendants. Its value must be a JavaScript expression that evaluates to an
179
+ object:
180
+
181
+ ```html
182
+ <section lw:state="{ count: 0, visible: true }">
183
+ <button lw:click="count++">Count: <span lw:text="count"></span></button>
184
+ <p lw:show="visible">This paragraph can be hidden.</p>
185
+ <button lw:click="visible = !visible">Toggle paragraph</button>
186
+ </section>
139
187
  ```
140
188
 
141
- ---
189
+ Assignments and deletions of top-level state properties trigger rendering of
190
+ bindings on that element and its descendants that belong to that state root.
191
+ State is shallowly reactive:
192
+ mutating a nested object or array in place does not itself trigger a render.
193
+ Replace the top-level property to trigger one, for example
194
+ `settings = { ...settings, enabled: false }`.
142
195
 
143
- ## 5. DOM Swapping & Navigation Mechanisms
196
+ Nested `lw:state` elements establish their own state scope. A binding uses the
197
+ nearest ancestor state root. Bindings outside a state root use Litewire's
198
+ global models instead.
144
199
 
145
- Litewire supports multiple strategies for swapping returned server HTML responses into target elements using the `lw-swap` attribute.
200
+ ### Text and visibility
146
201
 
147
- ### Swap Modes
202
+ - `lw:text="expression"` evaluates the expression and writes its result as
203
+ text. `null` and `undefined` render as an empty string.
204
+ - `lw:show="expression"` sets the element's `hidden` property based on the
205
+ truthiness of the result.
206
+ - `lw:class="{ className: expression }"` adds each class whose expression is
207
+ truthy and removes classes previously managed by this binding when they
208
+ become false. Other classes on the element are left alone; a key may contain
209
+ multiple space-separated class names.
148
210
 
149
- - **`innerHTML` (Default):** Replaces the complete inner HTML content of the target container.
150
- - **`outerHTML`:** Completely replaces the target element itself with the server response.
151
- - **`prepend`:** Inserts the new server HTML snippet right before the first child inside the target element.
152
- - **`append`:** Appends the new server HTML snippet right after the last child inside the target element.
211
+ Expressions are evaluated when the element is first scanned and again when
212
+ their owning local state renders. Global model updates refresh matching
213
+ `lw:text` bindings and reevaluate global `lw:class` bindings and loops.
153
214
 
154
- ### Automated Mutation Monitoring
215
+ ### Lists
155
216
 
156
- Litewire uses an internal `MutationObserver` attached to `document.body`. Whenever new DOM elements are appended via AJAX swaps, standard page renders, or dynamic JS scripts, Litewire automatically scans and binds all newly discovered `lw-*` elements and mounts dynamic `lw-component` directives seamlessly.
217
+ Use `lw:for` on a `<template>` to repeat its contents for each value in an
218
+ iterable. The repeated expressions can access the item name and `$index`;
219
+ `(item, index)` also declares a named index:
157
220
 
158
- ---
221
+ ```html
222
+ <ul lw:state="{ items: ['Apples', 'Pears'] }">
223
+ <template lw:for="(item, index) of items">
224
+ <li>
225
+ <span lw:text="index + 1"></span>.
226
+ <span lw:text="item"></span>
227
+ </li>
228
+ </template>
229
+ </ul>
230
+ ```
159
231
 
160
- ## 6. History & State Caching
232
+ The expression must evaluate to an iterable, such as an array, string, `Set`,
233
+ or `Map`; `null` and `undefined` render no rows. Assign a new array to the
234
+ top-level state property to refresh the list. Litewire recreates the repeated
235
+ DOM when the owning state renders; it does not key or preserve individual rows
236
+ across updates. `lw:for` also works with global models set through
237
+ `litewire.setModel()`.
161
238
 
162
- Enabling history tracking via `lw-push-url="true"` allows AJAX-driven applications to retain full browser history support, enabling smooth back-and-forward navigation without page refreshes.
239
+ ### Form models
163
240
 
164
- ### How State Caching Works
241
+ ```html
242
+ <div lw:state="{ name: '' }">
243
+ <label>
244
+ Your name
245
+ <input lw:model="name">
246
+ </label>
247
+ <p>Hello, <span lw:text="name"></span>.</p>
248
+ </div>
249
+ ```
165
250
 
166
- 1. When a request completes successfully, Litewire captures the target container's current `innerHTML`.
167
- 2. Saves an object containing `{ litewireUrl, targetSelector, html }` into `window.history.state`.
168
- 3. When users press the browser's **Back** or **Forward** buttons, a `popstate` event fires.
169
- 4. Litewire retrieves the cached HTML snippet directly from history state and restores the DOM instantly without initiating network requests.
170
- 5. If no cached HTML state exists, Litewire seamlessly falls back to re-fetching the current location URL.
251
+ The default `lw:model` and `lw:model.live` listen for `input`.
252
+ `lw:model.change` listens for `change`, and `lw:model.blur` listens for `blur`.
253
+ For checkboxes, the model value is a boolean; for radio inputs, it is the
254
+ checked value or `null`. Unchecked radio buttons do not initialize or overwrite
255
+ the shared model; other controls use their value.
171
256
 
172
- ---
257
+ Without a local `lw:state` root, models are stored globally and can be read or
258
+ written with the instance methods `litewire.getModel(name)` and
259
+ `litewire.setModel(name, value)`. Global model changes dispatch a
260
+ `litewire:model` event on `document`, with `{ name, value }` in `event.detail`.
173
261
 
174
- ## 7. Event System & Lifecycle Hooks
262
+ A model name beginning with `#` or `.` is treated as a CSS selector instead of
263
+ a state key. Litewire copies the control's value to matching elements (to
264
+ `value` for form controls, otherwise to `textContent`) and dispatches
265
+ `litewire:model` on each target with `{ value }` in `event.detail`.
175
266
 
176
- Litewire dispatches standard, bubbling DOM events during the request lifecycles. Custom JavaScript code can hook into these lifecycle stages using standard `addEventListener` calls.
267
+ ### Event expressions
177
268
 
178
- | Event Name | `event.detail` | Description / Usage |
179
- | ------------------------ | ------------------ | --------------------------------------------------------------------------------------------------------------- |
180
- | `litewire:beforeRequest` | `null` | Fires immediately before the HTTP request is initiated. Ideal for disabling inputs or custom client validation. |
181
- | `litewire:afterRequest` | `null` | Fires after the request succeeds and the HTML content swap is complete. Ideal for triggering animations. |
182
- | `litewire:error` | `{ error: Error }` | Fires if a network error occurs or the HTTP response returns an error status (non-2xx). |
269
+ Supported event attributes are:
183
270
 
184
- ### Lifecycle Hooks Example
271
+ ```text
272
+ lw:click lw:input lw:change lw:submit
273
+ lw:keydown lw:keyup lw:focus lw:blur
274
+ ```
185
275
 
186
- ```javascript
187
- // Global logging and notification system integration
188
- document.addEventListener("litewire:beforeRequest", (e) => {
189
- console.log("Initiating Litewire request from:", e.target);
190
- });
276
+ The attribute value is evaluated when its event fires. Within the expression,
277
+ the nearest local state properties (or global models) are in scope. Special
278
+ values are available:
191
279
 
192
- document.addEventListener("litewire:error", (e) => {
193
- alert(`Request failed: ${e.detail.error.message}`);
194
- });
195
- ```
280
+ - `$event`: the DOM event
281
+ - `$el`: the element carrying the directive
282
+ - `$value`: the current value of that element
283
+ - `$state`: the current state object
284
+ - `$wire`: the Litewire instance
196
285
 
197
- ---
286
+ ```html
287
+ <div lw:state="{ query: '' }">
288
+ <input lw:model="query">
289
+ <button lw:click="console.log(query, $event)">Log query</button>
290
+ </div>
291
+ ```
198
292
 
199
- ## 8. CSRF & Security Protocols
293
+ Supported event modifiers:
294
+
295
+ | Modifier | Behavior |
296
+ |---|---|
297
+ | `.prevent` | Calls `preventDefault()` |
298
+ | `.stop` | Calls `stopPropagation()` |
299
+ | `.once` | Runs the handler at most once |
300
+ | `.capture` | Registers the listener in capture phase |
301
+ | `.passive` | Registers a passive listener |
302
+ | `.self` | Runs only when the event target is the directive element |
303
+ | `.outside` | Runs only when the event target is outside the directive element |
304
+ | `.window` | Registers the listener on `window` |
305
+ | `.document` | Registers the listener on `document` |
306
+ | `.debounce` | Delays execution until events stop for 250 ms |
307
+ | `.throttle` | Runs at most once per 250 ms |
308
+
309
+ Modifiers can be combined, for example `lw:click.prevent.once="save()"`.
310
+ Debounce and throttle delays can be customized in milliseconds, for example
311
+ `lw:input.debounce.400ms="search()"` or
312
+ `lw:click.throttle.1000ms="refresh()"`. `.outside` listens at the document
313
+ level so it can observe events outside the element.
314
+
315
+ ## Dynamic components
316
+
317
+ An element with `lw-component` dynamically imports a JavaScript module. The
318
+ path is joined to Litewire's base URL. Litewire selects the module's default
319
+ export first, then a named `imageUploader` export, then the first export. If
320
+ the selected export is a function, it is called with the host element and its
321
+ `data-*` attributes converted to an object:
200
322
 
201
- Litewire natively secures outgoing write operations (`POST`, `PUT`, `DELETE`) against Cross-Site Request Forgery (CSRF) attacks.
323
+ ```html
324
+ <div
325
+ lw-component="components/greeting.js"
326
+ data-user-name="Ada"
327
+ ></div>
328
+ ```
202
329
 
203
- ### Automatic Token Attachment
330
+ ```js
331
+ // /app/components/greeting.js
332
+ export default class Greeting {
333
+ constructor(element, params) {
334
+ element.textContent = `Hello, ${params.userName}!`;
335
+ }
336
+ }
337
+ ```
204
338
 
205
- Before dispatching non-GET requests, Litewire scans the host page head for a CSRF meta tag:
339
+ New components inserted by a swap or added to the document are discovered
340
+ automatically. By default, and with `lw-trigger="load"`, a component mounts
341
+ when discovered. Set `lw-trigger` to another event name to defer mounting
342
+ until that event fires. On a triggered component, `lw-target` optionally
343
+ selects the element passed to the component constructor; the component path
344
+ and `data-*` parameters still come from the element carrying `lw-component`.
345
+ A component is mounted only once after a successful mount. To load one after
346
+ an event and mount it into another element, put `lw-trigger` and `lw-target`
347
+ on the component source:
206
348
 
207
349
  ```html
208
- <meta name="csrf-token" content="abc123securehash456" />
350
+ <button
351
+ type="button"
352
+ lw-component="components/widget.js"
353
+ lw-trigger="click"
354
+ lw-target="#widget"
355
+ data-mode="compact"
356
+ >
357
+ Load widget
358
+ </button>
359
+ <div id="widget"></div>
209
360
  ```
210
361
 
211
- If detected, Litewire automatically includes the retrieved token into the HTTP request headers:
362
+ The module path and `data-*` parameters come from the element carrying
363
+ `lw-component`; `lw-target` is the element passed to the component.
212
364
 
213
- ```text
214
- X-CSRF-TOKEN: abc123securehash456
215
- ```
365
+ ## HTML swaps and DOM updates
366
+
367
+ Set `lw-target` to a CSS selector to choose where a response is inserted. If
368
+ omitted, the element that triggered the request is the target. `lw-swap`
369
+ controls how response HTML is applied:
216
370
 
217
- ---
371
+ | Value | Behavior |
372
+ |---|---|
373
+ | `innerHTML` (default) | Replaces the target's contents |
374
+ | `outerHTML` | Replaces the target element |
375
+ | `prepend` | Inserts response HTML at the beginning of the target |
376
+ | `append` | Inserts response HTML at the end of the target |
218
377
 
219
- ## 9. Practical Implementation Patterns
378
+ Litewire scans swapped content and mounts its components. It also observes
379
+ added DOM elements so directives in newly inserted content are bound
380
+ automatically.
220
381
 
221
- ### Pattern A: Instant Dynamic Search Table
382
+ ## Browser history
383
+
384
+ Set `lw-push-url="true"` to push the request URL to browser history, or provide
385
+ a custom URL as the attribute value:
222
386
 
223
387
  ```html
224
- <!-- Live search input updating target table and browser address bar -->
225
- <input
226
- type="text"
227
- name="q"
228
- placeholder="Search users..."
229
- lw-get="/users/search"
230
- lw-trigger="keyup"
231
- lw-target="#user-table-body"
388
+ <a
389
+ href="/products"
390
+ lw-get="/products"
391
+ lw-target="#page"
232
392
  lw-push-url="true"
233
- />
234
-
235
- <table>
236
- <tbody id="user-table-body">
237
- <!-- Table rows updated dynamically -->
238
- </tbody>
239
- </table>
393
+ >
394
+ Products
395
+ </a>
240
396
  ```
241
397
 
242
- ```php
243
- // Route
244
- Route::get('/users/search', 'UsersController@search')->name('user.search');
245
-
246
- //UserController
247
- public function search(Request $request)
248
- {
249
- $query = $request->input('q', '');
250
- $search = $this->model('UsersModel')->searchUser($query);
251
- $data = [
252
- 'users' => $search
253
- ];
254
-
255
- $html = $this->render('users/search', $data);
256
- return $this->response($html);
257
- }
258
-
259
- //Blade
260
- @if (!empty($users))
261
- @foreach ($users as $index => $user)
262
- <tr>
263
- <td>{{ $index + 1 }}</td>
264
- <td>{{ $user['fullname'] }}</td>
265
- </tr>
266
- @endforeach
267
- @endif
268
- ```
398
+ Litewire stores the URL, target selector, swap type, and target markup in
399
+ history state. When it creates the initial history entry, it saves the current
400
+ markup so Back can restore the prior view. For `outerHTML`, cached markup
401
+ includes the target element itself. When navigating with Back or Forward,
402
+ Litewire restores cached markup when the target still exists; otherwise it
403
+ attempts to fetch the stored URL. This caches only the selected target's
404
+ markup, not the complete document or JavaScript state.
269
405
 
270
- ### Pattern B: Form Submission with Indicator & Append Swap
406
+ ## Lifecycle events
271
407
 
272
- ```html
273
- <form
274
- lw-post="/comments/add"
275
- lw-target="#comments-list"
276
- lw-swap="append"
277
- lw-indicator="#spinner"
278
- >
279
- <textarea name="comment" required placeholder="Write a comment..."></textarea>
408
+ Litewire dispatches bubbling events from the request trigger:
280
409
 
281
- <button type="submit">
282
- Post Comment
283
- <span id="spinner" class="litewire-indicator hidden">Posting...</span>
284
- </button>
285
- </form>
410
+ | Event | `event.detail` | When it fires |
411
+ |---|---|---|
412
+ | `litewire:beforeRequest` | — | Immediately before sending a request |
413
+ | `litewire:afterRequest` | — | After a successful response has been swapped |
414
+ | `litewire:error` | `{ error }` | On a network failure or non-success HTTP response |
286
415
 
287
- <div id="comments-list">
288
- <!-- New comments prepended or appended dynamically -->
289
- </div>
416
+ Reactive local state also dispatches `litewire:render` from its state root
417
+ after a render, with `{ state }` in `event.detail`.
418
+
419
+ ```js
420
+ document.addEventListener("litewire:error", (event) => {
421
+ console.error("Litewire request failed:", event.detail.error);
422
+ });
423
+
424
+ document.addEventListener("litewire:render", (event) => {
425
+ console.log("State rendered:", event.detail.state);
426
+ });
290
427
  ```
291
428
 
292
- ### Pattern C: Lazy-Loading Section on Page Load
429
+ ## CSRF
293
430
 
294
- ```html
295
- <!-- Automatically fetches and replaces content once page loads -->
296
- <div
297
- lw-get="/analytics/widget"
298
- lw-trigger="load"
299
- lw-target="#analytics-container"
300
- >
301
- <p>Loading real-time analytics data...</p>
302
- </div>
431
+ When this meta tag is present, Litewire adds its token as the
432
+ `X-CSRF-TOKEN` header to outgoing requests:
303
433
 
304
- <div id="analytics-container"></div>
434
+ ```html
435
+ <meta name="csrf-token" content="your-server-generated-token">
305
436
  ```
437
+
438
+ The server must still validate the token and enforce its own authorization and
439
+ request protections.
440
+
441
+ ## Security considerations
442
+
443
+ Litewire inserts response bodies as HTML. Only return trusted or appropriately
444
+ sanitized HTML from your server.
445
+
446
+ `lw:state` and event-expression attributes are evaluated as JavaScript. Use
447
+ them only in markup you control; do not construct these expressions from
448
+ untrusted user input.
449
+
450
+ ## API reference
451
+
452
+ | Attribute / API | Description |
453
+ |---|---|
454
+ | `lw-get`, `lw-post`, `lw-put`, `lw-delete` | Request path and HTTP method |
455
+ | `lw-trigger` | Request event (defaults to `click` or form `submit`; `load` runs on scan); for components, event to defer mounting |
456
+ | `lw-target` | CSS selector for the response target |
457
+ | `lw-swap` | `innerHTML`, `outerHTML`, `prepend`, or `append` |
458
+ | `lw-indicator` | CSS selector for loading indicators |
459
+ | `lw-push-url` | `true`, `false`, or a custom history URL |
460
+ | `lw-component` | Path to a dynamically imported ES module |
461
+ | `lw:state` | JavaScript object expression that initializes local reactive state |
462
+ | `lw:model`, `.live`, `.change`, `.blur` | Bind a form control to a state or global model |
463
+ | `lw:text` | Render an expression as text |
464
+ | `lw:show` | Toggle the element's `hidden` property from an expression |
465
+ | `lw:class` | Add or remove classes reactively from an object expression |
466
+ | `template[lw:for]` | Repeat template contents for an iterable expression; item and index variables are available to bindings |
467
+ | `lw:click`, `lw:input`, `lw:change`, `lw:submit`, `lw:keydown`, `lw:keyup`, `lw:focus`, `lw:blur` | Evaluate an expression in response to an event; supports the modifiers documented above |
468
+ | `litewire.getModel(name)` | Read a global model |
469
+ | `litewire.setModel(name, value)` | Set a global model and update matching bindings |
470
+
471
+ **Buy my kids schoolbooks:** [https://paypal.me/justumg](https://paypal.me/justumg)
package/package.json CHANGED
@@ -1,24 +1,25 @@
1
1
  {
2
2
  "name": "litewire",
3
- "version": "1.0.1",
4
- "description": "A lightweight HTML-driven AJAX engine and dynamic ES module component loader.",
5
- "main": "src/litewire.js",
6
- "module": "src/litewire.js",
3
+ "version": "1.0.3",
4
+ "description": "A lightweight HTML-driven request engine with reactive state and dynamic ES module components.",
5
+ "main": "./litewire.js",
6
+ "module": "./litewire.js",
7
+ "browser": "./litewire.js",
7
8
  "type": "module",
8
9
  "exports": {
9
- ".": "./src/litewire.js"
10
+ ".": "./litewire.js"
10
11
  },
11
12
  "files": [
12
- "src/",
13
- "README.md",
14
- "LICENSE"
13
+ "litewire.js",
14
+ "README.md"
15
15
  ],
16
16
  "keywords": [
17
17
  "ajax",
18
- "htmx",
19
18
  "html-over-the-wire",
19
+ "reactive",
20
20
  "components",
21
- "dom-observer"
21
+ "javascript",
22
+ "frontend"
22
23
  ],
23
24
  "author": "Zainurrahman <languaojs@gmail.com>",
24
25
  "license": "MIT",
package/src/litewire.js DELETED
@@ -1,261 +0,0 @@
1
- class Litewire {
2
- constructor(options = {}) {
3
- const metaBaseUrl = document.querySelector('meta[name="base-url"]')?.getAttribute('content');
4
- const rawBaseUrl = options.baseUrl
5
- || metaBaseUrl
6
- || window.AppConfig?.baseUrl
7
- || window.location.origin;
8
- this.baseUrl = rawBaseUrl.replace(/\/+$/, '');
9
- this.loadingClass = options.loadingClass || 'litewire-request';
10
- this.init();
11
- }
12
-
13
- init() {
14
- this.scan(document);
15
- this.loadComponents(document);
16
- this.observe();
17
- this.listenToHistory();
18
- }
19
-
20
- async loadComponents(root) {
21
- const selector = '[lw-component]';
22
- const elements = root.querySelectorAll ? root.querySelectorAll(selector) : [];
23
-
24
- if (root.nodeType === Node.ELEMENT_NODE && root.matches(selector)) {
25
- this.mountComponent(root);
26
- }
27
- elements.forEach(el => this.mountComponent(el));
28
- }
29
-
30
- async mountComponent(el) {
31
- if (el._litewireComponentMounted) return;
32
- el._litewireComponentMounted = true;
33
-
34
- const rawPath = el.getAttribute('lw-component');
35
- if (!rawPath) return;
36
-
37
- const cleanPath = rawPath.replace(/^\/+/, '');
38
- const fullPath = `${this.baseUrl}/${cleanPath}`;
39
-
40
- try {
41
- const module = await import(fullPath);
42
- const params = { ...el.dataset };
43
-
44
- const componentFn = module.default || module.imageUploader || Object.values(module)[0];
45
-
46
- if (typeof componentFn === 'function') {
47
- try {
48
- new componentFn(el, params);
49
- } catch {
50
- componentFn(el, params);
51
- }
52
- } else {
53
- console.error(`[Litewire Error] No valid export found in module: ${fullPath}`);
54
- }
55
- } catch (err) {
56
- console.error(`[Litewire Error] Failed to import component from ${fullPath}:`, err);
57
- }
58
- }
59
-
60
- scan(root) {
61
- const selector = '[lw-get], [lw-post], [lw-put], [lw-delete], [lw-trigger="load"]';
62
- const elements = root.querySelectorAll ? root.querySelectorAll(selector) : [];
63
-
64
- if (root.nodeType === Node.ELEMENT_NODE && root.matches(selector)) {
65
- this.bindElement(root);
66
- }
67
- elements.forEach(el => this.bindElement(el));
68
- }
69
-
70
- bindElement(el) {
71
- if (el._litewireBound) return;
72
- el._litewireBound = true;
73
-
74
- const trigger = el.getAttribute('lw-trigger') || (el.tagName === 'FORM' ? 'submit' : 'click');
75
-
76
- if (trigger === 'load') {
77
- this.executeRequest(el);
78
- } else {
79
- el.addEventListener(trigger, (e) => {
80
- if (el.tagName === 'FORM' || trigger === 'click') e.preventDefault();
81
- this.executeRequest(el);
82
- });
83
- }
84
- }
85
-
86
- getIndicators(el) {
87
- const indicatorAttr = el.getAttribute('lw-indicator');
88
- if (!indicatorAttr) {
89
- return Array.from(el.querySelectorAll('.litewire-indicator'));
90
- }
91
- return Array.from(document.querySelectorAll(indicatorAttr));
92
- }
93
-
94
- async executeRequest(el, isPopState = false) {
95
- const method = ['post', 'put', 'delete', 'get'].find(m => el.hasAttribute(`lw-${m}`)) || 'get';
96
- const path = el.getAttribute(`lw-${method}`);
97
- if (!path) return;
98
-
99
- const cleanPath = path.replace(/^\/+/, '');
100
- let url = `${this.baseUrl}/${cleanPath}`;
101
-
102
- const options = { method: method.toUpperCase(), headers: {} };
103
-
104
- const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
105
- if (csrfToken) {
106
- options.headers['X-CSRF-TOKEN'] = csrfToken;
107
- }
108
-
109
- if (method === 'get') {
110
- const form = el.tagName === 'FORM' ? el : el.closest('form');
111
- let params = new URLSearchParams();
112
-
113
- if (form) {
114
- params = new URLSearchParams(new FormData(form));
115
- } else if (el.name && el.value !== undefined) {
116
- params.append(el.name, el.value);
117
- }
118
-
119
- const queryString = params.toString();
120
- if (queryString) {
121
- url += (url.includes('?') ? '&' : '?') + queryString;
122
- }
123
- } else {
124
- const form = el.tagName === 'FORM' ? el : el.closest('form');
125
- if (form) {
126
- options.body = new FormData(form);
127
- }
128
- }
129
-
130
- const indicators = this.getIndicators(el);
131
-
132
- try {
133
- el.classList.add(this.loadingClass);
134
- indicators.forEach(ind => ind.classList.add(this.loadingClass));
135
-
136
- el.dispatchEvent(new CustomEvent('litewire:beforeRequest', { bubbles: true }));
137
-
138
- const response = await fetch(url, options);
139
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
140
- const html = await response.text();
141
-
142
- const target = this.swapHTML(el, html);
143
-
144
- if (!isPopState) {
145
- this.handleHistoryPush(el, url, target);
146
- }
147
-
148
- el.dispatchEvent(new CustomEvent('litewire:afterRequest', { bubbles: true }));
149
- } catch (err) {
150
- console.error(`[Litewire Error] ${method.toUpperCase()} ${url} failed:`, err);
151
- el.dispatchEvent(new CustomEvent('litewire:error', { bubbles: true, detail: { error: err } }));
152
- } finally {
153
- el.classList.remove(this.loadingClass);
154
- indicators.forEach(ind => ind.classList.remove(this.loadingClass));
155
- }
156
- }
157
-
158
- swapHTML(el, html) {
159
- const targetSelector = el.getAttribute('lw-target');
160
- const target = targetSelector ? document.querySelector(targetSelector) : el;
161
- const swapType = el.getAttribute('lw-swap') || 'innerHTML';
162
-
163
- if (!target) return target;
164
-
165
- if (swapType === 'outerHTML') {
166
- const parent = target.parentElement;
167
- target.outerHTML = html;
168
- if (parent) {
169
- this.scan(parent);
170
- this.loadComponents(parent);
171
- }
172
- } else if (swapType === 'prepend') {
173
- target.insertAdjacentHTML('afterbegin', html);
174
- this.scan(target);
175
- this.loadComponents(target);
176
- } else if (swapType === 'append') {
177
- target.insertAdjacentHTML('beforeend', html);
178
- this.scan(target);
179
- this.loadComponents(target);
180
- } else {
181
- target.innerHTML = html;
182
- this.scan(target);
183
- this.loadComponents(target);
184
- }
185
-
186
- return target;
187
- }
188
-
189
- handleHistoryPush(el, requestUrl, target) {
190
- const pushAttr = el.getAttribute('lw-push-url');
191
- if (!pushAttr || pushAttr === 'false') return;
192
-
193
- const newUrl = pushAttr === 'true' ? requestUrl : pushAttr;
194
- const targetSelector = el.getAttribute('lw-target') || '';
195
-
196
- if (!history.state) {
197
- history.replaceState({
198
- litewireUrl: window.location.href,
199
- targetSelector: targetSelector,
200
- html: target ? target.innerHTML : ''
201
- }, '', window.location.href);
202
- }
203
-
204
- history.pushState({
205
- litewireUrl: newUrl,
206
- targetSelector: targetSelector,
207
- html: target ? target.innerHTML : ''
208
- }, '', newUrl);
209
- }
210
-
211
- listenToHistory() {
212
- window.addEventListener('popstate', (e) => {
213
- if (!e.state) return;
214
-
215
- const { targetSelector, html, litewireUrl } = e.state;
216
-
217
- if (targetSelector && html !== undefined) {
218
- const target = document.querySelector(targetSelector);
219
- if (target) {
220
- target.innerHTML = html;
221
- this.scan(target);
222
- this.loadComponents(target);
223
- return;
224
- }
225
- }
226
-
227
- const fallbackEl = document.createElement('div');
228
- fallbackEl.setAttribute('lw-get', litewireUrl || window.location.href);
229
- if (targetSelector) fallbackEl.setAttribute('lw-target', targetSelector);
230
-
231
- this.executeRequest(fallbackEl, true);
232
- });
233
- }
234
-
235
- observe() {
236
- const observer = new MutationObserver((mutations) => {
237
- for (const m of mutations) {
238
- m.addedNodes.forEach(node => {
239
- if (node.nodeType === Node.ELEMENT_NODE) {
240
- this.scan(node);
241
- this.loadComponents(node);
242
- }
243
- });
244
- }
245
- });
246
- observer.observe(document.body, { childList: true, subtree: true });
247
- }
248
- }
249
-
250
- export default Litewire;
251
-
252
- // Auto-instantiate in browser environments if not loaded as a module
253
- if (typeof window !== 'undefined' && typeof document !== 'undefined') {
254
- if (document.readyState === 'loading') {
255
- document.addEventListener('DOMContentLoaded', () => {
256
- if (!window.litewire) window.litewire = new Litewire();
257
- });
258
- } else {
259
- if (!window.litewire) window.litewire = new Litewire();
260
- }
261
- }