Skip to content

Commit 5652d31

Browse files
committed
Move example post to new action, handle redirect from frame
Now UX is to click a "New post" button that will render the post form dynamically in a target turbo frame. The Stimulus refresh controller is renamed as it now needs to handle a special redirect use case; on successful submission of the POST from the "examples_post_form" we want to render an updated list in the "examples_posts" turbo frame. The framework does not yet provide an intuitive mechanism for breaking out of a frame on redirect, so we use a Stimulus Turbo.visit to the "examples_post" form here. This does result in a "double GET" request, the first results in re-rendering the requesting "examples_post_form", but the second is needed to re-render the "examples_posts" frame. There are other approaches, described in the resources below, which also lay out the problem in more detail: - https://www.ducktypelabs.com/turbo-break-out-and-redirect/ - hotwired/turbo-rails#367 (comment) - https://discuss.hotwired.dev/t/break-out-of-a-frame-during-form-redirect/1562/26
1 parent 9e43d9d commit 5652d31

File tree

9 files changed

+239
-83
lines changed

9 files changed

+239
-83
lines changed
Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
11
class Examples::PostsController < ApplicationController
22
def index
3-
post = Examples::Post.new(post_params_new)
3+
render Examples::Posts::IndexView.new(posts: Examples::Post.limit(25).order(created_at: :desc))
4+
end
5+
6+
def new
7+
post = Examples::Post.new(post_params_permitted)
48

5-
render Examples::Posts::IndexView.new(post: post)
9+
render Examples::Posts::NewView.new(post: post)
610
end
711

812
def create
9-
post = Examples::Post.new(post_create_params)
13+
post = Examples::Post.new(post_params_permitted)
1014

1115
if post.save
12-
redirect_to :index
16+
redirect_to examples_posts_path
1317
else
14-
render Examples::Posts::IndexView.new(post: post), status: :unprocessable_entity
18+
render Examples::Posts::NewView.new(post: post), status: :unprocessable_entity
1519
end
1620
end
1721

1822
private
1923

20-
def post_create_params = params.require(:examples_post, {}).permit(:title)
21-
2224
def post_params = params.fetch(:examples_post, {})
2325

24-
def post_params_new
26+
def post_params_permitted
2527
post_params.permit(:title, :postable_type, link_attributes: [:url], image_attributes: [:url], markdown_attributes: [:body])
2628
end
2729
end
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Controller } from '@hotwired/stimulus';
2+
import debug from '../../utils/debug';
3+
4+
const console = debug('app:javascript:controllers:forms:frame');
5+
6+
export default class extends Controller {
7+
static targets = ['refreshButton'];
8+
static values = {
9+
redirectFrame: String,
10+
};
11+
12+
connect() {
13+
console.log('Connect!');
14+
this.refreshButtonTarget.hidden = true;
15+
}
16+
17+
refresh() {
18+
console.log('Refresh!');
19+
this.refreshButtonTarget.click();
20+
}
21+
22+
redirect(event) {
23+
if (this.refreshButtonTarget === event.detail.formSubmission.submitter)
24+
return;
25+
26+
if (event.detail.success) {
27+
const fetchResponse = event.detail.fetchResponse;
28+
history.pushState(
29+
{ turbo_frame_history: true },
30+
'',
31+
fetchResponse.response.url,
32+
);
33+
console.log('Redirect!', fetchResponse.response.url, {
34+
frame: this.redirectFrameValue,
35+
});
36+
Turbo.visit(fetchResponse.response.url, {
37+
frame: this.redirectFrameValue,
38+
});
39+
}
40+
}
41+
}

app/javascript/controllers/forms/refresh.js

Lines changed: 0 additions & 16 deletions
This file was deleted.

app/javascript/controllers/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import PwaWebPushDemo from './pwa/web-push-demo';
1515
import AnalyticsCustomEvent from './analytics/custom-event';
1616
import TableOfContents from './table-of-contents';
1717

18-
import FormRefresh from './forms/refresh';
18+
import FrameForm from './forms/frame';
1919

2020
application.register('analytics', AnalyticsCustomEvent);
2121
application.register('code-example', CodeExample);
@@ -27,4 +27,4 @@ application.register('pwa-web-push-subscription', PwaWebPushSubscription);
2727
application.register('pwa-web-push-demo', PwaWebPushDemo);
2828
application.register('table-of-contents', TableOfContents);
2929

