Skip to content

Instantly share code, notes, and snippets.

@secretpray
Last active May 5, 2024 03:32
Show Gist options
  • Save secretpray/2b72a41d3d08200967fcf7a7481944d9 to your computer and use it in GitHub Desktop.
Save secretpray/2b72a41d3d08200967fcf7a7481944d9 to your computer and use it in GitHub Desktop.
 fetch(href, {
      headers: {
        Accept: "text/vnd.turbo-stream.html",
      },
    })
      .then(r => r.text())
      .then(html => Turbo.renderStreamMessage(html))
//  optionally reflect the url    .then(_ => history.replaceState(history.state, "", href))
import { Controller } from '@hotwired/stimulus'

export default class extends Controller {
  static values = {
    src: String,
  }

  connect() {
    fetch(this.srcValue, {
      headers: {
        Accept: "text/vnd.turbo-stream.html",
      },
    }).then(r => r.text())
      .then(html => Turbo.renderStreamMessage(html))
  }
}
@secretpray
Copy link
Author

Add turbo-frame to your webpage:

 <turbo-frame id="my-frame" src="/my/path">
 </turbo-frame>

Add an input element, say a button, somewhere on the page with onclick():

 <input class="btn" name="123" id="btn" onclick="update_my_frame(name)">

In the JS function triggered by onclick(), you can load/reload the turbo frame:

     update_my_frame(name) {
     // console.log(name);
     let frame = document.querySelector('turbo-frame#my-frame')
     frame.src = '/my/path' // => loads turbo-frame
     // ...
     frame.src = '/my/new/path'
     frame.reload() // => reload turbo-frame with updated src
 }

@secretpray
Copy link
Author

Trying to use Turbo.visit, but within a Turbo Frame
For reasons, I needed to be able to programmatically navigate within a Turbo Frame. I was hoping that Turbo.visit(path) would work, but it always affected the entire page. I found a feature that is only lightly documented. If I can select the turbo-frame element I can set the "src" attribute to make it load a new path. Here is a Stimulus controller I made to make table rows clickable. It will work within a Turbo Frame or just a regular table on a page:

import { Controller } from "stimulus"

export default class extends Controller {
  goToRecord(event) {
    // Hyperlink clicks automatically work within the Turbo Frame
    if (event.target.tagName === "A") {
      return;
    }

    let row = event.currentTarget;
    let path = row.dataset.href;
    let target = row.dataset.target;

    if (path) {
      if (target === "frame") {
        row.closest("turbo-frame").src = path;
      } else {
        Turbo.visit(path, { action: "replace" });
      }
    }
  }
}

@secretpray
Copy link
Author

import { Controller } from 'stimulus';
import { Turbo } from '@hotwired/turbo-rails';

export default class extends Controller {
  static targets = ['output'];

  async fetch() {
    const response = await Turbo.fetch('/data.json');
    const data = await response.json();
    this.render(data);
  }

  render(data) {
    const outputFrame = this.outputTarget.closest('turbo-frame');
    outputFrame.innerHTML = `<pre>${JSON.stringify(data, null, 2)}</pre>`;
  }
}

@secretpray
Copy link
Author

Turbo.fetch
Sure, here are a few examples of how to use Turbo's fetch method:

**Simple GET request:

const response = await Turbo.fetch('/data.json');
const data = await response.json();
console.log(data);

This code makes a GET request to /data.json, waits for the response, and then logs the parsed JSON data to the console.

POST request with JSON payload:

const payload = { name: 'John Doe', email: '[email protected]' };
const options = {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(payload),
};
const response = await Turbo.fetch('/api/users', options);
const data = await response.json();
console.log(data);

This code creates a JSON payload object, sets the necessary headers and options for a POST request, and then uses Turbo.fetch to make the request to /api/users. The response is then parsed as JSON and logged to the console.

Using response data to navigate to a new page:

const response = await Turbo.fetch('/data.json');
const data = await response.json();
Turbo.visit(`/details/${data.id}`);

This code makes a GET request to /data.json, waits for the response, and then parses the JSON data. It then uses the data to construct a new URL and navigates to it using Turbo.visit.

Note that in all of these examples, await is used to wait for the response before continuing with the code. This is necessary because Turbo.fetch returns a Promise, which needs to be resolved asynchronously.

@secretpray
Copy link
Author

Pause-continue turbo request

 document.addEventListener("turbo:before-fetch-request", function (event) {
      if (event.target.id === 'recently_cart' && !currentUserId()) {
        const searchParams = event.detail.url.searchParams
        getRecentPages(searchParams)
      }
      event.detail.resume()   
    })
