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

secretpray commented Dec 11, 2022

/* turbo_form_submit_redirect_controller.js */
import { Controller } from '@hotwired/stimulus'
import * as Turbo from "@hotwired/turbo"

export default class extends Controller {
  connect() {
    this.element.addEventListener("turbo:submit-end", (event) => {
      this.next(event)
    })
  }

  next(event) {
    if (event.detail.success) {
      Turbo.visit(event.detail.fetchResponse.response.url)
    }
  }
}

@secretpray
Copy link
Author

Plain in controller

def search
    query = params[:search].presence || '*'
    @results = Post.search(query)

    render turbo_stream: 
      turbo_stream.update('posts', partial: 'posts/posts', locals: { posts: @results})

end 

@secretpray
Copy link
Author

Turbo.visit("/uk/wizard_sign_in", { 
  action: "replace", 
  target: document.querySelector("#new_order_form"), 
  "turbo-frame": "new_order_form"
}); 
Turbo.visit("/uk/identity/new", { 
  action: "replace", 
  "turbo-frame": "new_order_form"
});
# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
  def show
    @order = Order.find(params[:id])
    respond_to do |format|
      format.html
      format.turbo_stream { render turbo_stream: turbo_stream.append(:order_details, partial: 'orders/details', locals: { order: @order }) }
    end
  end
end

In this example, the show action responds to a turbo_stream format and renders the details partial as a turbo_stream.append operation. The details partial should contain the HTML that you want to append to the DOM.

You can then call Turbo.visit from a Stimulus controller or another JavaScript function to trigger the partial to be appended to the DOM. For example:

// app/javascript/controllers/order_controller.js
import { Controller } from 'stimulus'

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

  async showDetails() {
    const response = await Turbo.visit(`/orders/${this.orderId}`, { turbo: false })
    const html = response.detail[0].template
    this.detailsTarget.innerHTML = html
  }

  get orderId() {
    return this.data.get('orderId')
  }
}

In this example, the showDetails method is triggered by a button click or other event. It calls Turbo.visit to navigate to the show action for the order and returns the response. The html variable is set to the HTML of the turbo_stream.append operation, which is then inserted into the DOM using innerHTML.

@secretpray
Copy link
Author

Example 1: Updating a single element

const element = document.querySelector('#my-element')

// Make a request to the server and replace the contents of the element with the response
Turbo.visit('/my-page', { target: element })

In this example, Turbo.visit makes a request to the server for the /my-page URL and replaces the contents of the element with the id my-element with the response.

Example 2: Replacing an entire page

// Replace the entire current page with the response from the server
Turbo.visit('/new-page', { action: 'replace' })

In this example, Turbo.visit makes a request to the server for the /new-page URL and replaces the entire current page with the response.

Example 3: Appending a new element

const container = document.querySelector('#my-container')

// Make a request to the server and append the response as a new element inside the container
Turbo.visit('/new-element', { action: 'append', target: container })

In this example, Turbo.visit makes a request to the server for the /new-element URL and appends the response as a new element inside the element with the id my-container.

Example 4: Updating a form

const form = document.querySelector('#my-form')

// Make a request to the server and update the form with the response
Turbo.visit('/new-form-data', { action: 'replace', target: form })

In this example, Turbo.visit makes a request to the server for the /new-form-data URL and updates the form with the response.

Example 5: Modifying the browser history

// Make a request to the server and replace the current history entry with the response
Turbo.visit('/new-page', { action: 'replace', history: true })

In this example, Turbo.visit makes a request to the server for the /new-page URL and replaces the current history entry with the response. This allows the user to use the back button to navigate back to the previous page.

@secretpray
Copy link
Author

secretpray commented Mar 28, 2023

Example 1: Using anchor with Turbo.visit

// Make a request to the server for the page with the anchor, and scroll to the anchor when the response is received
Turbo.visit('/my-page#my-anchor', { action: 'replace', scroll: true })

In this example, Turbo.visit makes a request to the server for the /my-page URL with the anchor #my-anchor, and replaces the current page with the response. The scroll option is set to true, which scrolls the page to the #my-anchor element when the response is received.

