From 6194793c955d3d5d96fda12d405f8e9152f7fdaa Mon Sep 17 00:00:00 2001 From: theforestvn88 Date: Tue, 21 May 2024 15:01:42 +0700 Subject: [PATCH 1/2] demo react-over-hotwired and proof #1508 fixed --- Gemfile | 2 +- app/controllers/comments_controller.rb | 5 + app/controllers/pages_controller.rb | 4 + app/views/comments/_form_1508.html.erb | 30 +++++ app/views/comments/create.turbo_stream.erb | 10 ++ app/views/comments/new.turbo_stream.erb | 7 + app/views/comments/test_1508.turbo_stream.erb | 3 + app/views/pages/hotwired.html.erb | 8 ++ .../CommentBox/CommentList/CommentList.jsx | 2 +- .../HotwiredCommentForm.jsx | 120 ++++++++++++++++++ .../HotwiredCommentScreen.jsx | 87 +++++++++++++ .../HotwiredCommentScreen.module.scss | 17 +++ .../NavigationBar/NavigationBar.jsx | 8 ++ .../app/bundles/comments/constants/paths.js | 1 + client/app/packs/client-bundle.js | 6 + config/routes.rb | 4 + 16 files changed, 312 insertions(+), 2 deletions(-) create mode 100644 app/views/comments/_form_1508.html.erb create mode 100644 app/views/comments/create.turbo_stream.erb create mode 100644 app/views/comments/new.turbo_stream.erb create mode 100644 app/views/comments/test_1508.turbo_stream.erb create mode 100644 app/views/pages/hotwired.html.erb create mode 100644 client/app/bundles/comments/components/HotwiredCommentScreen/HotwiredCommentForm.jsx create mode 100644 client/app/bundles/comments/components/HotwiredCommentScreen/HotwiredCommentScreen.jsx create mode 100644 client/app/bundles/comments/components/HotwiredCommentScreen/HotwiredCommentScreen.module.scss diff --git a/Gemfile b/Gemfile index 75e5aedfc..c81935b6a 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby "3.1.2" -gem "react_on_rails", "14.0.0" +gem "react_on_rails", path: '../react_on_rails' gem "shakapacker", "7.2.1" # Bundle edge Rails instead: gem "rails", github: "rails/rails" diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index d9b9934fe..e0ca6a6c5 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -33,6 +33,7 @@ def create format.html { redirect_to @comment, notice: I18n.t("Comment was successfully created.") } end format.json { render :show, status: :created, location: @comment } + format.turbo_stream else if turbo_frame_request? format.html @@ -99,6 +100,10 @@ def inline_form end end + def test_1508 + @comment = Comment.new + end + private def set_comments diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index c9f6543ec..6730abcb7 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -38,6 +38,10 @@ def simple; end def rescript; end + def hotwired + @props = comments_json_string + end + private def set_comments diff --git a/app/views/comments/_form_1508.html.erb b/app/views/comments/_form_1508.html.erb new file mode 100644 index 000000000..ef4280bce --- /dev/null +++ b/app/views/comments/_form_1508.html.erb @@ -0,0 +1,30 @@ +<%= form_for(@comment, html: { class: "flex flex-col gap-4" }) do |f| %> + <% if @comment.errors.any? %> +
+

<%= pluralize(comment.errors.count, "error") %> prohibited this comment from being saved:

+ + +
+ <% end %> + +
+ <%= f.label :author, 'Your Name' %>
+ <%= f.text_field :author, class: "px-3 py-1 leading-4 border border-gray-300 rounded" %> +
+
+ <%= f.label :text, 'Say something using markdown...' %>
+ <%= f.text_area :text, class: "px-3 py-1 leading-4 border border-gray-300 rounded" %> +
+
+ <%= f.submit 'Post', class: "self-start px-3 py-1 font-semibold border-0 rounded text-sky-50 bg-sky-600 hover:bg-sky-800 cursor-pointer" %> +
+ +