submiAction(event) {
    event.preventDefault()
    event.stopPropagation()

    const form = event.target.closest('form')
    const turboFrame = event.target.closest('turbo-frame#new_order_form')
    turboFrame.setAttribute('target', '_top')

    form.submit() // Turbo.visit(form.action)
  }

@secretpray
Copy link
Author

secretpray commented Jan 29, 2024

More (with delay)

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="replace"
export default class extends Controller {
  swap() {
    fetch("/test/replace", {
      method: "POST",
      headers: {
        Accept: "text/vnd.turbo-stream.html",
        "X-CSRF-Token": this.getCsrfToken()
      }
    })
      .then(r => r.text())
      .then(html => Turbo.renderStreamMessage(html)) // <turbo-stream action="replace"> ...</turbo-stream>
  }

  getCsrfToken() {
    return document.querySelector('meta[name="csrf-token"]').content;
  }
}

@secretpray
Copy link
Author

secretpray commented Jan 29, 2024

Observe changes

let observer = new MutationObserver(mutationRecords => {
  console.log(mutationRecords); // console.log(the changes)
  // execute your code here.
});
  
let elem = document.getElementById("div-id");
    
// observe everything except attributes
observer.observe(elem, {
  childList: true, // observe direct children
  subtree: true, // and lower descendants too
  characterDataOldValue: true // pass old data to callback
});

Link1

Link2

const afterRenderEvent = new Event("turbo:after-stream-render");
addEventListener("turbo:before-stream-render", (event) => {
  const originalRender = event.detail.render

  event.detail.render = function (streamElement) {
    originalRender(streamElement)
    document.dispatchEvent(afterRenderEvent);
  }
})

or

  show() {
      fetch("/path/to/partial")
        .then((res) => res.text())
        .then((html) => {
          const fragment = document
            .createRange()
            .createContextualFragment(html);

          this.element.appendChild(fragment);
          // OR document.getElementById("testy_element").appendChild(fragment)
        });
     
  }
<div class="container" data-controller="test">
<button data-action="test#show">Do the Thing</button>
<!-- partial would appear here -->
</div>

@secretpray
Copy link
Author

Broadcasting Progress from Background Jobs

# app/jobs/heavy_task_job.rb
class HeavyTaskJob < ApplicationJob
  queue_as :default
  before_perform :broadcast_initial_update

  def perform(current_user_id)

    total_count.times do |i|
      SmallTaskJob.perform_later(current_user_id, i, total_count)
    end
  end

  private

  def broadcast_initial_update
    Turbo::StreamsChannel.broadcast_replace_to ["heavy_task_channel", current_user.to_gid_param].join(":"),
      target: "heavy_task",
      partial: "heavy_tasks/progress",
      locals: {
        total_count: total_count
      }
  end

  def total_count
    @total_count ||= rand(10..100)
  end

  def current_user
    @current_user ||= User.find(self.arguments.first)
  end
end

@secretpray
Copy link
Author

Link

import { Controller } from "@hotwired/stimulus"
import { FetchRequest } from '@rails/request.js'


export default class extends Controller {
  static targets = [ "formSelect", "dynamicForm" ]

  async fetch_dynamic() {
    let selection = this.formSelectTarget.value
    const request = new FetchRequest('get', '/posts/new', { query: {type: selection}})
    const response = await request.perform()
    if (response.ok) {
      const form = await response.text
      const dynamic_form = this.dynamicFormTarget
      dynamic_form.innerHTML=form
    }
  }
}
= form_for @post, html: {'data-controller'=>'form'} do |f|

  = f.label :workflow 
  = f.select :workflow, options_for_select(@workflow_options), {}, 'data-action'=>'change->form#fetch_dynamic', 'data-form-target'=>'formSelect', id: 'form_select'
  %div{'data-form-target'=>'dynamicForm'}
    = f.label :information, "Enter your information"
    = f.text_field :information
  %br
  = f.submit "Create Post"
class PostsController < ApplicationController

# GET /posts/new
def new
  @post = Post.new
  @workflow_options = %i[foo bar baz qux] # different form partials
  requested_form = params[:type] # type of form partial specified in a query parameter
  if requested_form
    render partial: "#{requested_form}_form", layout: false
    return
  end
end

end
#_baz_form.html.haml

= label :post, :information, "Enter your baz information"
= text_field :post, :information

@secretpray
Copy link
Author

async uploadFileToServer(file) {
  // ...
  const response = await fetch("/test/upload", {
    // ...
  })
  
  const html = await response.text()
  Turbo.renderStreamMessage(html)
}

@secretpray
Copy link
Author

Turbo.visit(url, { frame: '_top', action: 'advance' })

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