30-
application.register('form-refresh', FormRefresh);
30+
application.register('frame-form', FrameForm);

app/views/examples/posts/form.rb

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# frozen_string_literal: true
2+
3+
class Examples::Posts::Form < ApplicationComponent
4+
include Phlex::Rails::Helpers::FormWith
5+
include Phlex::Rails::Helpers::LinkTo
6+
include Phlex::Rails::Helpers::TurboFrameTag
7+
include Phlex::Rails::Helpers::ClassNames
8+
9+
def initialize(post:)
10+
@post = post
11+
@image = post.image || Examples::Posts::Image.new
12+
@link = post.link || Examples::Posts::Link.new
13+
@markdown = post.markdown || Examples::Posts::Markdown.new
14+
@post.postable ||= @markdown
15+
end
16+
17+
def view_template
18+
form_with model: @post,
19+
url: examples_posts_path,
20+
method: :post,
21+
data: {
22+
controller: "frame-form",
23+
action: "turbo:submit-end->frame-form#redirect",
24+
frame_form_redirect_frame_value: "examples_posts"
25+
} do |form|
26+
div(class: "tabs") do
27+
form.label :postable_type, "Text", value: @markdown.class.name, class: "tab"
28+
form.radio_button :postable_type, @markdown.class.name, checked: markdown?, data: {action: "change->frame-form#refresh"}
29+
form.label :postable_type, "Link", value: @link.class.name, class: "tab"
30+
form.radio_button :postable_type, @link.class.name, checked: link?, data: {action: "frame-form#refresh"}
31+
form.label :postable_type, "Image", value: @image.class.name, class: "tab"
32+
form.radio_button :postable_type, @image.class.name, checked: image?, data: {action: "frame-form#refresh"}
33+
end
34+
35+
fieldset do
36+
form.label :title
37+
form.text_field :title, placeholder: "My Post 🎸", required: false
38+
end
39+
40+
form.fields_for :markdown, @markdown do |postable_form|
41+
fieldset(class: class_names(hidden: !markdown?)) do
42+
postable_form.label :body, "Text (Markdown)"
43+
postable_form.text_area :body, placeholder: "# This is a post about guitars 🎸", required: false
44+
end
45+
end
46+
form.fields_for :link, @link do |postable_form|
47+
fieldset(class: class_names(hidden: !link?)) do
48+
postable_form.label :url, "Link URL"
49+
postable_form.text_field :url, placeholder: "https://example.com", required: false
50+
end
51+
end
52+
form.fields_for :image, @image do |postable_form|
53+
fieldset(class: class_names(hidden: !image?)) do
54+
postable_form.label :url, "Image URL"
55+
postable_form.text_field :url, placeholder: "https://example.com/image.jpg", required: false
56+
end
57+
end
58+
59+
fieldset do
60+
form.submit "Refresh",
61+
class: "button primary hidden",
62+
formaction: new_examples_post_path,
63+
formmethod: "get",
64+
formnovalidate: true,
65+
data: {frame_form_target: "refreshButton"}
66+
end
67+
68+
fieldset do
69+
form.submit "Save", class: "button primary"
70+
end
71+
end
72+
end
73+
74+
private
75+
76+
def markdown? = @post.postable == @markdown
77+
78+
def link? = @post.postable == @link
79+
80+
def image? = @post.postable == @image
81+
end
Lines changed: 25 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
# frozen_string_literal: true
22

33
class Examples::Posts::IndexView < ApplicationView
4+
include Phlex::Rails::Helpers::ClassNames
5+
include Phlex::Rails::Helpers::ContentFor
46
include Phlex::Rails::Helpers::FormWith
57
include Phlex::Rails::Helpers::LinkTo
68
include Phlex::Rails::Helpers::TurboFrameTag
7-
include Phlex::Rails::Helpers::ClassNames
9+
include Phlex::Rails::Helpers::TurboRefreshesWith
810

9-
def initialize(post:)
10-
@post = post
11-
@image = post.image || Examples::Posts::Image.new
12-
@link = post.link || Examples::Posts::Link.new
13-
@markdown = post.markdown || Examples::Posts::Markdown.new
14-
@post.postable ||= @markdown
11+
def initialize(posts:)
12+
@posts = posts
1513
end
1614

1715
def view_template
@@ -21,63 +19,34 @@ def view_template
2119
)
2220

