islandjs-rails 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/README.md +108 -5
- data/lib/islandjs_rails/core_methods.rb +2 -1
- data/lib/islandjs_rails/rails_helpers.rb +52 -12
- data/lib/islandjs_rails/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 26c1475ed8f02b2c2c3217af0e96e95d3444083e94a35b9094d46170d0d88f00
|
4
|
+
data.tar.gz: ed6cd8e18382dcc1445b861f55b46aa7d1624ec4c4f9d063b2a0c87c1cb372a3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dac3858a5b178e3186850a22e01beed9b384ecd053cc653ee5572743bdef78562cf17c5b51a8c6d51a7f08fecd2aca31fd82cbb830339508c85c2daf1a69f4b4
|
7
|
+
data.tar.gz: 3887d3aad9bcc0072df24e933804daf4827e097eec4baa2ec7945d184e23356f819bb45d9f5fb2f74642a89a10573561642c532f8340d8f5051e8c4ec506a01d
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## [0.2.0] - 2025-01-04
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- **Placeholder Support**: New placeholder functionality for `react_component` helper to prevent layout shift during component mounting
|
12
|
+
- **Three Placeholder Patterns**: ERB block, CSS class, and inline style options for maximum flexibility
|
13
|
+
- **Turbo Stream Optimization**: Perfect integration with Turbo Stream updates to eliminate "jumpy" content behavior
|
14
|
+
- **Automatic Cleanup**: Leverages React's natural DOM replacement for reliable placeholder removal
|
15
|
+
- **Error Handling**: Graceful fallback that keeps placeholder visible if React fails to mount
|
16
|
+
|
17
|
+
### Enhanced
|
18
|
+
- `react_component` helper now accepts optional block for custom placeholder content
|
19
|
+
- Added `placeholder_class` and `placeholder_style` options for quick placeholder styling
|
20
|
+
- Improved error resilience in React mounting process
|
21
|
+
|
22
|
+
### Documentation
|
23
|
+
- Comprehensive placeholder documentation with real-world examples
|
24
|
+
- Turbo Stream integration patterns and best practices
|
25
|
+
- Updated helper method signatures and available options
|
26
|
+
|
8
27
|
## [0.1.0] - 2025-08-04
|
9
28
|
|
10
29
|
### Added
|
data/README.md
CHANGED
@@ -38,6 +38,11 @@ rails "islandjs:install[react-dom,18.3.1]"
|
|
38
38
|
```erb
|
39
39
|
<!-- In any view -->
|
40
40
|
<%= react_component('DashboardApp', { userId: current_user.id }) %>
|
41
|
+
|
42
|
+
<!-- With placeholder (v0.2.0+) to prevent layout shift -->
|
43
|
+
<%= react_component('DashboardApp', { userId: current_user.id }) do %>
|
44
|
+
<div class="loading-skeleton">Loading dashboard...</div>
|
45
|
+
<% end %>
|
41
46
|
```
|
42
47
|
|
43
48
|
> 💡 **Turbo Cache Compatible**: React components automatically persist state across Turbo navigation! See [Turbo Cache Integration](#turbo-cache-integration) for details.
|
@@ -98,13 +103,24 @@ Modern Rails developers face a painful choice:
|
|
98
103
|
|
99
104
|
### The IslandJS Rails Solution
|
100
105
|
```bash
|
101
|
-
# Instead of complex webpack configuration:
|
102
|
-
rails "islandjs:install[react]"
|
106
|
+
# Instead of complex vite/webpack configuration:
|
107
|
+
rails "islandjs:install[react,18.3.1]"
|
108
|
+
rails "islandjs:install[react-beautiful-dnd]"
|
109
|
+
rails "islandjs:install[quill]"
|
110
|
+
rails "islandjs:install[recharts]"
|
103
111
|
rails "islandjs:install[lodash]"
|
104
112
|
```
|
105
113
|
|
106
114
|
**Result**: Zero-to-no webpack configuration, instant prod builds, access to hundreds of UMD packages.
|
107
115
|
|
116
|
+
### Access UMD installed JS packages via the window from within React components:
|
117
|
+
```bash
|
118
|
+
\\ in SomeComponent.jsx
|
119
|
+
const quill = new window.Quill("#editor", {
|
120
|
+
theme: "snow",
|
121
|
+
});
|
122
|
+
```
|
123
|
+
|
108
124
|
## Rails 8 Ready
|
109
125
|
|
110
126
|
✅ **Tested against Rails 8**
|
@@ -119,6 +135,7 @@ rails "islandjs:install[lodash]"
|
|
119
135
|
- **CDN Downloads** - Fetches UMD builds from unpkg.com and jsdelivr.net
|
120
136
|
- **Rails Integration** - Serves auto-generated vendor UMD files for seamless integration
|
121
137
|
- **Webpack Externals** - Updates webpack config to prevent duplicate bundling while allowing development in jsx or other formats
|
138
|
+
- **Placeholder Support** - Eliminate layout shift with automatic placeholder management ⚡ *New in v0.2.0*
|
122
139
|
- **Flexible Architecture** - Compose and namespace libraries as needed
|
123
140
|
|
124
141
|
## CLI Commands
|
@@ -363,8 +380,8 @@ This automatically loads:
|
|
363
380
|
- Your webpack bundle
|
364
381
|
- Debug information in development
|
365
382
|
|
366
|
-
#### `react_component(name, props, options)`
|
367
|
-
Renders a React component with Turbo-compatible lifecycle.
|
383
|
+
#### `react_component(name, props, options, &block)`
|
384
|
+
Renders a React component with Turbo-compatible lifecycle and optional placeholder support.
|
368
385
|
|
369
386
|
```erb
|
370
387
|
<%= react_component('UserProfile', {
|
@@ -376,6 +393,92 @@ Renders a React component with Turbo-compatible lifecycle.
|
|
376
393
|
}) %>
|
377
394
|
```
|
378
395
|
|
396
|
+
**Available Options:**
|
397
|
+
- `container_id`: Custom ID for the container element
|
398
|
+
- `namespace`: JavaScript namespace for component access (default: `window.islandjsRails`)
|
399
|
+
- `tag`: HTML tag for container (default: `div`)
|
400
|
+
- `class`: CSS class for container
|
401
|
+
- `placeholder_class`: CSS class for placeholder content
|
402
|
+
- `placeholder_style`: Inline styles for placeholder content
|
403
|
+
|
404
|
+
## Placeholder Support
|
405
|
+
|
406
|
+
⚡ **New in v0.2.0** - Prevent layout shift when React components mount!
|
407
|
+
|
408
|
+
The `react_component` helper now supports placeholder content that displays while your React component loads, eliminating the "jumpy" effect common in dynamic content updates via Turbo Streams.
|
409
|
+
|
410
|
+
### Problem Solved
|
411
|
+
|
412
|
+
When React components mount (especially via Turbo Stream updates), there's often a brief moment where content height changes, causing layout shift:
|
413
|
+
|
414
|
+
```erb
|
415
|
+
<!-- Before: Content jumps when component mounts -->
|
416
|
+
<%= react_component("Reactions", { postId: post.id }) %>
|
417
|
+
<!-- Page content shifts down when reactions component renders -->
|
418
|
+
```
|
419
|
+
|
420
|
+
### Solution: Three Placeholder Patterns
|
421
|
+
|
422
|
+
#### 1. ERB Block Placeholder (Most Flexible)
|
423
|
+
```erb
|
424
|
+
<%= react_component("Reactions", { postId: post.id }) do %>
|
425
|
+
<div class="reactions-skeleton">
|
426
|
+
<div class="skeleton-button">👍</div>
|
427
|
+
<div class="skeleton-button">❤️</div>
|
428
|
+
<div class="skeleton-button">🚀</div>
|
429
|
+
<div class="skeleton-count">Loading...</div>
|
430
|
+
</div>
|
431
|
+
<% end %>
|
432
|
+
```
|
433
|
+
|
434
|
+
#### 2. CSS Class Placeholder (Design System Friendly)
|
435
|
+
```erb
|
436
|
+
<%= react_component("Reactions", { postId: post.id }, {
|
437
|
+
placeholder_class: "reactions-skeleton"
|
438
|
+
}) %>
|
439
|
+
```
|
440
|
+
|
441
|
+
#### 3. Inline Style Placeholder (Quick & Simple)
|
442
|
+
```erb
|
443
|
+
<%= react_component("Reactions", { postId: post.id }, {
|
444
|
+
placeholder_style: "height: 40px; background: #f8f9fa; border-radius: 4px;"
|
445
|
+
}) %>
|
446
|
+
```
|
447
|
+
|
448
|
+
### How It Works
|
449
|
+
|
450
|
+
1. **Placeholder renders** immediately with your ERB content or styles
|
451
|
+
2. **React mounts** and automatically replaces the entire container contents
|
452
|
+
3. **Zero manual cleanup** - React's natural DOM replacement handles removal
|
453
|
+
4. **On mount errors** - placeholder stays visible as graceful fallback
|
454
|
+
|
455
|
+
### Perfect for Turbo Streams
|
456
|
+
|
457
|
+
Placeholders shine in Turbo Stream scenarios where content updates dynamically:
|
458
|
+
|
459
|
+
```erb
|
460
|
+
<!-- app/views/posts/_reactions.html.erb -->
|
461
|
+
<%= turbo_stream.replace "post_#{@post.id}_reactions" do %>
|
462
|
+
<%= react_component("Reactions", {
|
463
|
+
postId: @post.id,
|
464
|
+
initialCount: @post.reactions.count
|
465
|
+
}) do %>
|
466
|
+
<div class="reactions-placeholder" style="height: 32px;">
|
467
|
+
<span class="text-muted">Loading reactions...</span>
|
468
|
+
</div>
|
469
|
+
<% end %>
|
470
|
+
<% end %>
|
471
|
+
```
|
472
|
+
|
473
|
+
### Benefits
|
474
|
+
|
475
|
+
- ✅ **Eliminates layout shift** during component mounting
|
476
|
+
- ✅ **Turbo Stream compatible** - perfect for dynamic updates
|
477
|
+
- ✅ **Zero JavaScript required** - handled automatically by the helper
|
478
|
+
- ✅ **Graceful degradation** - placeholder persists if React fails to load
|
479
|
+
- ✅ **Design system friendly** - use your existing skeleton/loading styles
|
480
|
+
- ✅ **Performance optimized** - leverages React's natural DOM clearing
|
481
|
+
|
379
482
|
## Turbo Cache Integration
|
380
483
|
|
381
484
|
IslandJS Rails includes **built-in Turbo cache compatibility** for React components, ensuring state persists seamlessly across navigation.
|
@@ -751,4 +854,4 @@ Planned features for future releases:
|
|
751
854
|
### 🎯 **Developer Experience**
|
752
855
|
- **Zero Webpack Expertise**: Rails developers stay in Rails
|
753
856
|
- **Turbo Compatible**: Seamless navigation and caching
|
754
|
-
- **Progressive Enhancement**: Start with Hotwire, add React islands
|
857
|
+
- **Progressive Enhancement**: Start with Hotwire, add React islands
|
@@ -398,7 +398,8 @@ module IslandjsRails
|
|
398
398
|
response = Net::HTTP.get_response(uri)
|
399
399
|
|
400
400
|
if response.code == '200'
|
401
|
-
|
401
|
+
# Force UTF-8 encoding to avoid encoding errors when writing to file
|
402
|
+
response.body.force_encoding('UTF-8')
|
402
403
|
else
|
403
404
|
raise IslandjsRails::Error, "Failed to download UMD from #{url}: #{response.code}"
|
404
405
|
end
|
@@ -49,7 +49,8 @@ module IslandjsRails
|
|
49
49
|
end
|
50
50
|
|
51
51
|
# Mount a React component with props and Turbo-compatible lifecycle
|
52
|
-
|
52
|
+
# Supports optional placeholder content via block or options
|
53
|
+
def react_component(component_name, props = {}, options = {}, &block)
|
53
54
|
# Generate component ID - use custom container_id if provided
|
54
55
|
if options[:container_id]
|
55
56
|
component_id = options[:container_id]
|
@@ -62,6 +63,10 @@ module IslandjsRails
|
|
62
63
|
css_class = options[:class] || ''
|
63
64
|
namespace = options[:namespace] || 'window.islandjsRails'
|
64
65
|
|
66
|
+
# Handle placeholder options
|
67
|
+
placeholder_class = options[:placeholder_class]
|
68
|
+
placeholder_style = options[:placeholder_style]
|
69
|
+
|
65
70
|
# For turbo-cache compatibility, store initial state as JSON in data attribute
|
66
71
|
initial_state_json = props.to_json
|
67
72
|
|
@@ -95,7 +100,24 @@ module IslandjsRails
|
|
95
100
|
# Add data-initial-state for turbo-cache compatibility
|
96
101
|
initial_state_attr = " data-initial-state=\"#{initial_state_json.gsub('"', '"')}\""
|
97
102
|
|
98
|
-
|
103
|
+
# Generate placeholder content
|
104
|
+
placeholder_content = if block_given?
|
105
|
+
placeholder_html = capture(&block)
|
106
|
+
"<div data-island-placeholder=\"true\">#{placeholder_html}</div>"
|
107
|
+
elsif placeholder_class || placeholder_style
|
108
|
+
class_attr = placeholder_class ? " class=\"#{placeholder_class}\"" : ""
|
109
|
+
style_attr = placeholder_style ? " style=\"#{placeholder_style}\"" : ""
|
110
|
+
"<div data-island-placeholder=\"true\"#{class_attr}#{style_attr}></div>"
|
111
|
+
else
|
112
|
+
""
|
113
|
+
end
|
114
|
+
|
115
|
+
# Build container HTML with optional placeholder
|
116
|
+
if placeholder_content.empty?
|
117
|
+
container_html = "<#{tag_name} id=\"#{component_id}\"#{class_part}#{data_part}#{initial_state_attr}></#{tag_name}>"
|
118
|
+
else
|
119
|
+
container_html = "<#{tag_name} id=\"#{component_id}\"#{class_part}#{data_part}#{initial_state_attr}>#{placeholder_content}</#{tag_name}>"
|
120
|
+
end
|
99
121
|
|
100
122
|
html_safe_string("#{container_html}\n#{mount_script}")
|
101
123
|
end
|
@@ -241,30 +263,48 @@ module IslandjsRails
|
|
241
263
|
<script>
|
242
264
|
(function() {
|
243
265
|
function mount#{component_name}() {
|
266
|
+
const container = document.getElementById('#{component_id}');
|
267
|
+
if (!container) return;
|
268
|
+
|
269
|
+
// Check for component availability
|
244
270
|
if (typeof #{namespace_with_optional} === 'undefined' || !#{namespace_with_optional}.#{component_name}) {
|
245
271
|
console.warn('IslandJS: #{component_name} component not found. Make sure it\\'s exported in your bundle.');
|
272
|
+
// Restore placeholder visibility if component fails to load
|
273
|
+
const placeholder = container.querySelector('[data-island-placeholder="true"]');
|
274
|
+
if (placeholder) {
|
275
|
+
placeholder.style.display = '';
|
276
|
+
}
|
246
277
|
return;
|
247
278
|
}
|
248
279
|
|
249
280
|
if (typeof React === 'undefined' || typeof window.ReactDOM === 'undefined') {
|
250
281
|
console.warn('IslandJS: React or ReactDOM not loaded. Install with: rails "islandjs:install[react]" and rails "islandjs:install[react-dom]"');
|
282
|
+
// Restore placeholder visibility if React fails to load
|
283
|
+
const placeholder = container.querySelector('[data-island-placeholder="true"]');
|
284
|
+
if (placeholder) {
|
285
|
+
placeholder.style.display = '';
|
286
|
+
}
|
251
287
|
return;
|
252
288
|
}
|
253
289
|
|
254
|
-
const container = document.getElementById('#{component_id}');
|
255
|
-
if (!container) return;
|
256
|
-
|
257
290
|
const props = { containerId: '#{component_id}' };
|
258
291
|
const element = React.createElement(#{namespace_with_optional}.#{component_name}, props);
|
259
292
|
|
260
|
-
|
261
|
-
|
262
|
-
if (
|
263
|
-
container._reactRoot
|
293
|
+
try {
|
294
|
+
// Use React 18 createRoot if available, fallback to React 17 render
|
295
|
+
if (window.ReactDOM.createRoot) {
|
296
|
+
if (!container._reactRoot) {
|
297
|
+
container._reactRoot = window.ReactDOM.createRoot(container);
|
298
|
+
}
|
299
|
+
container._reactRoot.render(element);
|
300
|
+
// React 18 automatically clears container - no manual cleanup needed
|
301
|
+
} else {
|
302
|
+
// React 17 - render is synchronous and clears container automatically
|
303
|
+
window.ReactDOM.render(element, container);
|
264
304
|
}
|
265
|
-
|
266
|
-
|
267
|
-
|
305
|
+
} catch (error) {
|
306
|
+
console.error('IslandJS: Failed to mount #{component_name}:', error);
|
307
|
+
// On error, keep placeholder visible by not modifying container
|
268
308
|
}
|
269
309
|
}
|
270
310
|
|