hotwired-laravel / turbo-laravel

This package gives you a set of conventions to make the most out of Hotwire in Laravel.
https://turbo-laravel.com
MIT License
795 stars 48 forks source link

[SUGGESTION] Don't render data-turbo-permanent elements + solution #13

Closed WalterWoshid closed 3 years ago

WalterWoshid commented 3 years ago

So I've been using Turbo and I love it. But I've been struggling with some basic things. For example I have a live chat that appears on every page and it has to load all the messages and their user relation (2 querys). I've added a "data-turbo-permanent" attribute (and an ID) to the live chat element and it works, but the server still has to render those elements.

My solution was to add a header to every Turbo request by searching for every "data-turbo-permanent" attribute on the page

// app.js
document.addEventListener('turbo:before-fetch-request', (e) => {
    let permanentElements = []
    document.querySelectorAll('[data-turbo-permanent]').forEach(function(el) {
        permanentElements.push(el.id)
    })
    e.detail.fetchOptions.headers['Turbo-Permanent'] = JSON.stringify(permanentElements)
});

Then in my blade templates I check if this header exists and return nothing if it does

// app.blade.php
<div id="live-chat" data-turbo-permanent>
    // Check if header exists && has the "live-chat" id
    @if(!(request()->hasHeader('Turbo-Permanent') && in_array('live-chat', json_decode(request()->header('Turbo-Permanent')))))
        // My life chat html & logic
    @endif
</div>

If I visit the page the first time or visit the page with Turbo and the element appears the first time, it gets rendered. And if I visit another page with Turbo and the element still exists, it doesn't get rendered and the response has an empty \

so Turbo knows not to delete the element, like this:

// response
...
<body>
    ...
    <div id="live-chat" data-turbo-permanent></div>
    ...
</body>
...

I bet this could be programmed in a much better way without having to check the request in the blade template and also the render method also needs the request checking logic which I didn't include here.



I'd also have a solution for persisting scroll positions with a custom "data-turbo-scroll" attribute

// app.js
Turbo['scrollPositionsToSave'] = []
document.addEventListener('turbo:before-visit', function () {
    let elements = document.querySelectorAll('[data-turbo-scroll]')
    elements.forEach(function(el) {
        if (el.scrollTop) {
            Turbo['scrollPositionsToSave'].push({ element: el,  scrollPosition : el.scrollTop })
        }
    })
}, false)
document.addEventListener('turbo:load', function () {
    Turbo['scrollPositionsToSave'].forEach(function(el) { el.element.scrollTo(0, el.scrollPosition) })
    Turbo['scrollPositionsToSave'] = [];
}, false)
tonysm commented 3 years ago

For the chat example, I think you could have a Turbo Frame inside the data-turbo-permanent element and load its contents after the page load (not using lazy loading turbo frames, manually triggering a form or something)

<div id="chatroom-container" data-turbo-permanent>
  <form
    data-turbo-frame="chatroom"
    action="{{ route('chatroom.index') }}"
    method="GET"
    class="hidden"
    x-data
    x-ref="loadRoomsForm"
    x-init="$refs.loadRoomsForm.submit()"
  >
    <button type="submit">Load</button>
  </form>

  <turbo-frame id="chatroom">
    <p>Loading...</p>
  </turbo-frame>
</div>

Subsequent requests would contain the turbo permanent element, but its contents would only load the first time the form enters the DOM.

tonysm commented 3 years ago

(maybe event just a hidden link instead of the form?)

WalterWoshid commented 3 years ago

@tonysm I'm going to try that, thanks. To be honest I didn't understand any of those \ or \ elements.

WalterWoshid commented 3 years ago

@tonysm It's not really what I'm looking for, the chat room has to be loaded from the start, without user input and without lazy loading. It also has to persist on every page. The "data-turbo-permanent" attribute is working like it should, but the server doesn't know that and still sends the data and does all the queries. With my method it would be possible to persist elements without rendering them multiple times and with little to no effort.

tonysm commented 3 years ago

Hey, I don't think we should implement something custom in the package that is not part of the Turbo.js or Turbo Rails source (unless something like this already exists there?).

Consider bringing up this issue on the Hotwire Discuss forum, please. Others might have a solution for this that feels more like a 1st-party.

WalterWoshid commented 3 years ago

I understand. This is just more or less laravel specific.

tonysm commented 3 years ago

It sounds like it's more generic. I can totally see the same problem occurring in Rails. But there they use fragment caching, so this is maybe the only difference?