Skip to content

Conversation

@amitaibu
Copy link
Collaborator

@amitaibu amitaibu commented Jul 8, 2025

Migrates IHP from legacy Turbolinks 5.1.1 to modern Hotwire Turbo 8.0.13, replacing morphdom with Turbo's built-in morphing.

Changes

• Replace Turbolinks with Turbo 8.0.13 bundle
• Remove morphdom dependency - use Turbo's Idiomorph instead
• Enable morphing with <meta name="turbo-refresh-method" content="morph">
• Update auto-refresh to use Turbo Streams
• Migrate transitionToNewPage() to Turbo.renderPage() API
• Update events: turbolinks:loadturbo:load

Benefits

🚀 Better performance with Idiomorph vs morphdom
📦 Single library instead of multiple dependencies
🔧 Actively maintained vs discontinued Turbolinks

Test plan

  • IDE navigation works
  • Auto-refresh preserves form state
  • Live reload
  • Handle js-delete
  • Server-side components functional
  • IDE close modal dialogs
  • IDE Delete button

🤖 Generated with Claude Code

@amitaibu
Copy link
Collaborator Author

amitaibu commented Jul 8, 2025

This will require more testing, and likely would need a new release with upgrade notes (to update the JS files projects call on Layout.hs)

Peek.2025-07-08.17-39.mp4

But so far it's working 🤯

Seems to also fix #2063 that triggered this experiment 😄

@amitaibu
Copy link
Collaborator Author

amitaibu commented Jul 8, 2025

Auto refresh is also working.

Peek.2025-07-08.18-59.mp4

@amitaibu
Copy link
Collaborator Author

amitaibu commented Jul 8, 2025

I'm trying to check the SSC following https://ihp.digitallyinduced.com/Guide/server-side-components.html#serverside-components

But I'm getting this error.

image

My flake.nix has ihp.url = "path:///home/amitaibu/Sites/ihp-landing-page/IHP";, and I've nix flake update and direnv allow

What else might I be missing?

p.s. Is anyone using SSC? Is this a feature we might want to drop at a certain point?

@amitaibu
Copy link
Collaborator Author

amitaibu commented Jul 8, 2025

@mpscholten this can already benefit from a review/ testing locally, as it's a big change.

@amitaibu
Copy link
Collaborator Author

amitaibu commented Jul 8, 2025

I've fixed ihp-scc not working, maybe it's not needed if running non-local IHP 🤷