Below is a react component which render inside a form (that will proof #1508 is fixed), You can turn-off the flag `force_load` and this below form will not show.

+
+ <%= react_component('HotwiredCommentForm', props: { }, prerender: false, force_load: true) %> +
+<% end %> diff --git a/app/views/comments/create.turbo_stream.erb b/app/views/comments/create.turbo_stream.erb new file mode 100644 index 000000000..3d93e4b74 --- /dev/null +++ b/app/views/comments/create.turbo_stream.erb @@ -0,0 +1,10 @@ +<%= turbo_stream.prepend "comments" do %> +
+

<%= @comment.author %>

+ <%= markdown_to_html(@comment.text) %> +
+<% end %> + +<%= turbo_stream.update "comment-form" do %> + <%= link_to "New Comment", new_comment_path, data: {turbo_stream: true} %> +<% end %> \ No newline at end of file diff --git a/app/views/comments/new.turbo_stream.erb b/app/views/comments/new.turbo_stream.erb new file mode 100644 index 000000000..844cb6dc2 --- /dev/null +++ b/app/views/comments/new.turbo_stream.erb @@ -0,0 +1,7 @@ +<%= turbo_stream.update "comment-form" do %> +
+

New Comment

+ + <%= react_component('HotwiredCommentForm', props: { }, prerender: false, force_load: true) %> +
+<% end %> \ No newline at end of file diff --git a/app/views/comments/test_1508.turbo_stream.erb b/app/views/comments/test_1508.turbo_stream.erb new file mode 100644 index 000000000..fb704ace2 --- /dev/null +++ b/app/views/comments/test_1508.turbo_stream.erb @@ -0,0 +1,3 @@ +<%= turbo_stream.replace dom_id(@comment), target: '_top' do %> + <%= render partial: 'form_1508' %> +<% end %> \ No newline at end of file diff --git a/app/views/pages/hotwired.html.erb b/app/views/pages/hotwired.html.erb new file mode 100644 index 000000000..0a9db2a6a --- /dev/null +++ b/app/views/pages/hotwired.html.erb @@ -0,0 +1,8 @@ +

Demo React Over Hotwired

+ +<%= react_component('HotwiredCommentScreen', props: @props, prerender: false) %> + + +<%= turbo_frame_tag "new_comment" do %> + <%= link_to "proof that #1508 fixed", "/test_1508", data: {turbo_stream: true} %> +<% end %> \ No newline at end of file diff --git a/client/app/bundles/comments/components/CommentBox/CommentList/CommentList.jsx b/client/app/bundles/comments/components/CommentBox/CommentList/CommentList.jsx index 128f8878b..cc74ebf3c 100644 --- a/client/app/bundles/comments/components/CommentBox/CommentList/CommentList.jsx +++ b/client/app/bundles/comments/components/CommentBox/CommentList/CommentList.jsx @@ -78,7 +78,7 @@ export default class CommentList extends BaseComponent {
{this.errorWarning()} - + {commentNodes}
diff --git a/client/app/bundles/comments/components/HotwiredCommentScreen/HotwiredCommentForm.jsx b/client/app/bundles/comments/components/HotwiredCommentScreen/HotwiredCommentForm.jsx new file mode 100644 index 000000000..10e89092e --- /dev/null +++ b/client/app/bundles/comments/components/HotwiredCommentScreen/HotwiredCommentForm.jsx @@ -0,0 +1,120 @@ +// eslint-disable-next-line max-classes-per-file +import React from 'react'; +import request from 'axios'; +import Immutable from 'immutable'; +import _ from 'lodash'; +import ReactOnRails from 'react-on-rails'; +import { IntlProvider, injectIntl } from 'react-intl'; +import BaseComponent from 'libs/components/BaseComponent'; +import SelectLanguage from 'libs/i18n/selectLanguage'; +import { defaultMessages, defaultLocale } from 'libs/i18n/default'; +import { translations } from 'libs/i18n/translations'; + +import { Turbo } from '@hotwired/turbo-rails'; +import CommentForm from '../CommentBox/CommentForm/CommentForm'; +import css from './HotwiredCommentScreen.module.scss'; + +class HotwiredCommentForm extends BaseComponent { + constructor(props) { + super(props); + + this.state = { + isSaving: false, + submitCommentError: null, + }; + + _.bindAll(this, 'handleCommentSubmit'); + } + + componentDidMount() { + } + + handleCommentSubmit(comment) { + this.setState({ isSaving: true }); + + const requestConfig = { + responseType: 'text/vnd.turbo-stream.html', + headers: ReactOnRails.authenticityHeaders(), + }; + + return request + .post('comments.turbo_stream', { comment }, requestConfig) + .then(r => r.data) + .then(html => { + Turbo.renderStreamMessage(html) + }) + .then(() => { + const { $$comments } = this.state; + const $$comment = Immutable.fromJS(comment); + + this.setState({ + $$comments: $$comments.unshift($$comment), + submitCommentError: null, + isSaving: false, + }); + }) + .catch((error) => { + this.setState({ + submitCommentError: error, + isSaving: false, + }); + }); + } + + render() { + const { handleSetLocale, locale, intl } = this.props; + const cssTransitionGroupClassNames = { + enter: css.elementEnter, + enterActive: css.elementEnterActive, + exit: css.elementLeave, + exitActive: css.elementLeaveActive, + }; + + return ( +
+ {SelectLanguage(handleSetLocale, locale)} + + + +
+ ); + } +} + +export default class I18nWrapper extends BaseComponent { + constructor(props) { + super(props); + + this.state = { + locale: defaultLocale, + }; + + _.bindAll(this, 'handleSetLocale'); + } + + handleSetLocale(locale) { + this.setState({ locale }); + } + + render() { + const { locale } = this.state; + const messages = translations[locale]; + const InjectedHotwiredCommentForm = injectIntl(HotwiredCommentForm); + + return ( + + + + ); + } +} diff --git a/client/app/bundles/comments/components/HotwiredCommentScreen/HotwiredCommentScreen.jsx b/client/app/bundles/comments/components/HotwiredCommentScreen/HotwiredCommentScreen.jsx new file mode 100644 index 000000000..94278b42d --- /dev/null +++ b/client/app/bundles/comments/components/HotwiredCommentScreen/HotwiredCommentScreen.jsx @@ -0,0 +1,87 @@ +// eslint-disable-next-line max-classes-per-file +import React from 'react'; +import Immutable from 'immutable'; +import ReactOnRails from 'react-on-rails'; +import { IntlProvider, injectIntl } from 'react-intl'; +import BaseComponent from 'libs/components/BaseComponent'; +import SelectLanguage from 'libs/i18n/selectLanguage'; +import { defaultMessages, defaultLocale } from 'libs/i18n/default'; +import { translations } from 'libs/i18n/translations'; + +import CommentList from '../CommentBox/CommentList/CommentList'; +import css from './HotwiredCommentScreen.module.scss'; + +class HotwiredCommentScreen extends BaseComponent { + constructor(props) { + super(props); + + this.state = { + $$comments: Immutable.fromJS(props.comments), + }; + } + + componentDidMount() { + } + + render() { + const { handleSetLocale, locale, intl } = this.props; + const { formatMessage } = intl; + const cssTransitionGroupClassNames = { + enter: css.elementEnter, + enterActive: css.elementEnterActive, + exit: css.elementLeave, + exitActive: css.elementLeaveActive, + }; + + return ( +
+ +

