@live-react-islands/core 0.1.0 → 0.1.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.
- package/LICENSE +1 -1
- package/README.md +454 -0
- package/dist/createHooks.d.ts +3 -1
- package/dist/createHooks.d.ts.map +1 -1
- package/dist/index.esm.js +9 -2
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/ssr.esm.js.map +1 -1
- package/dist/ssr.js.map +1 -1
- package/package.json +21 -15
- package/src/createHooks.ts +12 -3
- package/dist/IslandRenderer.d.ts +0 -11
- package/dist/IslandRenderer.d.ts.map +0 -1
- package/dist/client/IndividualIslandRenderer.d.ts +0 -14
- package/dist/client/IndividualIslandRenderer.d.ts.map +0 -1
- package/dist/client/PortalIslandsRenderer.d.ts +0 -4
- package/dist/client/PortalIslandsRenderer.d.ts.map +0 -1
- package/dist/client/islandsManager.d.ts +0 -23
- package/dist/client/islandsManager.d.ts.map +0 -1
- package/dist/client.d.ts +0 -23
- package/dist/client.d.ts.map +0 -1
- package/dist/island-manager.d.ts +0 -59
- package/dist/island-manager.d.ts.map +0 -1
- package/dist/server.d.ts +0 -18
- package/dist/server.d.ts.map +0 -1
- package/dist/useIslandStream.d.ts +0 -15
- package/dist/useIslandStream.d.ts.map +0 -1
package/LICENSE
CHANGED
package/README.md
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
# Live React Islands
|
|
2
|
+
|
|
3
|
+
**React-powered interactive islands** inside Phoenix LiveView. Harness the NPM ecosystem with server-driven state, real-time streams and zero-lag forms + SSR.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Why Live React Islands?
|
|
8
|
+
|
|
9
|
+
**The best of both worlds!** ✨
|
|
10
|
+
|
|
11
|
+
Phoenix LiveView is excellent for server-driven UIs, but sometimes you need the rich interactivity of React for specific components. Live React Islands lets you:
|
|
12
|
+
|
|
13
|
+
- Use React components as "islands" within your LiveView templates
|
|
14
|
+
- Maintain server-side state in Elixir while rendering in React
|
|
15
|
+
- Send events from React to Elixir and push props back
|
|
16
|
+
- Use forms and streams in React with dedicated hooks
|
|
17
|
+
- Share global state across multiple islands
|
|
18
|
+
- Optionally server-side render for faster loads, SEO and no flicker
|
|
19
|
+
|
|
20
|
+
## Comparison
|
|
21
|
+
|
|
22
|
+
Choose Live React Islands when you need rich, interactive React components without giving up LiveView’s server-driven simplicity.
|
|
23
|
+
|
|
24
|
+
| Feature | LiveView Only | LiveView + Alpine | Live React Islands (this) | Pure SPA (Next.js/Vite) |
|
|
25
|
+
| :--------------------- | :----------------------------------------- | :------------------- | :------------------------------------- | :---------------------- |
|
|
26
|
+
| **UI Ecosystem** | Limited (HEEX/Custom) | Small (Alpine kits) | **Infinite (NPM/React)** | **Infinite (NPM)** |
|
|
27
|
+
| **Interactivity** | Server-Roundtrip (JS hooks for edge cases) | Simple Client-side | **High-Fidelity / Fluid** | High-Fidelity / Fluid |
|
|
28
|
+
| **State Management** | Single (Server) | Fragmented | **Single (Server-Led)** | Dual (API + Client) |
|
|
29
|
+
| **Initial Load / SEO** | Instant | Instant | **Instant (SSR-enabled)** | Slow / Complex SSR |
|
|
30
|
+
| **JS Bundle Size** | ~0kb (Core only) | Small (+15kb) | **Large (React: ~100–150 kB gzipped)** | Large |
|
|
31
|
+
| **Developer Speed** | Very High | High (until complex) | **High (Asset Reuse)** | Low (API Plumbing) |
|
|
32
|
+
| **Component Logic** | Elixir Only | Mixed (Strings) | **JSX (Encapsulated)** | JSX |
|
|
33
|
+
| **Complexity Ceiling** | Struggles with app-like complexity | Hits wall on "State" | **High** | Very High |
|
|
34
|
+
|
|
35
|
+
## When NOT to Use Live React Islands
|
|
36
|
+
|
|
37
|
+
- If your UI is mostly static or CRUD-heavy, plain LiveView is simpler and faster.
|
|
38
|
+
- If you only need light client-side behavior (toggles, dropdowns), LiveView + Alpine may be sufficient.
|
|
39
|
+
- If your application requires full offline support or heavy client-side state, a traditional SPA may be a better fit.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
### Elixir
|
|
44
|
+
|
|
45
|
+
Add to your `mix.exs`:
|
|
46
|
+
|
|
47
|
+
```elixir
|
|
48
|
+
def deps do
|
|
49
|
+
[
|
|
50
|
+
{:live_react_islands, "~> 0.1.0"},
|
|
51
|
+
# For development SSR (optional):
|
|
52
|
+
{:live_react_islands_ssr_vite, "~> 0.1.0", only: :dev},
|
|
53
|
+
# For production SSR (optional):
|
|
54
|
+
{:live_react_islands_ssr_deno, "~> 0.1.0", only: :prod}
|
|
55
|
+
]
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### JavaScript
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm install @live-react-islands/core
|
|
63
|
+
# For development SSR (optional):
|
|
64
|
+
npm install --save-dev @live-react-islands/vite-plugin-ssr
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Quick Start
|
|
68
|
+
|
|
69
|
+
### 1. Create a React Component
|
|
70
|
+
|
|
71
|
+
```jsx
|
|
72
|
+
// src/islands/Counter.jsx
|
|
73
|
+
const Counter = ({ count, title, pushEvent }) => {
|
|
74
|
+
return (
|
|
75
|
+
<div>
|
|
76
|
+
<h2>{title}</h2>
|
|
77
|
+
<p>Count: {count}</p>
|
|
78
|
+
<button onClick={() => pushEvent("increment", {})}>+1</button>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export default Counter;
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 2. Set Up the LiveView Hooks
|
|
87
|
+
|
|
88
|
+
```jsx
|
|
89
|
+
// src/islands/index.js
|
|
90
|
+
export default { Counter: () => import("./Counter") };
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Islands can be lazy loaded to only load the JS used on the page.
|
|
94
|
+
|
|
95
|
+
```jsx
|
|
96
|
+
// src/main.jsx
|
|
97
|
+
import { createHooks } from "@live-react-islands/core";
|
|
98
|
+
import islands from "./islands";
|
|
99
|
+
|
|
100
|
+
const islandHooks = createHooks({ islands });
|
|
101
|
+
|
|
102
|
+
// Add to your LiveSocket
|
|
103
|
+
let liveSocket = new LiveSocket("/live", Socket, {
|
|
104
|
+
hooks: { ...islandHooks },
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 3. Create an Elixir Component
|
|
109
|
+
|
|
110
|
+
```elixir
|
|
111
|
+
defmodule MyAppWeb.Components.CounterIsland do
|
|
112
|
+
use LiveReactIslands.Component,
|
|
113
|
+
component: "Counter",
|
|
114
|
+
props: %{count: 0, title: "My Counter"}
|
|
115
|
+
|
|
116
|
+
def handle_event("increment", _params, socket) do
|
|
117
|
+
new_count = socket.assigns.count + 1
|
|
118
|
+
{:noreply, update_prop(socket, :count, new_count)}
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 4. Use in Your LiveView
|
|
124
|
+
|
|
125
|
+
```elixir
|
|
126
|
+
defmodule MyAppWeb.CounterLive do
|
|
127
|
+
use MyAppWeb, :live_view
|
|
128
|
+
use LiveReactIslands.LiveView
|
|
129
|
+
|
|
130
|
+
def render(assigns) do
|
|
131
|
+
~H"""
|
|
132
|
+
<.live_component module={MyAppWeb.Components.CounterIsland} id="counter-1" />
|
|
133
|
+
"""
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## How It Works
|
|
139
|
+
|
|
140
|
+
**React components receive these props automatically:**
|
|
141
|
+
|
|
142
|
+
| Prop | Description |
|
|
143
|
+
| -------------------- | --------------------------------- |
|
|
144
|
+
| `id` | The island's unique identifier |
|
|
145
|
+
| `pushEvent` | Function to send events to Elixir |
|
|
146
|
+
| All defined props | Current values from Elixir |
|
|
147
|
+
| All consumed globals | Current global state values |
|
|
148
|
+
|
|
149
|
+
## Features
|
|
150
|
+
|
|
151
|
+
### Props
|
|
152
|
+
|
|
153
|
+
Define props with default values. Props can be set from the template or updated from event handlers:
|
|
154
|
+
|
|
155
|
+
```elixir
|
|
156
|
+
use LiveReactIslands.Component,
|
|
157
|
+
component: "Counter",
|
|
158
|
+
props: %{count: 0, title: "Default Title"}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Elixir components can override `init/2` for dynamic initialization:**
|
|
162
|
+
|
|
163
|
+
```elixir
|
|
164
|
+
def init(assigns, socket) do
|
|
165
|
+
# Called once on mount, before SSR and first render
|
|
166
|
+
socket
|
|
167
|
+
|> update_prop(:computed, compute_value(assigns))
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Updating props from Elixir:**
|
|
172
|
+
|
|
173
|
+
```elixir
|
|
174
|
+
def handle_event("increment", _, socket) do
|
|
175
|
+
{:noreply, update_prop(socket, :count, socket.assigns.count + 1)}
|
|
176
|
+
end
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Passing props from templates:**
|
|
180
|
+
|
|
181
|
+
```heex
|
|
182
|
+
<.live_component module={CounterIsland} id="counter-1" title="Custom Title" />
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Once a prop is set from outside the component any `update_prop` call on it will raise an error to prevent a nasty set of bugs. To just initialize the component use `init_[prop]` to set the value once and then the component takes over.
|
|
186
|
+
|
|
187
|
+
```heex
|
|
188
|
+
<.live_component module={CounterIsland} id="counter-1" init_count={5} />
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Events
|
|
192
|
+
|
|
193
|
+
Send events from React to Elixir using `pushEvent`:
|
|
194
|
+
|
|
195
|
+
```jsx
|
|
196
|
+
// React
|
|
197
|
+
<button onClick={() => pushEvent("save", { data: formData })}>Save</button>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
```elixir
|
|
201
|
+
# Elixir
|
|
202
|
+
def handle_event("save", %{"data" => data}, socket) do
|
|
203
|
+
# Handle the event
|
|
204
|
+
{:noreply, socket}
|
|
205
|
+
end
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Global State
|
|
209
|
+
|
|
210
|
+
Share state across multiple islands. When a global changes, all islands that use it automatically rerender.
|
|
211
|
+
|
|
212
|
+
**Set up in your LiveView:**
|
|
213
|
+
|
|
214
|
+
```elixir
|
|
215
|
+
defmodule MyAppWeb.DashboardLive do
|
|
216
|
+
use MyAppWeb, :live_view
|
|
217
|
+
use LiveReactIslands.LiveView, expose_globals: [:user, :theme]
|
|
218
|
+
|
|
219
|
+
def mount(_params, session, socket) do
|
|
220
|
+
{:ok, assign(socket, user: get_user(session), theme: "light")}
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Consume in your island:**
|
|
226
|
+
|
|
227
|
+
```elixir
|
|
228
|
+
use LiveReactIslands.Component,
|
|
229
|
+
component: "Header",
|
|
230
|
+
props: %{},
|
|
231
|
+
globals: [:user, :theme]
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Optional globals** (won't error if not set):
|
|
235
|
+
|
|
236
|
+
```elixir
|
|
237
|
+
globals: [:user?] # The ? suffix makes it optional
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The globals are passed as props to your React component:
|
|
241
|
+
|
|
242
|
+
```jsx
|
|
243
|
+
const Header = ({ user, theme }) => (
|
|
244
|
+
<header className={theme}>Welcome, {user.name}</header>
|
|
245
|
+
);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Forms with Server Validation
|
|
249
|
+
|
|
250
|
+
Build forms with React UI and Elixir/Ecto validation. Input is collected client side with zero typing latency and send to Elixir for validation. Errors from the changeset get pushed back to React.
|
|
251
|
+
|
|
252
|
+
The `useForm` hook implements a "Validation Lock" pattern: Updates are versioned and `isValid` will only be true until the server confirms the current form state is valid.
|
|
253
|
+
|
|
254
|
+
**Elixir component:**
|
|
255
|
+
|
|
256
|
+
```elixir
|
|
257
|
+
defmodule MyAppWeb.Components.ContactFormIsland do
|
|
258
|
+
use LiveReactIslands.Component,
|
|
259
|
+
component: "ContactForm",
|
|
260
|
+
props: %{form: %{}}
|
|
261
|
+
|
|
262
|
+
alias MyApp.Contact
|
|
263
|
+
|
|
264
|
+
def init(_assigns, socket) do
|
|
265
|
+
changeset = Contact.changeset(%Contact{}, %{})
|
|
266
|
+
socket |> init_form(:form, changeset)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def handle_form(:validate, :form, attrs, socket) do
|
|
270
|
+
changeset = Contact.changeset(%Contact{}, attrs)
|
|
271
|
+
{:noreply, update_form(socket, :form, changeset)}
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def handle_form(:submit, :form, attrs, socket) do
|
|
275
|
+
case Contact.create(attrs) do
|
|
276
|
+
{:ok, _contact} ->
|
|
277
|
+
{:noreply, init_form(socket, :form, Contact.changeset(%Contact{}, %{}))}
|
|
278
|
+
{:error, changeset} ->
|
|
279
|
+
{:noreply, update_form(socket, :form, changeset)}
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**React component:**
|
|
286
|
+
|
|
287
|
+
```jsx
|
|
288
|
+
import { useForm } from "@live-react-islands/core";
|
|
289
|
+
|
|
290
|
+
const ContactForm = ({ form, pushEvent }) => {
|
|
291
|
+
const {
|
|
292
|
+
getFieldProps,
|
|
293
|
+
getError,
|
|
294
|
+
isRequired,
|
|
295
|
+
isTouched,
|
|
296
|
+
handleSubmit,
|
|
297
|
+
isValid,
|
|
298
|
+
} = useForm(form, pushEvent);
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<form onSubmit={handleSubmit}>
|
|
302
|
+
<input {...getFieldProps("name")} />
|
|
303
|
+
{isTouched("name") && getError("name") && (
|
|
304
|
+
<span className="error">{getError("name")}</span>
|
|
305
|
+
)}
|
|
306
|
+
|
|
307
|
+
<input {...getFieldProps("email")} type="email" />
|
|
308
|
+
{isTouched("email") && getError("email") && (
|
|
309
|
+
<span className="error">{getError("email")}</span>
|
|
310
|
+
)}
|
|
311
|
+
|
|
312
|
+
<button type="submit" disabled={!isValid}>
|
|
313
|
+
Submit
|
|
314
|
+
</button>
|
|
315
|
+
</form>
|
|
316
|
+
);
|
|
317
|
+
};
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**`useForm` returns:**
|
|
321
|
+
|
|
322
|
+
| Property | Description |
|
|
323
|
+
| ----------------------- | ----------------------------------------------------- |
|
|
324
|
+
| `values` | Current form values |
|
|
325
|
+
| `errors` | Validation errors by field |
|
|
326
|
+
| `touched` | Fields the user has interacted with |
|
|
327
|
+
| `getFieldProps(name)` | Props to spread on inputs (`value`, `onChange`, etc.) |
|
|
328
|
+
| `getError(name)` | First error message for a field |
|
|
329
|
+
| `isRequired(name)` | Whether a field is required |
|
|
330
|
+
| `isTouched(name)` | Whether user has modified this field |
|
|
331
|
+
| `setField(name, value)` | Programmatically set a field value |
|
|
332
|
+
| `handleSubmit` | Form submit handler |
|
|
333
|
+
| `reset()` | Reset form to server values |
|
|
334
|
+
| `isSyncing` | True while waiting for server validation |
|
|
335
|
+
| `isValid` | True only when synced AND server says valid |
|
|
336
|
+
|
|
337
|
+
### Streams
|
|
338
|
+
|
|
339
|
+
Stream data to React components for real-time updates like feeds, chat, or infinite scrolling:
|
|
340
|
+
|
|
341
|
+
**Define a stream prop:**
|
|
342
|
+
|
|
343
|
+
```elixir
|
|
344
|
+
use LiveReactIslands.Component,
|
|
345
|
+
component: "MessageList",
|
|
346
|
+
props: %{
|
|
347
|
+
messages: {:stream, default: []}
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
**Push stream events from Elixir:**
|
|
352
|
+
|
|
353
|
+
```elixir
|
|
354
|
+
# Insert new item (prepends by default)
|
|
355
|
+
socket |> stream_insert(:messages, %{id: 1, text: "Hello"})
|
|
356
|
+
|
|
357
|
+
# Update existing item
|
|
358
|
+
socket |> stream_update(:messages, %{id: 1, text: "Hello, edited"})
|
|
359
|
+
|
|
360
|
+
# Delete an item
|
|
361
|
+
socket |> stream_delete(:messages, 1)
|
|
362
|
+
|
|
363
|
+
# Reset the entire stream
|
|
364
|
+
socket |> stream_reset(:messages)
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Consume in React:**
|
|
368
|
+
|
|
369
|
+
```jsx
|
|
370
|
+
import { useStream } from "@live-react-islands/core";
|
|
371
|
+
|
|
372
|
+
const MessageList = ({ messages: messagesHandle }) => {
|
|
373
|
+
const messages = useStream(messagesHandle, { limit: 100 });
|
|
374
|
+
|
|
375
|
+
return (
|
|
376
|
+
<ul>
|
|
377
|
+
{messages.map((msg) => (
|
|
378
|
+
<li key={msg.id}>{msg.text}</li>
|
|
379
|
+
))}
|
|
380
|
+
</ul>
|
|
381
|
+
);
|
|
382
|
+
};
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Shared Context
|
|
386
|
+
|
|
387
|
+
Islands using `:none` (default) or `:overwrite` SSR strategies render into a shared React root via portals. This enables powerful patterns like drag-and-drop between islands, shared state managers, or animation libraries that need to coordinate across components.
|
|
388
|
+
|
|
389
|
+
**Wrap all islands in a shared context:**
|
|
390
|
+
|
|
391
|
+
```jsx
|
|
392
|
+
// src/main.jsx
|
|
393
|
+
import { createHooks } from "@live-react-islands/core";
|
|
394
|
+
import { DndProvider } from "react-beautiful-dnd";
|
|
395
|
+
import islands from "./islands";
|
|
396
|
+
|
|
397
|
+
const SharedContextProvider = ({ children }) => (
|
|
398
|
+
<DndProvider backend={HTML5Backend}>{children}</DndProvider>
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const islandHooks = createHooks({
|
|
402
|
+
islands,
|
|
403
|
+
SharedContextProvider,
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Now all your islands can participate in drag-and-drop with each other, even though they're scattered across your LiveView template.
|
|
408
|
+
|
|
409
|
+
> **Note:** Islands using `:hydrate_root` SSR strategy have their own isolated React root and do not participate in the shared context. Use `:overwrite` or `:none` if you need context sharing between islands.
|
|
410
|
+
|
|
411
|
+
### Server-Side Rendering (SSR)
|
|
412
|
+
|
|
413
|
+
SSR improves initial page load performance by rendering React components on the server.
|
|
414
|
+
|
|
415
|
+
```elixir
|
|
416
|
+
use LiveReactIslands.Component,
|
|
417
|
+
component: "Counter",
|
|
418
|
+
props: %{count: 0},
|
|
419
|
+
ssr_strategy: :overwrite # or :hydrate_root or :none (default)
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
| Strategy | Shared Root | Best For |
|
|
423
|
+
| --------------- | ----------- | ----------------------------------------------------------------------- |
|
|
424
|
+
| `:none` | Yes | Interactive components where initial render doesn't matter |
|
|
425
|
+
| `:overwrite` | Yes | Most islands, especially when you need cross-island context (e.g., DnD) |
|
|
426
|
+
| `:hydrate_root` | No | Large islands where you want to avoid the overwrite flash |
|
|
427
|
+
|
|
428
|
+
> ⚠️ SSR is optional. Many islands work perfectly without it. Enable SSR when initial paint, SEO, or perceived performance matter.
|
|
429
|
+
|
|
430
|
+
See the **[SSR Guide](https://github.com/dcza/live-react-islands/blob/main/docs/SSR.md)** for complete setup instructions, caching strategies, and custom renderer implementation.
|
|
431
|
+
|
|
432
|
+
## Requirements
|
|
433
|
+
|
|
434
|
+
- Elixir >= 1.14
|
|
435
|
+
- Phoenix LiveView >= 1.0
|
|
436
|
+
- React 18 or 19
|
|
437
|
+
- Any JavaScript bundler (built-in SSR plugin for Vite)
|
|
438
|
+
|
|
439
|
+
### Running Examples
|
|
440
|
+
|
|
441
|
+
```bash
|
|
442
|
+
cd examples/vite-example
|
|
443
|
+
mix deps.get
|
|
444
|
+
cd assets && yarn install && yarn dev # in one terminal
|
|
445
|
+
cd .. && mix phx.server # in another terminal
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
## Contributing
|
|
449
|
+
|
|
450
|
+
See [CONTRIBUTING.md](https://github.com/dcza/live-react-islands/blob/main/CONTRIBUTING.md) for development setup and guidelines.
|
|
451
|
+
|
|
452
|
+
## License
|
|
453
|
+
|
|
454
|
+
MIT License - see [LICENSE](https://github.com/dcza/live-react-islands/blob/main/LICENSE) for details.
|
package/dist/createHooks.d.ts
CHANGED
|
@@ -3,7 +3,9 @@ import { type IslandsMap, type ContextProviderComponent } from "./types";
|
|
|
3
3
|
interface LiveViewHook {
|
|
4
4
|
el: HTMLElement;
|
|
5
5
|
liveSocket: {
|
|
6
|
-
on: (event: string, callback: (e:
|
|
6
|
+
on: (event: string, callback: (e: CustomEvent<{
|
|
7
|
+
kind: string;
|
|
8
|
+
}>) => void) => void;
|
|
7
9
|
};
|
|
8
10
|
pushEvent: (event: string, payload: any, callback?: (reply: any, ref: any) => void) => void;
|
|
9
11
|
pushEventTo: (target: string, event: string, payload: any) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createHooks.d.ts","sourceRoot":"","sources":["../src/createHooks.ts"],"names":[],"mappings":"AAAA,OAAuB,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EACL,KAAK,UAAU,EAGf,KAAK,wBAAwB,EAM9B,MAAM,SAAS,CAAC;AAMjB,UAAU,YAAY;IACpB,EAAE,EAAE,WAAW,CAAC;IAChB,UAAU,EAAE;QACV,EAAE,EAAE,
|
|
1
|
+
{"version":3,"file":"createHooks.d.ts","sourceRoot":"","sources":["../src/createHooks.ts"],"names":[],"mappings":"AAAA,OAAuB,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EACL,KAAK,UAAU,EAGf,KAAK,wBAAwB,EAM9B,MAAM,SAAS,CAAC;AAMjB,UAAU,YAAY;IACpB,EAAE,EAAE,WAAW,CAAC;IAChB,UAAU,EAAE;QACV,EAAE,EAAE,CACF,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,CAAC,CAAC,EAAE,WAAW,CAAC;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,KAAK,IAAI,KACjD,IAAI,CAAC;KACX,CAAC;IACF,SAAS,EAAE,CACT,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,GAAG,EACZ,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,KACtC,IAAI,CAAC;IACV,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,KAAK,IAAI,CAAC;IACnE,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,KAAK,IAAI,CAAC;CACrE;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,UAAU,CAAC;IACpB,qBAAqB,CAAC,EAAE,wBAAwB,CAAC;IACjD,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAwED,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EAAE,UAAe,EACxB,qBAA2C,EAC3C,OAAwB,GACzB,EAAE,kBAAkB;;iBASR,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI;kBAC3B,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI;mBAC3B,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI;;EAgO1C"}
|
package/dist/index.esm.js
CHANGED
|
@@ -380,7 +380,14 @@ function createHooks({ islands: islandsMap = {}, SharedContextProvider = NullCon
|
|
|
380
380
|
}
|
|
381
381
|
if (!navigationListenerAttached) {
|
|
382
382
|
navigationListenerAttached = true;
|
|
383
|
-
this.liveSocket.on("phx:page-loading-start", () => {
|
|
383
|
+
this.liveSocket.on("phx:page-loading-start", (e) => {
|
|
384
|
+
// Patches (e.g. push_patch tab switches) stay within the same
|
|
385
|
+
// LiveView mount and globals version counter, so islands that
|
|
386
|
+
// remain mounted through the patch must keep their cached
|
|
387
|
+
// globals. Only a real navigation (new mount, version resets
|
|
388
|
+
// to 0 server-side) needs the stale client-side version cleared.
|
|
389
|
+
if (e.detail?.kind === "patch")
|
|
390
|
+
return;
|
|
384
391
|
globalsRequested = false;
|
|
385
392
|
managerState = manager.resetGlobals(managerState);
|
|
386
393
|
});
|
|
@@ -469,7 +476,7 @@ function createHooks({ islands: islandsMap = {}, SharedContextProvider = NullCon
|
|
|
469
476
|
const el = this.el;
|
|
470
477
|
pendingMounts.add(elId);
|
|
471
478
|
(async () => {
|
|
472
|
-
const islandConfig = await resolveIslandConfig(islandsMap, componentName,
|
|
479
|
+
const islandConfig = await resolveIslandConfig(islandsMap, componentName, NullContextProvider$1);
|
|
473
480
|
if (!pendingMounts.has(elId))
|
|
474
481
|
return;
|
|
475
482
|
pendingMounts.delete(elId);
|