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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d78a2c4f572cfe85c3a5a220463132357e77077654fd8445a0b44fc98035f991
4
- data.tar.gz: 5e5db353b07167587ad36572da27688ecb7409eec0124ede59a41604831f0c7a
3
+ metadata.gz: 26c1475ed8f02b2c2c3217af0e96e95d3444083e94a35b9094d46170d0d88f00
4
+ data.tar.gz: ed6cd8e18382dcc1445b861f55b46aa7d1624ec4c4f9d063b2a0c87c1cb372a3
5
5
  SHA512:
6
- metadata.gz: 79d300dd1047994557c217aaae37aa88cda075d1b5aeb4d3f8d95de635add49441102bdda8622fb528ba6e0c1202bcc2746381a50505a2fac37606120160f696
7
- data.tar.gz: 2acd431790e153784cb7e78eb31973684afdbc02c02510c4b393e724f12c44b0dd204550b91ae4e5b0bf7cf60fbcaa19519644a0abaf708c59f342ec27e0714a
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
- response.body
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
- def react_component(component_name, props = {}, options = {})
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('"', '&quot;')}\""
97
102
 
98
- container_html = "<#{tag_name} id=\"#{component_id}\"#{class_part}#{data_part}#{initial_state_attr}></#{tag_name}>"
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
- // Use React 18 createRoot if available, fallback to React 17 render
261
- if (window.ReactDOM.createRoot) {
262
- if (!container._reactRoot) {
263
- container._reactRoot = window.ReactDOM.createRoot(container);
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
- container._reactRoot.render(element);
266
- } else {
267
- window.ReactDOM.render(element, container);
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
 
@@ -1,3 +1,3 @@
1
1
  module IslandjsRails
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: islandjs-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Arnold