{formatMessage(defaultMessages.comments)}

+ {SelectLanguage(handleSetLocale, locale)} + + + + +
+
+ ); + } +} + +export default class I18nWrapper extends BaseComponent { + constructor(props) { + super(props); + + this.state = { + locale: defaultLocale, + }; + + _.bindAll(this, 'handleSetLocale'); + } + + handleSetLocale(locale) { + this.setState({ locale }); + } + + render() { + const { locale } = this.state; + const messages = translations[locale]; + const InjectedHotwiredCommentScreen = injectIntl(HotwiredCommentScreen); + + return ( + + + + ); + } +} diff --git a/client/app/bundles/comments/components/HotwiredCommentScreen/HotwiredCommentScreen.module.scss b/client/app/bundles/comments/components/HotwiredCommentScreen/HotwiredCommentScreen.module.scss new file mode 100644 index 000000000..c96d339f3 --- /dev/null +++ b/client/app/bundles/comments/components/HotwiredCommentScreen/HotwiredCommentScreen.module.scss @@ -0,0 +1,17 @@ +.elementEnter { + opacity: 0.01; + + &.elementEnterActive { + opacity: 1; + transition: opacity $animation-duration ease-in; + } +} + +.elementLeave { + opacity: 1; + + &.elementLeaveActive { + opacity: 0.01; + transition: opacity $animation-duration ease-in; + } +} diff --git a/client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx b/client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx index 65dc3b402..639981041 100644 --- a/client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx +++ b/client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx @@ -81,6 +81,14 @@ function NavigationBar(props) { Simple React +
  • + + HotWired + +
  • "/cable" + + + get "test_1508", to: "comments#test_1508", format: :turbo_stream end From 4f7480ba323da7d8bc00f9c93fc7006e19fa909f Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 17 Mar 2026 16:47:22 -1000 Subject: [PATCH 2/2] Modernize hotwired demo and remove issue-1508 proof code --- app/controllers/comments_controller.rb | 11 ++----- app/controllers/pages_controller.rb | 2 +- app/views/comments/_form_1508.html.erb | 30 ----------------- app/views/comments/test1508.turbo_stream.erb | 3 -- app/views/pages/hotwired.html.erb | 4 --- .../ror_components/HotwiredCommentForm.jsx | 3 +- .../ror_components/HotwiredCommentScreen.jsx | 3 +- .../app/bundles/comments/constants/paths.js | 2 ++ config/routes.rb | 1 - spec/requests/hotwired_spec.rb | 32 +++++++++++++++++++ 10 files changed, 41 insertions(+), 50 deletions(-) delete mode 100644 app/views/comments/_form_1508.html.erb delete mode 100644 app/views/comments/test1508.turbo_stream.erb create mode 100644 spec/requests/hotwired_spec.rb diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 088a81bc0..c620a23a2 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -35,12 +35,9 @@ def create format.json { render :show, status: :created, location: @comment } format.turbo_stream else - if turbo_frame_request? - format.html - else - format.html { render :new } - end + format.html { render :new, status: :unprocessable_entity } format.json { render json: @comment.errors, status: :unprocessable_entity } + format.turbo_stream { render :new, status: :unprocessable_entity } end end end @@ -100,10 +97,6 @@ def inline_form end end - def test1508 - @comment = Comment.new - end - private def set_comments diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index b0cc930e7..818c99c32 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -50,7 +50,7 @@ def set_comments def comments_json_string render_to_string(template: "/comments/index", - locals: { comments: Comment.all }, formats: :json) + locals: { comments: @comments }, formats: :json) end def render_html diff --git a/app/views/comments/_form_1508.html.erb b/app/views/comments/_form_1508.html.erb deleted file mode 100644 index e645199a5..000000000 --- a/app/views/comments/_form_1508.html.erb +++ /dev/null @@ -1,30 +0,0 @@ -<%= form_for(@comment, html: { class: "flex flex-col gap-4" }) do |f| %> - <% if @comment.errors.any? %> -
    -

    <%= pluralize(@comment.errors.count, "error") %> prohibited this comment from being saved:

    - -
      - <% @comment.errors.full_messages.each do |message| %> -
    • <%= message %>
    • - <% end %> -
    -
    - <% end %> - -
    - <%= f.label :author, 'Your Name' %>
    - <%= f.text_field :author, class: "px-3 py-1 leading-4 border border-gray-300 rounded" %> -
    -
    - <%= f.label :text, 'Say something using markdown...' %>
    - <%= f.text_area :text, class: "px-3 py-1 leading-4 border border-gray-300 rounded" %> -
    -
    - <%= f.submit 'Post', class: "self-start px-3 py-1 font-semibold border-0 rounded text-sky-50 bg-sky-600 hover:bg-sky-800 cursor-pointer" %> -
    - -

    Below is a react component which render inside a form (that will proof #1508 is fixed), You can turn-off the flag `force_load` and this below form will not show.

    -
    - <%= react_component('HotwiredCommentForm', props: { }, prerender: false, force_load: true) %> -
    -<% end %> diff --git a/app/views/comments/test1508.turbo_stream.erb b/app/views/comments/test1508.turbo_stream.erb deleted file mode 100644 index 47c89d6fd..000000000 --- a/app/views/comments/test1508.turbo_stream.erb +++ /dev/null @@ -1,3 +0,0 @@ -<%= turbo_stream.replace dom_id(@comment), target: "_top" do %> - <%= render partial: "form_1508" %> -<% end %> diff --git a/app/views/pages/hotwired.html.erb b/app/views/pages/hotwired.html.erb index d3f79383a..5cbaa3869 100644 --- a/app/views/pages/hotwired.html.erb +++ b/app/views/pages/hotwired.html.erb @@ -1,7 +1,3 @@

    Demo React Over Hotwired

    <%= react_component("HotwiredCommentScreen", props: @props, prerender: false) %> - -<%= turbo_frame_tag "new_comment" do %> - <%= link_to "proof that #1508 fixed", "/test_1508", data: { turbo_stream: true } %> -<% end %> diff --git a/client/app/bundles/comments/components/HotwiredCommentScreen/ror_components/HotwiredCommentForm.jsx b/client/app/bundles/comments/components/HotwiredCommentScreen/ror_components/HotwiredCommentForm.jsx index 40da8cf2f..7fa3fdbe8 100644 --- a/client/app/bundles/comments/components/HotwiredCommentScreen/ror_components/HotwiredCommentForm.jsx +++ b/client/app/bundles/comments/components/HotwiredCommentScreen/ror_components/HotwiredCommentForm.jsx @@ -8,6 +8,7 @@ import BaseComponent from 'libs/components/BaseComponent'; import SelectLanguage from 'libs/i18n/selectLanguage'; import { defaultLocale } from 'libs/i18n/default'; import { translations } from 'libs/i18n/translations'; +import { COMMENTS_TURBO_STREAM_PATH } from '../../../constants/paths'; import { Turbo } from '@hotwired/turbo-rails'; import CommentForm from '../../CommentBox/CommentForm/CommentForm'; @@ -34,7 +35,7 @@ class HotwiredCommentForm extends BaseComponent { }; return request - .post('/comments.turbo_stream', { comment }, requestConfig) + .post(COMMENTS_TURBO_STREAM_PATH, { comment }, requestConfig) .then(r => r.data) .then(html => { Turbo.renderStreamMessage(html); diff --git a/client/app/bundles/comments/components/HotwiredCommentScreen/ror_components/HotwiredCommentScreen.jsx b/client/app/bundles/comments/components/HotwiredCommentScreen/ror_components/HotwiredCommentScreen.jsx index 43751c48b..61717785c 100644 --- a/client/app/bundles/comments/components/HotwiredCommentScreen/ror_components/HotwiredCommentScreen.jsx +++ b/client/app/bundles/comments/components/HotwiredCommentScreen/ror_components/HotwiredCommentScreen.jsx @@ -7,6 +7,7 @@ import BaseComponent from 'libs/components/BaseComponent'; import SelectLanguage from 'libs/i18n/selectLanguage'; import { defaultMessages, defaultLocale } from 'libs/i18n/default'; import { translations } from 'libs/i18n/translations'; +import { NEW_COMMENT_PATH } from '../../../constants/paths'; import CommentList from '../../CommentBox/CommentList/CommentList'; import css from '../HotwiredCommentScreen.module.scss'; @@ -42,7 +43,7 @@ class HotwiredCommentScreen extends BaseComponent { />
    diff --git a/client/app/bundles/comments/constants/paths.js b/client/app/bundles/comments/constants/paths.js index 03958728f..aec855d68 100644 --- a/client/app/bundles/comments/constants/paths.js +++ b/client/app/bundles/comments/constants/paths.js @@ -6,3 +6,5 @@ export const SIMPLE_REACT_PATH = '/simple'; export const HOTWIRED_PATH = '/hotwired'; export const STIMULUS_PATH = '/stimulus'; export const RAILS_PATH = '/comments'; +export const NEW_COMMENT_PATH = '/comments/new'; +export const COMMENTS_TURBO_STREAM_PATH = '/comments.turbo_stream'; diff --git a/config/routes.rb b/config/routes.rb index 3a4683138..0b703db04 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -23,5 +23,4 @@ get "comment-list", to: "comments#comment_list" resources :comments mount ActionCable.server => "/cable" - get "test_1508", to: "comments#test1508", format: :turbo_stream end diff --git a/spec/requests/hotwired_spec.rb b/spec/requests/hotwired_spec.rb new file mode 100644 index 000000000..12178e7b8 --- /dev/null +++ b/spec/requests/hotwired_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe "Hotwired Demo" do + describe "routing" do + it "routes /hotwired to pages#hotwired" do + expect(Rails.application.routes.recognize_path("/hotwired", method: :get)) + .to include(controller: "pages", action: "hotwired") + end + end + + describe "POST /comments.turbo_stream" do + it "returns turbo stream response on success" do + params = { comment: attributes_for(:comment) } + + expect { post comments_path(format: :turbo_stream), params: params }.to change(Comment, :count).by(1) + expect(response).to have_http_status(:ok) + expect(response.media_type).to eq(Mime[:turbo_stream].to_s) + expect(response.body).to include('turbo-stream action="prepend" target="comments"') + end + + it "returns unprocessable entity turbo stream response on validation error" do + params = { comment: { author: "", text: "" } } + + expect { post comments_path(format: :turbo_stream), params: params }.not_to change(Comment, :count) + expect(response).to have_http_status(422) + expect(response.media_type).to eq(Mime[:turbo_stream].to_s) + expect(response.body).to include('turbo-stream action="update" target="comment-form"') + end + end +end