2321
div(class: "section-content container py-gap") do
24-
turbo_frame_tag :examples_post_form do
25-
form_with model: @post, url: examples_posts_path, method: :post, data: {controller: "form-refresh"} do |form|
26-
div(class: "tabs") do
27-
form.label :postable_type, "Text", value: @markdown.class.name, class: "tab"
28-
form.radio_button :postable_type, @markdown.class.name, checked: markdown?, data: {action: "form-refresh#refresh"}
29-
form.label :postable_type, "Link", value: @link.class.name, class: "tab"
30-
form.radio_button :postable_type, @link.class.name, checked: link?, data: {action: "form-refresh#refresh"}
31-
form.label :postable_type, "Image", value: @image.class.name, class: "tab"
32-
form.radio_button :postable_type, @image.class.name, checked: image?, data: {action: "form-refresh#refresh"}
33-
end
22+
link_to "New Post", new_examples_post_path, class: "button primary", data: {turbo_frame: "examples_post_form"}
3423

35-
fieldset do
36-
form.label :title
37-
form.text_field :title, placeholder: "My Post 🎸", required: false
38-
end
24+
turbo_frame_tag :examples_post_form
3925

40-
form.fields_for :markdown, @markdown do |postable_form|
41-
fieldset(class: class_names(hidden: !markdown?)) do
42-
postable_form.label :body, "Text (Markdown)"
43-
postable_form.text_area :body, placeholder: "# This is a post about guitars 🎸", required: false
44-
end
45-
end
46-
form.fields_for :link, @link do |postable_form|
47-
fieldset(class: class_names(hidden: !link?)) do
48-
postable_form.label :url, "Link URL"
49-
postable_form.text_field :url, placeholder: "https://example.com", required: false
50-
end
26+
turbo_frame_tag :examples_posts, refresh: "morph" do
27+
if @posts.empty?
28+
div do
29+
p { "Nothing has been posted yet!" }
5130
end
52-
form.fields_for :image, @image do |postable_form|
53-
fieldset(class: class_names(hidden: !image?)) do
54-
postable_form.label :url, "Image URL"
55-
postable_form.text_field :url, placeholder: "https://example.com/image.jpg", required: false
31+
end
32+
@posts.each do |post|
33+
div do
34+
h2 { post.title }
35+
case post.postable
36+
when Examples::Posts::Link
37+
a(href: post.postable.url, rel: "noopener noreferrer", target: "_blank") { post.postable.url }
38+
when Examples::Posts::Image
39+
img(src: post.postable.url, alt: post.title)
40+
when Examples::Posts::Markdown
41+
markdown(post.postable.body)
5642
end
5743
end
58-
59-
fieldset do
60-
form.submit "Refresh",
61-
class: "button primary",
62-
formaction: examples_posts_path,
63-
formmethod: "get",
64-
formnovalidate: true,
65-
data: {form_refresh_target: "refreshButton"}
66-
end
67-
68-
fieldset do
69-
form.submit "Save", class: "button primary"
70-
end
7144
end
7245
end
7346
end
7447
end
7548

76-
private
77-
78-
def markdown? = @post.postable == @markdown
79-
80-
def link? = @post.postable == @link
81-
82-
def image? = @post.postable == @image
49+
def markdown(content)
50+
render Markdown::Base.new(content)
51+
end
8352
end

app/views/examples/posts/new_view.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# frozen_string_literal: true
2+
3+
class Examples::Posts::NewView < ApplicationView
4+
include Phlex::Rails::Helpers::FormWith
5+
include Phlex::Rails::Helpers::LinkTo
6+
include Phlex::Rails::Helpers::TurboFrameTag
7+
include Phlex::Rails::Helpers::ClassNames
8+
9+
def initialize(post:)
10+
@post = post
11+
end
12+
13+
def view_template
14+
render Pages::Header.new(
15+
title: "Examples: New Post",
16+
description: "This is an example for creating a post of different types, like text, link, or image."
17+
)
18+
19+
div(class: "section-content container py-gap") do
20+
turbo_frame_tag :examples_post_form do
21+
render Examples::Posts::Form.new(post: @post)
22+
end
23+
end
24+
end
25+
end

config/routes.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
namespace :examples do
1414
resource :counters, only: [:show, :update, :destroy]
1515
resource :hello, only: [:show]
16-
resources :posts, only: [:index, :create]
16+
resources :posts, only: [:index, :create, :new]
1717
end
1818
resources :examples, only: [:index, :show]
1919

0 commit comments

Comments
 (0)