-
Notifications
You must be signed in to change notification settings - Fork 373
demo react-over-hotwired and proof #1508 fixed #592
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
base: master
Are you sure you want to change the base?
Changes from 2 commits
6194793
f3687a4
1983eb5
4f7480b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| <%= form_for(@comment, html: { class: "flex flex-col gap-4" }) do |f| %> | ||
| <% if @comment.errors.any? %> | ||
| <div id="error_explanation" class="prose bg-pink-100 p-4 mb-4 border border-pink-200 rounded text-red-800 prose-strong:text-red-800 prose-ul:my-1"> | ||
| <h2><%= pluralize(@comment.errors.count, "error") %> prohibited this comment from being saved:</h2> | ||
|
|
||
| <ul> | ||
| <% @comment.errors.full_messages.each do |message| %> | ||
| <li><%= message %></li> | ||
| <% end %> | ||
| </ul> | ||
| </div> | ||
| <% end %> | ||
|
|
||
| <div class="field"> | ||
| <%= f.label :author, 'Your Name' %><br> | ||
| <%= f.text_field :author, class: "px-3 py-1 leading-4 border border-gray-300 rounded" %> | ||
| </div> | ||
| <div class="field"> | ||
| <%= f.label :text, 'Say something using markdown...' %><br> | ||
| <%= f.text_area :text, class: "px-3 py-1 leading-4 border border-gray-300 rounded" %> | ||
| </div> | ||
| <div class="actions"> | ||
| <%= 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" %> | ||
| </div> | ||
|
|
||
| <h1>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. </h1> | ||
| <div class="mx-10 border border-red-600 rounded"> | ||
| <%= react_component('HotwiredCommentForm', props: { }, prerender: false, force_load: true) %> | ||
| </div> | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| <% end %> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| <%= turbo_stream.prepend "comments" do %> | ||
| <div> | ||
| <h2 class="text-blue-800"><%= @comment.author %></h2> | ||
| <span><%= markdown_to_html(@comment.text) %></span> | ||
| </div> | ||
| <% end %> | ||
|
Comment on lines
+1
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't prepend into a React-owned list.
🤖 Prompt for AI Agents |
||
|
|
||
| <%= turbo_stream.update "comment-form" do %> | ||
| <%= link_to "New Comment", new_comment_path, data: { turbo_stream: true } %> | ||
| <% end %> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| <%= turbo_stream.update "comment-form" do %> | ||
| <div class="p-5 m-5 border border-red-600 rounded"> | ||
| <h2>New Comment</h2> | ||
|
|
||
| <%= react_component("HotwiredCommentForm", props: {}, prerender: false, force_load: true) %> | ||
| </div> | ||
| <% end %> |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,3 @@ | ||||||
| <%= turbo_stream.replace dom_id(@comment), target: "_top" do %> | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: The turbo_stream.replace helper in turbo-rails supports the following main options, based on official documentation and source code: - target: Required DOM ID (string, model/record, or array) of the element to replace. - Rendering options for content: partial:, locals: {}, block content &block, or direct content string/HTML. - method: :morph (or "morph" string): Uses morphing instead of direct replacement/innerHTML for the replace action. Examples from docs: <%= turbo_stream.replace "clearance_5", " Replace... " %> <%= turbo_stream.replace clearance, partial: "clearances/clearance", locals: { title: "Hello" } %> <%= turbo_stream.replace clearance, "Morph the dom target ", method: :morph %> Additional attributes like attributes can be passed to the underlying tag via turbo_stream_action_tag, but _top is not a supported option for turbo_stream.replace. The _top value is a special data-turbo-frame target for Turbo Frames (e.g., data: { turbo_frame: "_top" } on links/forms), used to break out of nested frames and navigate the top-level page. It has no meaning or support in Turbo Streams actions like replace, which target arbitrary DOM elements by ID/CSS via target/targets attributes, independent of frame navigation.
Citations:
Remove unsupported Line 1 passes ♻️ Suggested fix-<%= turbo_stream.replace dom_id(`@comment`), target: "_top" do %>
+<%= turbo_stream.replace dom_id(`@comment`) do %>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| <%= render partial: "form_1508" %> | ||||||
| <% end %> | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| <h2>Demo React Over Hotwired</h2> | ||
|
|
||
| <%= 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 %> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| // eslint-disable-next-line max-classes-per-file | ||
| import React from 'react'; | ||
| import request from 'axios'; | ||
| 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 { 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'); | ||
| } | ||
|
|
||
| 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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Axios browser adapter (XHR) valid responseType values are: '' (default), 'arraybuffer', 'blob', 'document', 'json', 'text'. Note that 'stream' is listed in docs but only works in Node.js (http adapter), not browser XHR. To request a Turbo Stream HTML response (text/vnd.turbo-stream.html) from Axios in browser: 1. Set the Accept header to include the Turbo Stream MIME type. 2. Use responseType: 'text' (or omit, defaults to text/json handling but works for HTML). 3. After receiving response, if Turbo is loaded, call Turbo.renderStreamMessage(response.data) to process the turbo-stream elements. Example: axios.get('/your-endpoint', { headers: { 'Accept': 'text/vnd.turbo-stream.html, text/html, application/xhtml+xml' }, responseType: 'text' }) .then(response => { if (response.headers['content-type']?.includes('turbo-stream')) { Turbo.renderStreamMessage(response.data); } else { // handle as regular HTML/text } }); Default Axios Accept is 'application/json, text/plain, /', so must override for server to return turbo-stream format. Citations:
Use Line 32 passes a MIME type where Axios expects transport values like ♻️ Suggested fix const requestConfig = {
- responseType: 'text/vnd.turbo-stream.html',
- headers: ReactOnRails.authenticityHeaders(),
+ responseType: 'text',
+ headers: {
+ Accept: 'text/vnd.turbo-stream.html',
+ ...ReactOnRails.authenticityHeaders(),
+ },
};🤖 Prompt for AI Agents |
||
| .then(r => r.data) | ||
| .then(html => { | ||
| Turbo.renderStreamMessage(html); | ||
| }) | ||
| .then(() => { | ||
| this.setState({ | ||
| submitCommentError: null, | ||
| isSaving: false, | ||
| }); | ||
| }) | ||
| .catch((error) => { | ||
| this.setState({ | ||
| submitCommentError: error, | ||
| isSaving: false, | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| render() { | ||
| const { handleSetLocale, locale } = this.props; | ||
| const cssTransitionGroupClassNames = { | ||
| enter: css.elementEnter, | ||
| enterActive: css.elementEnterActive, | ||
| exit: css.elementLeave, | ||
| exitActive: css.elementLeaveActive, | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="commentBox prose max-w-none prose-a:text-sky-700 prose-li:my-0"> | ||
| {SelectLanguage(handleSetLocale, locale)} | ||
|
|
||
| <CommentForm | ||
| isSaving={this.state.isSaving} | ||
| actions={{ submitComment: this.handleCommentSubmit }} | ||
| error={{ error: this.state.submitCommentError, nodeRef: React.createRef(null) }} | ||
| cssTransitionGroupClassNames={cssTransitionGroupClassNames} | ||
| /> | ||
| </div> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| 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 ( | ||
| <IntlProvider locale={locale} key={locale} messages={messages}> | ||
| <InjectedHotwiredCommentForm | ||
| /* eslint-disable-next-line react/jsx-props-no-spreading */ | ||
| {...this.props} | ||
| locale={locale} | ||
| handleSetLocale={this.handleSetLocale} | ||
| /> | ||
| </IntlProvider> | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| // eslint-disable-next-line max-classes-per-file | ||
| import React from 'react'; | ||
| import Immutable from 'immutable'; | ||
| import _ from 'lodash'; | ||
| 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), | ||
| }; | ||
| } | ||
|
|
||
| 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 ( | ||
| <div className="commentBox prose max-w-none prose-a:text-sky-700 prose-li:my-0"> | ||
| <turbo-frame id="comment-box"> | ||
| <h2>{formatMessage(defaultMessages.comments)}</h2> | ||
| {SelectLanguage(handleSetLocale, locale)} | ||
|
|
||
| <CommentList | ||
| $$comments={this.state.$$comments} | ||
| cssTransitionGroupClassNames={cssTransitionGroupClassNames} | ||
| /> | ||
|
|
||
| <div id="comment-form"> | ||
| <a data-turbo-stream="true" href="/comments/new">New Comment</a> | ||
| </div> | ||
| </turbo-frame> | ||
| </div> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| 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 ( | ||
| <IntlProvider locale={locale} key={locale} messages={messages}> | ||
| <InjectedHotwiredCommentScreen | ||
| /* eslint-disable-next-line react/jsx-props-no-spreading */ | ||
| {...this.props} | ||
| locale={locale} | ||
| handleSetLocale={this.handleSetLocale} | ||
| /> | ||
| </IntlProvider> | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mirror the existing
render_to_stringresponse handling here.Line 42 calls
comments_json_string, which usesrender_to_string. The controller comment above and theindex/no_routeractions both show that an explicit HTML response is needed afterward. Withoutrender_html,pages#hotwiredcan render/negotiation-handle differently from the other controller-driven pages.♻️ Suggested fix
def hotwired `@props` = comments_json_string + render_html end📝 Committable suggestion
🤖 Prompt for AI Agents