Example 2: Using turbo_frame with Turbo.visit

// Make a request to the server and replace the contents of the 'my-frame' turbo_frame with the response
Turbo.visit('/my-page', { action: 'replace', target: 'my-frame' })

In this example, Turbo.visit makes a request to the server for the /my-page URL and replaces the contents of the turbo_frame with the id my-frame with the response. Note that the target option is used to specify the turbo_frame to update.

Example 3: Using turbo_frame with append action

// Make a request to the server and append the response to the contents of the 'my-frame' turbo_frame
Turbo.visit('/my-page', { action: 'append', target: 'my-frame' })

In this example, Turbo.visit makes a request to the server for the /my-page URL and appends the response as a new element inside the turbo_frame with the id my-frame. Note that the action option is set to append to specify that the response should be appended to the current contents of the turbo_frame.

Example 4: Using turbo_frame with a form

const form = document.querySelector('#my-form')

// Make a request to the server with the form data and replace the contents of the 'my-frame' turbo_frame with the response
Turbo.visit(form.action, { action: 'replace', target: 'my-frame', body: new FormData(form) })

In this example, Turbo.visit makes a request to the server with the data from the form with the id my-form. The action option is set to replace to specify that the response should replace the current contents of the turbo_frame with the id my-frame. The body option is used to send the form data with the request.

@secretpray
Copy link
Author

secretpray commented Mar 28, 2023

Turbo.visit(url, data: {"turbo-frame"=> "my_frame"})

When you redirect after the form submission, there is no with the same id, so you get frame missing error. For some details and my first attempt at a reusable solution:

https://stackoverflow.com/a/75704489/207090

I don't want to post a duplicate answer, but I'll mention that there is a turbo:frame-missing event, that you can listen for and handle the error:

document.addEventListener("turbo:frame-missing", (event) => {
  const { detail: { response, visit } } = event;
  event.preventDefault();
  visit(response.url);
});

One caveat with doing a simple visit(response.url) is that it will be the second request to that url (first one is from the redirect).

The ideal place to handle this would be at the redirect response, because that's where you'd know if form submission was successful or not and send redirect as a regular Turbo.visit outside of the frame. Which you can do by sending some javascript in a turbo_stream:

render turbo_stream: turbo_stream.append(
  request.headers["Turbo-Frame"],
  helpers.javascript_tag(
    "Turbo.visit('#{model_url(@model)}')",
    data: {turbo_temporary: true} # one time only; don't cache it
  )
)

Make a custom turbo_stream.redirect action

Tag for "redirect" action would look like this:


Add new action to Turbo to handle it on the front end:
https://turbo.hotwired.dev/handbook/streams#custom-actions

// app/javascript/application.js

import { Turbo } from "@hotwired/turbo-rails";

Turbo.StreamActions.redirect = function () {
  Turbo.visit(this.target);
};

// or using event listener

document.addEventListener("turbo:before-stream-render", (event) => {
  const fallbackToDefaultActions = event.detail.render;
  event.detail.render = function (streamElement) {
    if (streamElement.action == "redirect") {
      Turbo.visit(streamElement.target);
    } else {
      fallbackToDefaultActions(streamElement);
    }
  };
});

Use generic action method to render it in a controller or in a template:

turbo_stream.action(:redirect, model_url(@model))

You can also add redirect method to turbo_stream helper:

# config/initializers/stream_actions.rb

module CustomTurboStreamActions
  # turbo_stream.redirect(model_url(@model))
  def redirect url
    action(:redirect, url)
  end
end
Turbo::Streams::TagBuilder.include(CustomTurboStreamActions)
Example of how to get out of the frame:

# app/controllers/*_controller.rb

def update
  respond_to do |format|
    if @model.update(model_params)
      format.turbo_stream do
        render turbo_stream: turbo_stream.redirect(model_url(@model)) 
      end
      format.html { redirect_to model_url(@model), notice: "Updated." }
    else
      format.html { render :edit, status: :unprocessable_entity }
    end
  end
end

https://stackoverflow.com/questions/75738570/getting-a-turbo-frame-error-of-content-missing

@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