diff --git a/app/assets/javascripts/application_non_webpack.js b/app/assets/javascripts/application_non_webpack.js index a761af074..8e3c3994b 100644 --- a/app/assets/javascripts/application_non_webpack.js +++ b/app/assets/javascripts/application_non_webpack.js @@ -1,5 +1,3 @@ // All webpack assets in development will be loaded via webpack dev server // turbolinks comes from npm and is listed in webpack.client.base.config.js - -//= require rails_startup diff --git a/app/assets/javascripts/rails_startup.js b/app/assets/javascripts/rails_startup.js deleted file mode 100644 index 21eb42a87..000000000 --- a/app/assets/javascripts/rails_startup.js +++ /dev/null @@ -1,5 +0,0 @@ -$(document).on('ready turbolinks:load', function () { - // highlight active page in the top menu - $('nav a').parents('li,ul').removeClass('active'); - $('nav a[href="' + this.location.pathname + '"]').parents('li,ul').addClass('active'); -}); diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 45b76a0f2..1ef8b0d37 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -1,9 +1,10 @@ class PagesController < ApplicationController + include ReactOnRails::Controller before_action :set_comments def index # NOTE: The below notes apply if you want to set the value of the props in the controller, as - # compared to he view. However, it's more convenient to use Jbuilder from the view. See + # compared to the view. However, it's more convenient to use Jbuilder from the view. See # app/views/pages/index.html.erb:20 # # <%= react_component('App', props: render(template: "/comments/index.json.jbuilder"), @@ -20,10 +21,15 @@ def index # respond_to do |format| # format.html # end + + redux_store("routerCommentsStore", props: comments_json_string) + render_html end # Declaring no_router and simple to indicate we have views for them def no_router + redux_store("commentsStore", props: comments_json_string) + render_html end def simple @@ -34,4 +40,15 @@ def simple def set_comments @comments = Comment.all.order("id DESC") end + + def comments_json_string + render_to_string(template: "/comments/index.json.jbuilder", + locals: { comments: Comment.all }, format: :json) + end + + def render_html + respond_to do |format| + format.html + end + end end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 9e26eae84..841075b77 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -20,36 +20,16 @@ <%= csrf_meta_tags %> - +<%= react_component "NavigationBarApp" %>
<%= yield %>
+ +<%= redux_store_hydration_data %> + diff --git a/app/views/pages/index.html.erb b/app/views/pages/index.html.erb index 07022d4ab..88568b141 100644 --- a/app/views/pages/index.html.erb +++ b/app/views/pages/index.html.erb @@ -9,5 +9,5 @@ <%= render "header" %> -<%= react_component('RouterApp', props: render(template: "/comments/index.json.jbuilder"), - prerender: true, raise_on_prerender_error: true, id: "RouterApp-react-component-0") %> + +<%= react_component('RouterApp', id: "RouterApp-react-component-0") %> diff --git a/app/views/pages/no_router.html.erb b/app/views/pages/no_router.html.erb index fe9f07818..1e83c7c7f 100644 --- a/app/views/pages/no_router.html.erb +++ b/app/views/pages/no_router.html.erb @@ -2,5 +2,5 @@ react_on_rails gem) <%= render "header" %> -<%= react_component('App', props: render(template: "/comments/index.json.jbuilder"), - prerender: true) %> + +<%= react_component('App') %> diff --git a/app/views/pages/simple.html.erb b/app/views/pages/simple.html.erb index 33efd42e7..704c70d66 100644 --- a/app/views/pages/simple.html.erb +++ b/app/views/pages/simple.html.erb @@ -9,4 +9,5 @@
+ <%= react_component('SimpleCommentScreen', props: {}, prerender: false) %> diff --git a/client/app/bundles/comments/components/CommentScreen/CommentScreen.jsx b/client/app/bundles/comments/components/CommentScreen/CommentScreen.jsx index 5bd0376a9..2e3ad836c 100644 --- a/client/app/bundles/comments/components/CommentScreen/CommentScreen.jsx +++ b/client/app/bundles/comments/components/CommentScreen/CommentScreen.jsx @@ -32,7 +32,7 @@ export default class CommentScreen extends BaseComponent { {this._renderNotification()}
( +
  • + + Comments: {props.commentsCount} + +
  • +); + +CommentsCount.propTypes = { + commentsCount: PropTypes.number.isRequired, +}; + +export default CommentsCount; diff --git a/client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx b/client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx new file mode 100644 index 000000000..6995cabff --- /dev/null +++ b/client/app/bundles/comments/components/NavigationBar/NavigationBar.jsx @@ -0,0 +1,77 @@ +import React, {PropTypes} from 'react'; +import ReactOnRails from 'react-on-rails'; +import classNames from 'classnames'; +import CommentsCount from './CommentsCount'; +import * as paths from '../../constants/paths'; + +const NavigationBar = (props) => { + const { commentsCount, pathname } = props; + + return ( + + ); +}; + +NavigationBar.propTypes = { + commentsCount: PropTypes.number, + pathname: PropTypes.string.isRequired, +}; + +export default NavigationBar; diff --git a/client/app/bundles/comments/constants/paths.js b/client/app/bundles/comments/constants/paths.js new file mode 100644 index 000000000..d4353d284 --- /dev/null +++ b/client/app/bundles/comments/constants/paths.js @@ -0,0 +1,4 @@ +export const ROUTER_PATH = '/'; +export const NO_ROUTER_PATH = '/no-router'; +export const SIMPLE_REACT_PATH = '/simple'; +export const RAILS_PATH = '/comments'; diff --git a/client/app/bundles/comments/containers/NavigationBarContainer.jsx b/client/app/bundles/comments/containers/NavigationBarContainer.jsx new file mode 100644 index 000000000..4b7983629 --- /dev/null +++ b/client/app/bundles/comments/containers/NavigationBarContainer.jsx @@ -0,0 +1,37 @@ +import React, { PropTypes } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; + +import NavigationBar from '../components/NavigationBar/NavigationBar'; +import * as commentsActionCreators from '../actions/commentsActionCreators'; +import BaseComponent from 'libs/components/BaseComponent'; + +function stateToProps(state) { + // Which part of the Redux global state does our component want to receive as props? + if (state.$$commentsStore) { + return { + commentsCount: state.$$commentsStore.get('$$comments').size, + pathname: state.railsContext.pathname, + }; + } else { + return { }; + } +} + +class NavigationBarContainer extends BaseComponent { + static propTypes = { + commentsCount: PropTypes.number.isRequired, + pathname: PropTypes.string.isRequired, + }; + + render() { + const { commentsCount, pathname } = this.props; + + return ( + + ); + } +} + +// Don't forget to actually use connect! +export default connect(stateToProps)(NavigationBarContainer); diff --git a/client/app/bundles/comments/reducers/index.js b/client/app/bundles/comments/reducers/index.js index 8d8e6730d..ab99ff483 100644 --- a/client/app/bundles/comments/reducers/index.js +++ b/client/app/bundles/comments/reducers/index.js @@ -1,9 +1,12 @@ import commentsReducer, { $$initialState as $$commentsState } from './commentsReducer'; +import railsContextReducer, { initialState as railsContextState } from './railsContextReducer'; export default { $$commentsStore: commentsReducer, + railsContext: railsContextReducer, }; export const initialStates = { $$commentsState, + railsContextState, }; diff --git a/client/app/bundles/comments/reducers/railsContextReducer.js b/client/app/bundles/comments/reducers/railsContextReducer.js new file mode 100644 index 000000000..e8282141b --- /dev/null +++ b/client/app/bundles/comments/reducers/railsContextReducer.js @@ -0,0 +1,5 @@ +export const initialState = {}; + +export default function railsContextReducer(state = initialState, action = null) { + return state; +} diff --git a/client/app/bundles/comments/startup/ClientApp.jsx b/client/app/bundles/comments/startup/App.jsx similarity index 69% rename from client/app/bundles/comments/startup/ClientApp.jsx rename to client/app/bundles/comments/startup/App.jsx index 0571b5596..6f9c0239e 100644 --- a/client/app/bundles/comments/startup/ClientApp.jsx +++ b/client/app/bundles/comments/startup/App.jsx @@ -1,11 +1,10 @@ import React from 'react'; import { Provider } from 'react-redux'; - -import createStore from '../store/commentsStore'; import NonRouterCommentsContainer from '../containers/NonRouterCommentsContainer'; -export default props => { - const store = createStore(props); +export default (_props, _railsContext) => { + const store = ReactOnRails.getStore('commentsStore'); + return ( diff --git a/client/app/bundles/comments/startup/ClientRouterApp.jsx b/client/app/bundles/comments/startup/ClientRouterApp.jsx index 65f71e3cd..dfa0222ba 100644 --- a/client/app/bundles/comments/startup/ClientRouterApp.jsx +++ b/client/app/bundles/comments/startup/ClientRouterApp.jsx @@ -1,12 +1,13 @@ +// Compare to ../ServerRouterApp.jsx import React from 'react'; import { Provider } from 'react-redux'; +import ReactOnRails from 'react-on-rails'; import { Router, browserHistory } from 'react-router'; -import createStore from '../store/routerCommentsStore'; import routes from '../routes/routes'; import { syncHistoryWithStore } from 'react-router-redux'; -export default (props, location) => { - const store = createStore(props); +export default (_props, _railsContext) => { + const store = ReactOnRails.getStore('routerCommentsStore'); // Create an enhanced history that syncs navigation events with the store const history = syncHistoryWithStore( diff --git a/client/app/bundles/comments/startup/NavigationBarApp.jsx b/client/app/bundles/comments/startup/NavigationBarApp.jsx new file mode 100644 index 000000000..17e26d4fe --- /dev/null +++ b/client/app/bundles/comments/startup/NavigationBarApp.jsx @@ -0,0 +1,36 @@ +// Top level component for client side. +// Compare this to the ./ServerApp.jsx file which is used for server side rendering. + +import React from 'react'; +import ReactOnRails from 'react-on-rails'; +import NavigationBar from '../components/NavigationBar/NavigationBar'; +import NavigationBarContainer from '../containers/NavigationBarContainer'; +import { Provider } from 'react-redux'; +import * as paths from '../constants/paths'; + +/* + * Export a function that returns a ReactComponent, depending on a store named SharedReduxStore. + * This is used for the client rendering hook after the page html is rendered. + * React will see that the state is the same and not do anything. + */ +export default (_props, railsContext) => { + // This is where we get the existing store. + const stores = ReactOnRails.stores(); + const { pathname } = railsContext; + let store; + if (pathname === paths.ROUTER_PATH) { + store = ReactOnRails.getStore('routerCommentsStore', false); + } else if (pathname === paths.NO_ROUTER_PATH) { + store = ReactOnRails.getStore('commentsStore', false); + } else { + return ( + + ); + } + + return ( + + + + ); +}; diff --git a/client/app/bundles/comments/startup/ServerApp.jsx b/client/app/bundles/comments/startup/ServerApp.jsx deleted file mode 100644 index 0571b5596..000000000 --- a/client/app/bundles/comments/startup/ServerApp.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import { Provider } from 'react-redux'; - -import createStore from '../store/commentsStore'; -import NonRouterCommentsContainer from '../containers/NonRouterCommentsContainer'; - -export default props => { - const store = createStore(props); - return ( - - - - ); -}; diff --git a/client/app/bundles/comments/startup/ServerRouterApp.jsx b/client/app/bundles/comments/startup/ServerRouterApp.jsx index adc614f34..58d80ba3b 100644 --- a/client/app/bundles/comments/startup/ServerRouterApp.jsx +++ b/client/app/bundles/comments/startup/ServerRouterApp.jsx @@ -1,12 +1,12 @@ +// Compare to ../ClientRouterApp.jsx import React from 'react'; import { Provider } from 'react-redux'; import { match, RouterContext } from 'react-router'; -import createStore from '../store/commentsStore'; import routes from '../routes/routes'; export default (props, railsContext) => { - const store = createStore(props); + const store = ReactOnRails.getStore('routerCommentsStore'); let error; let redirectLocation; diff --git a/client/app/bundles/comments/startup/clientRegistration.jsx b/client/app/bundles/comments/startup/clientRegistration.jsx index 6edf2d114..9da891e8c 100644 --- a/client/app/bundles/comments/startup/clientRegistration.jsx +++ b/client/app/bundles/comments/startup/clientRegistration.jsx @@ -1,16 +1,23 @@ -import App from './ClientApp'; +import App from './App'; import RouterApp from './ClientRouterApp'; import SimpleCommentScreen from '../components/SimpleCommentScreen/SimpleCommentScreen'; +import routerCommentsStore from '../store/routerCommentsStore'; +import commentsStore from '../store/commentsStore'; +import NavigationBarApp from './NavigationBarApp'; import ReactOnRails from 'react-on-rails'; ReactOnRails.setOptions({ traceTurbolinks: TRACE_TURBOLINKS, // eslint-disable-line no-undef }); -ReactOnRails.register( - { - App, - RouterApp, - SimpleCommentScreen, - } -); +ReactOnRails.register({ + App, + RouterApp, + NavigationBarApp, + SimpleCommentScreen, +}); + +ReactOnRails.registerStore({ + routerCommentsStore, + commentsStore, +}); diff --git a/client/app/bundles/comments/startup/serverRegistration.jsx b/client/app/bundles/comments/startup/serverRegistration.jsx index 8a7d4eaa2..305bdf77f 100644 --- a/client/app/bundles/comments/startup/serverRegistration.jsx +++ b/client/app/bundles/comments/startup/serverRegistration.jsx @@ -1,11 +1,22 @@ // Example of React + Redux -import App from './ServerApp'; +import App from './App'; import RouterApp from './ServerRouterApp'; +import SimpleCommentScreen from '../components/SimpleCommentScreen/SimpleCommentScreen'; import ReactOnRails from 'react-on-rails'; +import NavigationBarApp from './NavigationBarApp'; +import routerCommentsStore from '../store/routerCommentsStore'; +import commentsStore from '../store/commentsStore'; ReactOnRails.register( { App, RouterApp, + NavigationBarApp, + SimpleCommentScreen, } ); + +ReactOnRails.registerStore({ + routerCommentsStore, + commentsStore, +}); diff --git a/client/app/bundles/comments/store/commentsStore.js b/client/app/bundles/comments/store/commentsStore.js index f08822401..8fa34b7d8 100644 --- a/client/app/bundles/comments/store/commentsStore.js +++ b/client/app/bundles/comments/store/commentsStore.js @@ -3,13 +3,14 @@ import thunkMiddleware from 'redux-thunk'; import loggerMiddleware from 'libs/middlewares/loggerMiddleware'; import reducers, { initialStates } from '../reducers'; -export default props => { +export default (props, railsContext) => { const initialComments = props.comments; const { $$commentsState } = initialStates; const initialState = { $$commentsStore: $$commentsState.merge({ $$comments: initialComments, }), + railsContext, }; const reducer = combineReducers(reducers); diff --git a/client/app/bundles/comments/store/routerCommentsStore.js b/client/app/bundles/comments/store/routerCommentsStore.js index 0794fea51..08bed2de3 100644 --- a/client/app/bundles/comments/store/routerCommentsStore.js +++ b/client/app/bundles/comments/store/routerCommentsStore.js @@ -4,13 +4,14 @@ import loggerMiddleware from 'libs/middlewares/loggerMiddleware'; import reducers, { initialStates } from '../reducers'; import { routerReducer } from 'react-router-redux'; -export default props => { +export default (props, railsContext) => { const initialComments = props.comments; const { $$commentsState } = initialStates; const initialState = { $$commentsStore: $$commentsState.merge({ $$comments: initialComments, }), + railsContext, }; // https://github.com/reactjs/react-router-redux diff --git a/client/package.json b/client/package.json index 27cc9ead6..be14fd2a8 100644 --- a/client/package.json +++ b/client/package.json @@ -55,6 +55,7 @@ "babel-runtime": "^6.9.2", "bootstrap-loader": "^1.0.10", "bootstrap-sass": "^3.3.6", + "classnames": "^2.2.3", "css-loader": "^0.23.1", "es5-shim": "^4.5.9", "expose-loader": "^0.7.1", diff --git a/config/initializers/react_on_rails.rb b/config/initializers/react_on_rails.rb index e68dbed69..4d3adeec8 100644 --- a/config/initializers/react_on_rails.rb +++ b/config/initializers/react_on_rails.rb @@ -28,7 +28,9 @@ # Below options can be overriden by passing options to the react_on_rails # `render_component` view helper method. ################################################################################ - # default is false + + # Default is false. Can be overriden at the component level. + # Set to false for debugging issues before turning on to true. config.prerender = true # default is true for development, off otherwise @@ -36,6 +38,8 @@ ################################################################################ # SERVER RENDERING OPTIONS + # Applicable options can be overriden by passing options to the react_on_rails + # `render_component` view helper method. ################################################################################ # If set to true, this forces Rails to reload the server bundle if it is modified @@ -48,7 +52,10 @@ # Default is true. Logs server rendering messages to Rails.logger.info config.logging_on_server = true - config.raise_on_prerender_error = false # change to true to raise exception on server if the JS code throws + # Change to true to raise exception on server if the JS code throws. Let's do this only if not + # in production, as the JS code might still work on the client and we don't want to blow up the + # whole Rails page. + config.raise_on_prerender_error = !Rails.env.production? # Server rendering only (not for render_component helper) # You can configure your pool of JS virtual machines and specify where it should load code: diff --git a/spec/features/add_new_comment_spec.rb b/spec/features/add_new_comment_spec.rb index 3d25893b8..87f8f4de9 100644 --- a/spec/features/add_new_comment_spec.rb +++ b/spec/features/add_new_comment_spec.rb @@ -5,42 +5,42 @@ feature "Add new comment" do context "React Router", page: :main, js: true do context "via Horizontal Form", form: :horizontal do - include_examples "New Comment Submission" + include_examples "New Comment Submission", true end context "via Inline Form", form: :inline do - include_examples "New Comment Submission" + include_examples "New Comment Submission", true end context "via Stacked Form", form: :stacked do - include_examples "New Comment Submission" + include_examples "New Comment Submission", true end end context "React/Redux", page: :react_demo, js: true do context "via Horizontal Form", form: :horizontal do - include_examples "New Comment Submission" + include_examples "New Comment Submission", true end context "via Inline Form", form: :inline do - include_examples "New Comment Submission" + include_examples "New Comment Submission", true end context "via Stacked Form", form: :stacked do - include_examples "New Comment Submission" + include_examples "New Comment Submission", true end end context "simple page", page: :simple, js: true do context "via Horizontal Form", form: :horizontal do - include_examples "New Comment Submission" + include_examples "New Comment Submission", false end context "via Inline Form", form: :inline do - include_examples "New Comment Submission" + include_examples "New Comment Submission", false end context "via the Stacked Form", form: :stacked, driver: js_selenium_driver do - include_examples "New Comment Submission" + include_examples "New Comment Submission", false end end context "from classic page", page: :classic do background { click_link "New Comment" } - include_examples "New Comment Submission" + include_examples "New Comment Submission", false end end diff --git a/spec/features/shared/examples.rb b/spec/features/shared/examples.rb index e0abc4111..ae9226fc3 100644 --- a/spec/features/shared/examples.rb +++ b/spec/features/shared/examples.rb @@ -1,7 +1,7 @@ require "rails_helper" require "features/shared/contexts" -shared_examples "New Comment Submission" do +shared_examples "New Comment Submission" do |expect_comment_count| context "when the new comment is submitted" do let(:name) { "John Smith" } let(:text) { "Hello there!" } @@ -10,6 +10,10 @@ scenario "comment is added" do expect(page).to have_css(".js-comment-author", text: name) expect(page).to have_css(".js-comment-text", text: text) + if expect_comment_count + expect(page).to have_css("#js-comment-count", + text: "Comments: #{Comment.count}") + end end end @@ -18,6 +22,10 @@ scenario "comment is not added" do expect(page).to have_selector(".comment", count: comments_count) + if expect_comment_count + expect(page).to have_css("#js-comment-count", + text: "Comments: #{Comment.count}") + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 244591709..8413c67a6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -61,8 +61,8 @@ # `:focus` metadata. When nothing is tagged with `:focus`, all examples # get run. - # config.filter_run :focus - # config.run_all_when_everything_filtered = true + config.filter_run :focus + config.run_all_when_everything_filtered = true # Limits the available syntax to the non-monkey patched syntax that is # recommended. For more details, see: