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.
- package/LICENSE +2 -2
- package/README.MD +383 -217
- package/package.json +11 -10
- package/src/litewire.js +0 -261
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026
|
|
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
|
|
1
|
+
# Litewire 1.0.3
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
27
|
+
## Install and start
|
|
14
28
|
|
|
15
|
-
|
|
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
|
-
|
|
33
|
+
```html
|
|
34
|
+
<script type="module" src="/js/litewire.js"></script>
|
|
35
|
+
```
|
|
28
36
|
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
+
Install version 1.0.3 from your configured npm registry:
|
|
35
44
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
51
|
+
```js
|
|
52
|
+
import "litewire";
|
|
53
|
+
```
|
|
45
54
|
|
|
46
|
-
|
|
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
|
-
###
|
|
59
|
+
### First request
|
|
49
60
|
|
|
50
|
-
|
|
61
|
+
```html
|
|
62
|
+
<button
|
|
63
|
+
lw-get="/hello"
|
|
64
|
+
lw-target="#result"
|
|
65
|
+
>
|
|
66
|
+
Load greeting
|
|
67
|
+
</button>
|
|
51
68
|
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
+
The placeholder prevents the module's browser auto-initializer from creating
|
|
110
|
+
another instance before your configured instance is constructed.
|
|
74
111
|
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
130
|
+
<div id="messages"></div>
|
|
131
|
+
```
|
|
95
132
|
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
lw-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
169
|
+
## Reactive state and bindings
|
|
119
170
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
128
|
-
}
|
|
175
|
+
### Local state
|
|
129
176
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
200
|
+
### Text and visibility
|
|
146
201
|
|
|
147
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
###
|
|
215
|
+
### Lists
|
|
155
216
|
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
+
### Form models
|
|
163
240
|
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
267
|
+
### Event expressions
|
|
177
268
|
|
|
178
|
-
|
|
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
|
-
|
|
271
|
+
```text
|
|
272
|
+
lw:click lw:input lw:change lw:submit
|
|
273
|
+
lw:keydown lw:keyup lw:focus lw:blur
|
|
274
|
+
```
|
|
185
275
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
|
|
323
|
+
```html
|
|
324
|
+
<div
|
|
325
|
+
lw-component="components/greeting.js"
|
|
326
|
+
data-user-name="Ada"
|
|
327
|
+
></div>
|
|
328
|
+
```
|
|
202
329
|
|
|
203
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
406
|
+
## Lifecycle events
|
|
271
407
|
|
|
272
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
429
|
+
## CSRF
|
|
293
430
|
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "A lightweight HTML-driven
|
|
5
|
-
"main": "
|
|
6
|
-
"module": "
|
|
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
|
-
".": "./
|
|
10
|
+
".": "./litewire.js"
|
|
10
11
|
},
|
|
11
12
|
"files": [
|
|
12
|
-
"
|
|
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
|
-
"
|
|
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
|
-
}
|