haskellPackages = p: with p; [
    # Haskell dependencies go here
    p.ihp
+   p.ihp-ssc

@amitaibu
Copy link
Collaborator Author

amitaibu commented Jul 8, 2025

SSC is now included in the compile, but it complains about the code from the guide. So maybe someone that has ever used it, should give it a try 😄

@amitaibu
Copy link
Collaborator Author

@unhammer care to check your scenario with the back button? You can do it by changing in your flake.nix

  1. ihp.url = "github:digitallyinduced/ihp/pull/2087/head"; and direnv reload
  2. And in Layout.hs
- <script src={assetPath "/vendor/morphdom-umd.min.js"}></script>
+ <script src={assetPath "/vendor/turbo.js"}></script>
- <script src={assetPath "/vendor/turbolinks.js"}></script>
- <script src={assetPath "/vendor/turbolinksInstantClick.js"}></script>
- <script src={assetPath "/vendor/turbolinksMorphdom.js"}></script>

@amitaibu amitaibu marked this pull request as ready for review July 10, 2025 11:23
@amitaibu amitaibu marked this pull request as draft July 10, 2025 11:31
@amitaibu amitaibu marked this pull request as ready for review July 10, 2025 11:37
@unhammer
Copy link
Collaborator

unhammer commented Sep 22, 2025

I haven't noticed any big issues yet, so this seems very promising :-)

I did find one minor regression. I have a form that submits when I hit enter/tab to the next field. This worked fine in turbolinks, but in turbo it loses focus:

turbolinks, here I'm typing numbers and hitting enter, no mouse needed:
turbolinks

hotwire turbo, after I hit enter it does switch to the next field but then when the POST is complete and console says ihp:load runs, it loses focus:
turbo

@unhammer
Copy link
Collaborator

unhammer commented Sep 22, 2025

I'm pretty sure it's because of this change to ihp/data/static/helpers.js, which to me seems like it sidesteps any morphing and just reverts to plain document.body.innerHTML = newHtml.body.innerHTML:

     var isModalOpen = document.body.classList.contains('modal-open');
-    morphdom(                                                                             
-        document.body,
-        newHtml.tagName === 'BODY' ? newHtml : newHtml.body,
-        {                                                                                                                                                                            
-            childrenOnly: false,                                                          
-            onBeforeElUpdated: function (from, to) {      
-                if (                  
-                    newHtml.body &&                                                       
-                    newHtml.body.classList.contains('modal-open') &&
-                    from.id === 'main-row'                                                
-                ) {
-                    return false;
-                } else if (isModalOpen && from.id === 'main-row') {
-                    return false;                                                         
-                } else if (              
-                    from.classList.contains('flatpickr-input') &&
-                    from._flatpickr
-                ) {                                                                       
-                    unsafeSetTimeout(
-                        function (from, to) {
-                            console.log(
-                                'FROM',
-                                from,
-                                to.getAttribute('value'),
-                                to.value                                                  
-                            );
-                            from.value = to.value;
-                            // from.setAttribute('value', to.getAttribute('value'));
-                        },                                                                
-                        0,                                                                
-                        from,
-                        to
-                    );         
-                }             
-            },                                                                            
-            getNodeKey: function (el) {                                                   
-                var key = el.id;  
-                if (el.id) {
-                    key = el.id;
-                } else if (el instanceof HTMLScriptElement) {
-                    key = el.src;
-                }
-                return key;
-            },                                                                                                                                                                       
-        }               
-    );                                                                                    
+    var newBodyHasModal = newHtml.body && newHtml.body.classList.contains('modal-open');
  
+    if (isModalOpen && !newBodyHasModal) {
+        // Modal is currently open but new content doesn't have modal - preserve modal state
+        // Only update non-modal content areas
+        var mainRow = document.getElementById('main-row');
+        if (mainRow && newHtml.body) {                                                    
+            var newMainRow = newHtml.body.querySelector('#main-row');                                                                                                                
+            if (newMainRow) {
+                mainRow.innerHTML = newMainRow.innerHTML;
+            }
+        }
+    } else {
+        // Normal page update - replace entire body content
+        if (newHtml.tagName === 'BODY') {
+            document.body.innerHTML = newHtml.innerHTML;
+        } else if (newHtml.body) {
+            document.body.innerHTML = newHtml.body.innerHTML;
+        }
+    }

So turbo doesn't get a chance to run any morphing here.

@unhammer
Copy link
Collaborator

unhammer commented Sep 22, 2025

If I simply disable the window.submitForm override on submit events like

--- ../ihp/ihp/data/static/helpers.js   2025-09-22 11:25:01.897260252 +0200
+++ static/helpers.js   2025-09-22 12:40:17.399831586 +0200
@@ -149,28 +149,20 @@
 function initDisableButtonsOnSubmit() {
     if (window.initDisableButtonsOnSubmitRun) {
         return;
     }
     window.initDisableButtonsOnSubmitRun = true;

-    var lastClicked = null;
-    document.addEventListener('submit', function (event) {
-        event.preventDefault();
-
-        var form = event.target;
-        window.submitForm(form, lastClicked);
-    });
-
     document.addEventListener('mouseup', function (event) {
         lastClicked = event.target;
     });
 }

then turbo.js seems to pick up the POST results and morph them on its own, no need for explicit calls to morphdom. I think most of what window.submitForm does is handled by the browser (and should be automatically handled by turbo morph), but it also does stuff like:

  • pause/unpause live reload
  • dismiss alerts
  • handle disableJavascriptSubmission
  • in transitionToNewPage:
    • dispatchEvent(ihpUnloadEvent) – I don't know what this is
    • only updating main-row if .modal-open (seems very special-cased? haven't seen any reference to those classes in IHP before now, but it's been there since 2020).

So one way forward would be to let turbo do the form submissions, and we hook into turbo's form submission events: https://turbo.hotwired.dev/handbook/drive#form-submissions where I guess we could do most of the stuff that window.submitForm currently does, and data-turbo="false" should be equivalent to data-disable-javascript-submission (though for backwards compat maybe helpers.js could go through and add data-turbo=false to all data-disable-javascript-submission).


Also, turbo.js seems to require an id attribute on forms to be able to morph them (maybe something to put in formFor, doesn't seem to matter what the id is or even that it's the same in the response, so maybe a random one would work)

@amitaibu
Copy link
Collaborator Author

amitaibu commented Oct 1, 2025

@unhammer it's very possible stuff are wrong. It was a session of me and Claude AI, and it's for sure not perfect.

I wouldn't mind if you want to take it from here 😄

@unhammer
Copy link
Collaborator

unhammer commented Oct 1, 2025

👍 I don't think there's that much missing for it to be functional, apart from that window.submitForm (which I think is mostly about removing code). I'll see if I find some time to make an attempt at it after Potato Holiday.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants