Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Turbo frames conditional rendering #378

Closed
james-reading opened this issue Sep 6, 2021 · 6 comments · Fixed by #431
Closed

Turbo frames conditional rendering #378

james-reading opened this issue Sep 6, 2021 · 6 comments · Fixed by #431

Comments

@james-reading
Copy link

james-reading commented Sep 6, 2021

I want to stick a form in a modal, using a link that will request the new action to render the form HTML which then gets injected into the modal and opened.

This is a pretty common pattern that I've implemented many times before with Rails UJS.

I am now trying to do the same thing with Turbo.

<%# index.html.erb %>

<%= link_to 'New Post', new_post_path, data: { 'turbo-frame': 'modal-body' } %>

<div class="modal" data-controller="modal">
  <%= turbo_frame_tag "modal-body", 'data-action': 'turbo:frame-render->modal#show' %>
</div>
<%# new.html.erb %>

<h1>New Post</h1>

<%= turbo_frame_tag "modal-body" do %>
  <%= form_with(model: @post) do |form| %>
    <div class="field">
      <%= form.label :title %>
      <%= form.text_field :title %>
    </div>

    <div class="actions">
      <%= form.submit %>
    </div>
  <% end %>
<% end %>

<%= link_to 'Show', @post %> |
<%= link_to 'Back', posts_path %>

So I have a pre-rendered empty modal with turbo_frame_tag in the modal body. When I click the link, the controller renders the new template which has the form wrapped in a matching turbo_frame_tag. The form gets injected into the modal and I have a stimulus controller which opens the modal on turbo:frame-render.

This works pretty well, but there seems to be a pretty big limitation - The form HTML has to be identical for both the modal, and for when I visit the new URL directly. For example, if I need a close button when in a modal, but a cancel link when visiting the page directly, this doesn't seem possible to handle server-side.

With Rails UJS, My new controller action could respond to format.js for the modal and format.html when visiting directly, making it possible to render different HTML. As new action is a GET, this rules out format.turbo_stream which is for non-GET requests only.

@acetinick
Copy link

Hi @james-reading , this might be a duplicate of this one #257 ?

@james-reading
Copy link
Author

james-reading commented Sep 7, 2021

Hi @james-reading , this might be a duplicate of this one #257 ?

@acetinick Hmm I think it might be slightly different. I am not talking about conditionally setting the response turbo frame. I mean conditionally rendering different HTML based on the request turbo frame.

What I'd like to do is something like

class PostsController < ApplicationController
  def new
    @post = Post.new

    case request.headers['Turbo-Frame']
    when 'modal-body'
      render partial: 'posts/modal_body'
    when nil
      render :new
    end
  end
end

This works pretty well, but I also need to have the same case statement in the create action when the save returns false, except with status: :unprocessable_entity for it to work properly.

Something like this seems like it would be a double win because not only does it mean I can render different HTML based on the requesting frame, but the server also doesn't have to spend time rendering HTML that ultimately won't be rendered in the browser.

@tleish
Copy link
Contributor

tleish commented Sep 7, 2021

Are you suggesting adding a turbo-frame mime type?:

respond_to do |format|
  format.turbo_frame
  format.html
end

In your scenario I prefer to use the same HTML (perhaps with CSS), but show/hide elements

<% if turbo_frame_request? %>
  <a href="#" title="Close">X</a>  
<% end %>

I'm not as worried about some of the extra HTML it might include.

If you want to get to that level, you can still use:

class PostsController < ApplicationController
  def new
    @post = Post.new
    if turbo_frame_request?
      render partial: 'posts/modal_body'
    else
      render :new
    end
  end

  def create
    respond_to do |format|
      format.turbo_stream
      format.html { redirect_to ... }
    end
  end
end

@tleish
Copy link
Contributor

tleish commented Sep 7, 2021

Since turbo_frame response is really an HTML mime-type, adding a custom mime type just for turbo-frames wouldn't be a good solution (although, I guess you could make that same argument for turbo-streams being html). You could implement an html variant (search for variants in ActionController::MimeResponds):

Example:

class ApplicationController < ActionController::Base
  private

  def turbo_frame_request_variant
    request.variant = :turbo_frame if turbo_frame_request?
  end
end

class PostsController < ApplicationController
  before_action :turbo_frame_request_variant, only: %i[new create]

  def new
    @post = Post.new
    respond_to do |format|
      format.html do |variant|
        variant.turbo_frame # renders app/views/posts/new.html+turbo_frame.erb
        variant.none # renders app/views/posts/new.html.erb
      end
    end
  end

  def create
    respond_to do |format|
      format.turbo_stream do |variant|
        variant.turbo_frame { redirect_to ... }
        variant.none { redirect_to ... }
      end
      format.html { redirect_to ... }
    end
  end
end

@james-reading
Copy link
Author

Thanks @tleish, variants work brilliants actually. It works without the boilerplate format.html do |variant| in the controller too, as long as the variant is set in the before action it renders the variant template if it exists.

Is there any harm to putting turbo_frame_request_variant in ApplicationController before all request?

@tleish
Copy link
Contributor

tleish commented Sep 8, 2021

I suggested this as a default in the turbo-rails gem

see: hotwired/turbo-rails#229

seanpdoyle added a commit to seanpdoyle/turbo that referenced this issue Oct 31, 2021
The problem
---

The rendering process during a page-wide navigation is opaque, and
cannot be extended. Proposals have been made to use third-party
rendering tools like [morphdom][], or other animation libraries.

Outside of the HTML manipulation, Turbo is also responsible for loading
script, transposing permanent elements, etc.

How might these tools integrate with Turbo in a way that's compliant
with permanent elements.

The solution
---

When publishing a `turbo:before-render` event, dispatch it with a
`render()` function property in addition to the `resume()`.

This way, consumer applications can override rendering:

```html
import morphdom from "morphdom"

addEventListener("turbo:before-render", ({ detail }) => {
  detail.render = (currentElement, newElement) => {
    morphdom(currentElement, newElement)
  }

  // or, more tersely
  detail.render = morphdom
})
```

Potentially Closes [hotwired#197][]
Potentially Closes [hotwired#378][]
Potentially Closes [hotwired#218][]

[morphdom]: https://github.com/patrick-steele-idem/morphdom
[hotwired#218]: hotwired#218
[hotwired#378]: hotwired#378
[hotwired#197]: hotwired#197
seanpdoyle added a commit to seanpdoyle/turbo that referenced this issue Nov 11, 2021
The problem
---

The rendering process during a page-wide navigation is opaque, and
cannot be extended. Proposals have been made to use third-party
rendering tools like [morphdom][], or other animation libraries.

Outside of the HTML manipulation, Turbo is also responsible for loading
script, transposing permanent elements, etc.

How might these tools integrate with Turbo in a way that's compliant
with permanent elements.

The solution
---

When publishing a `turbo:before-render` event, dispatch it with a
`render()` function property in addition to the `resume()`.

This way, consumer applications can override rendering:

```html
import morphdom from "morphdom"

addEventListener("turbo:before-render", ({ detail }) => {
  detail.render = (currentElement, newElement) => {
    morphdom(currentElement, newElement)
  }

  // or, more tersely
  detail.render = morphdom
})
```

Potentially Closes [hotwired#197][]
Potentially Closes [hotwired#378][]
Potentially Closes [hotwired#218][]

[morphdom]: https://github.com/patrick-steele-idem/morphdom
[hotwired#218]: hotwired#218
[hotwired#378]: hotwired#378
[hotwired#197]: hotwired#197
seanpdoyle added a commit to seanpdoyle/turbo that referenced this issue Jul 16, 2022
The problem
---

The rendering process during a page-wide navigation is opaque, and
cannot be extended. Proposals have been made to use third-party
rendering tools like [morphdom][], or other animation libraries.

Outside of the HTML manipulation, Turbo is also responsible for loading
script, transposing permanent elements, etc.

How might these tools integrate with Turbo in a way that's compliant
with permanent elements.

The solution
---

When publishing a `turbo:before-render` event, dispatch it with a
`render()` function property in addition to the `resume()`.

This way, consumer applications can override rendering:

```html
import morphdom from "morphdom"

addEventListener("turbo:before-render", ({ detail }) => {
  detail.render = (currentElement, newElement) => {
    morphdom(currentElement, newElement)
  }

  // or, more tersely
  detail.render = morphdom
})
```

Potentially Closes [hotwired#197][]
Potentially Closes [hotwired#378][]
Potentially Closes [hotwired#218][]

[morphdom]: https://github.com/patrick-steele-idem/morphdom
[hotwired#218]: hotwired#218
[hotwired#378]: hotwired#378
[hotwired#197]: hotwired#197
seanpdoyle added a commit to seanpdoyle/turbo that referenced this issue Jul 16, 2022
The problem
---

The rendering process during a page-wide navigation is opaque, and
cannot be extended. Proposals have been made to use third-party
rendering tools like [morphdom][], or other animation libraries.

Outside of the HTML manipulation, Turbo is also responsible for loading
script, transposing permanent elements, etc.

How might these tools integrate with Turbo in a way that's compliant
with permanent elements.

The solution
---

When publishing a `turbo:before-render` event, dispatch it with a
`render()` function property in addition to the `resume()`.

This way, consumer applications can override rendering:

```html
import morphdom from "morphdom"

addEventListener("turbo:before-render", ({ detail }) => {
  detail.render = (currentElement, newElement) => {
    morphdom(currentElement, newElement)
  }

  // or, more tersely
  detail.render = morphdom
})
```

Potentially Closes [hotwired#197][]
Potentially Closes [hotwired#378][]
Potentially Closes [hotwired#218][]

[morphdom]: https://github.com/patrick-steele-idem/morphdom
[hotwired#218]: hotwired#218
[hotwired#378]: hotwired#378
[hotwired#197]: hotwired#197
seanpdoyle added a commit to seanpdoyle/turbo that referenced this issue Jul 16, 2022
The problem
---

The rendering process during a page-wide navigation is opaque, and
cannot be extended. Proposals have been made to use third-party
rendering tools like [morphdom][], or other animation libraries.

Outside of the HTML manipulation, Turbo is also responsible for loading
script, transposing permanent elements, etc.

How might these tools integrate with Turbo in a way that's compliant
with permanent elements.

The solution
---

When publishing a `turbo:before-render` event, dispatch it with a
`render()` function property in addition to the `resume()`.

This way, consumer applications can override rendering:

```html
import morphdom from "morphdom"

addEventListener("turbo:before-render", ({ detail }) => {
  detail.render = (currentElement, newElement) => {
    morphdom(currentElement, newElement)
  }

  // or, more tersely
  detail.render = morphdom
})
```

Potentially Closes [hotwired#197][]
Potentially Closes [hotwired#378][]
Potentially Closes [hotwired#218][]

[morphdom]: https://github.com/patrick-steele-idem/morphdom
[hotwired#218]: hotwired#218
[hotwired#378]: hotwired#378
[hotwired#197]: hotwired#197
seanpdoyle added a commit to seanpdoyle/turbo that referenced this issue Jul 17, 2022
The problem
---

The rendering process during a page-wide navigation is opaque, and
cannot be extended. Proposals have been made to use third-party
rendering tools like [morphdom][], or other animation libraries.

Outside of the HTML manipulation, Turbo is also responsible for loading
script, transposing permanent elements, etc.

How might these tools integrate with Turbo in a way that's compliant
with permanent elements.

The solution
---

When publishing a `turbo:before-render` event, dispatch it with a
`render()` function property in addition to the `resume()`.

This way, consumer applications can override rendering:

```html
import morphdom from "morphdom"

addEventListener("turbo:before-render", ({ detail }) => {
  detail.render = (currentElement, newElement) => {
    morphdom(currentElement, newElement)
  }

  // or, more tersely
  detail.render = morphdom
})
```

Potentially Closes [hotwired#197][]
Potentially Closes [hotwired#378][]
Potentially Closes [hotwired#218][]

[morphdom]: https://github.com/patrick-steele-idem/morphdom
[hotwired#218]: hotwired#218
[hotwired#378]: hotwired#378
[hotwired#197]: hotwired#197
seanpdoyle added a commit to seanpdoyle/turbo that referenced this issue Jul 18, 2022
The problem
---

The rendering process during a page-wide navigation is opaque, and
cannot be extended. Proposals have been made to use third-party
rendering tools like [morphdom][], or other animation libraries.

Outside of the HTML manipulation, Turbo is also responsible for loading
script, transposing permanent elements, etc.

How might these tools integrate with Turbo in a way that's compliant
with permanent elements.

The solution
---

When publishing a `turbo:before-render` event, dispatch it with a
`render()` function property in addition to the `resume()`.

This way, consumer applications can override rendering:

```html
import morphdom from "morphdom"

addEventListener("turbo:before-render", ({ detail }) => {
  detail.render = (currentElement, newElement) => {
    morphdom(currentElement, newElement)
  }

  // or, more tersely
  detail.render = morphdom
})
```

Potentially Closes [hotwired#197][]
Potentially Closes [hotwired#378][]
Potentially Closes [hotwired#218][]

[morphdom]: https://github.com/patrick-steele-idem/morphdom
[hotwired#218]: hotwired#218
[hotwired#378]: hotwired#378
[hotwired#197]: hotwired#197
seanpdoyle added a commit to seanpdoyle/turbo that referenced this issue Jul 18, 2022
The problem
---

The rendering process during a page-wide navigation is opaque, and
cannot be extended. Proposals have been made to use third-party
rendering tools like [morphdom][], or other animation libraries.

Outside of the HTML manipulation, Turbo is also responsible for loading
script, transposing permanent elements, etc.

How might these tools integrate with Turbo in a way that's compliant
with permanent elements.

The solution
---

When publishing a `turbo:before-render` event, dispatch it with a
`render()` function property in addition to the `resume()`.

This way, consumer applications can override rendering:

```html
import morphdom from "morphdom"

addEventListener("turbo:before-render", ({ detail }) => {
  detail.render = (currentElement, newElement) => {
    morphdom(currentElement, newElement)
  }

  // or, more tersely
  detail.render = morphdom
})
```

Potentially Closes [hotwired#197][]
Potentially Closes [hotwired#378][]
Potentially Closes [hotwired#218][]

[morphdom]: https://github.com/patrick-steele-idem/morphdom
[hotwired#218]: hotwired#218
[hotwired#378]: hotwired#378
[hotwired#197]: hotwired#197
@dhh dhh closed this as completed in #431 Jul 18, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging a pull request may close this issue.

3 participants