litewire 1.0.0 → 1.0.2

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