Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…into improve-json-safe-and-pretty

# Conflicts:
#	app/helpers/react_on_rails_helper.rb
  • Loading branch information
cheremukhin23 committed Apr 13, 2017
2 parents 5ad43e8 + d5d579e commit 78a1c3c
Show file tree
Hide file tree
Showing 34 changed files with 1,257 additions and 848 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@ All notable changes to this project's source code will be documented in this fil
Contributors: please follow the recommendations outlined at [keepachangelog.com](http://keepachangelog.com/). Please use the existing headings and styling as a guide, and add a link for the version diff at the bottom of the file. Also, please update the `Unreleased` link to compare to the latest release version.

## [Unreleased]
### Added
- Add an ability to return multiple HTML strings in a `Hash` as a result of `react_component` method call. Allows to build `<head>` contents with [React Helmet](https://github.com/nfl/react-helmet).
[#800](https://github.com/shakacode/react_on_rails/pull/800) by [udovenko](https://github.com/udovenko).
### Fixed
- Fix PropTypes, createClass deprecation warnings for React 15.5.x. [#804](https://github.com/shakacode/react_on_rails/pull/804) by [udovenko ](https://github.com/udovenko).

## [6.9.3] - 2017-04-03

### Fixed
- Removed call of to_json on strings when formatting props. [#791](https://github.com/shakacode/react_on_rails/pull/791) by [justin808](https://github.com/justin808)
- Removed call of to_json on strings when formatting props. [#791](https://github.com/shakacode/react_on_rails/pull/791) by [justin808](https://github.com/justin808).

## [6.9.2] - 2017-04-02

Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,10 @@ If you do want different code to run, you'd setup a separate webpack compilation
#### Generator Functions
Why would you create a function that returns a React component? For example, you may want the ability to use the passed-in props to initialize a redux store or setup react-router. Or you may want to return different components depending on what's in the props. ReactOnRails will automatically detect a registered generator function.

Another reason to user a generator function is that sometimes in server rendering, specifically with React Router, you need to return the result of calling ReactDOMServer.renderToString(element). You can do this by returning an object with the following shape: { renderedHtml, redirectLocation, error }.
Another reason to use a generator function is that sometimes in server rendering, specifically with React Router, you need to return the result of calling ReactDOMServer.renderToString(element). You can do this by returning an object with the following shape: { renderedHtml, redirectLocation, error }.

For server rendering, if you wish to return multiple HTML strings from a generator function, you may return an Object from your generator function with a single top level property of renderedHtml. Inside this Object, place a key called componentHtml, along with any other needed keys. This is useful when you using side effects libraries like [React Helmet](https://github.com/nfl/react-helmet). Your Ruby code will get this Object as a Hash containing keys componentHtml and any other custom keys that you added:
{ renderedHtml: { componentHtml, customKey1, customKey2} }

#### Renderer Functions
A renderer function is a generator function that accepts three arguments: `(props, railsContext, domNodeId) => { ... }`. Instead of returning a React component, a renderer is responsible for calling `ReactDOM.render` to manually render a React component into the dom. Why would you want to call `ReactDOM.render` yourself? One possible use case is [code splitting](./docs/additional-reading/code-splitting.md).
Expand Down
98 changes: 83 additions & 15 deletions app/helpers/react_on_rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
module ReactOnRailsHelper
include ReactOnRails::Utils::Required

COMPONENT_HTML_KEY = "componentHtml".freeze

# The env_javascript_include_tag and env_stylesheet_link_tag support the usage of a webpack
# dev server for providing the JS and CSS assets during development mode. See
# https://github.com/shakacode/react-webpack-rails-tutorial/ for a working example.
Expand Down Expand Up @@ -117,21 +119,23 @@ def react_component(component_name, raw_options = {})
server_rendered_html = result["html"]
console_script = result["consoleReplayScript"]

content_tag_options = options.html_options
content_tag_options[:id] = options.dom_id

rendered_output = content_tag(:div,
server_rendered_html.html_safe,
content_tag_options)

# IMPORTANT: Ensure that we mark string as html_safe to avoid escaping.
result = <<-HTML.html_safe
#{component_specification_tag}
#{rendered_output}
#{options.replay_console ? console_script : ''}
HTML

prepend_render_rails_context(result)
if server_rendered_html.is_a?(String)
build_react_component_result_for_server_rendered_string(
server_rendered_html: server_rendered_html,
component_specification_tag: component_specification_tag,
console_script: console_script,
options: options
)
elsif server_rendered_html.is_a?(Hash)
build_react_component_result_for_server_rendered_hash(
server_rendered_html: server_rendered_html,
component_specification_tag: component_specification_tag,
console_script: console_script,
options: options
)
else
raise "server_rendered_html expected to be a String or a Hash."
end
end

# Separate initialization of store from react_component allows multiple react_component calls to
Expand Down Expand Up @@ -247,6 +251,70 @@ def old_json_escape(json)
json.to_s.gsub(json_escape_regexp, json_escape)
end

def build_react_component_result_for_server_rendered_string(
server_rendered_html: required("server_rendered_html"),
component_specification_tag: required("component_specification_tag"),
console_script: required("console_script"),
options: required("options")
)
content_tag_options = options.html_options
content_tag_options[:id] = options.dom_id

rendered_output = content_tag(:div,
server_rendered_html.html_safe,
content_tag_options)

result_console_script = options.replay_console ? console_script : ""
result = compose_react_component_html_with_spec_and_console(
component_specification_tag, rendered_output, result_console_script
)

prepend_render_rails_context(result)
end

def build_react_component_result_for_server_rendered_hash(
server_rendered_html: required("server_rendered_html"),
component_specification_tag: required("component_specification_tag"),
console_script: required("console_script"),
options: required("options")
)
content_tag_options = options.html_options
content_tag_options[:id] = options.dom_id

unless server_rendered_html[COMPONENT_HTML_KEY]
raise "server_rendered_html hash expected to contain \"#{COMPONENT_HTML_KEY}\" key."
end

rendered_output = content_tag(:div,
server_rendered_html[COMPONENT_HTML_KEY].html_safe,
content_tag_options)

result_console_script = options.replay_console ? console_script : ""
result = compose_react_component_html_with_spec_and_console(
component_specification_tag, rendered_output, result_console_script
)

# Other HTML strings need to be marked as html_safe too:
server_rendered_hash_except_component = server_rendered_html.except(COMPONENT_HTML_KEY)
server_rendered_hash_except_component.each do |key, html_string|
server_rendered_hash_except_component[key] = html_string.html_safe
end

result_with_rails_context = prepend_render_rails_context(result)
{ COMPONENT_HTML_KEY => result_with_rails_context }.merge(
server_rendered_hash_except_component
)
end

def compose_react_component_html_with_spec_and_console(component_specification_tag, rendered_output, console_script)
# IMPORTANT: Ensure that we mark string as html_safe to avoid escaping.
<<-HTML.html_safe
#{component_specification_tag}
#{rendered_output}
#{console_script}
HTML
end

# prepend the rails_context if not yet applied
def prepend_render_rails_context(render_value)
return render_value if @rendered_rails_context
Expand Down
78 changes: 78 additions & 0 deletions docs/additional-reading/react-helmet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Using React Helmet to build `<head>` content

## Installation and general usage
See https://github.com/nfl/react-helmet for details. Run `yarn add react-helmet` in your `client` directory to add this package to your application.

## Example
Here is what you need to do in order to configure your Rails application to work with **ReactHelmet**.

Create generator function for server rendering like this:

```javascript
export default (props, _railsContext) => {
const componentHtml = renderToString(<App {...props} />);
const helmet = Helmet.renderStatic();

const renderedHtml = {
componentHtml,
title: helmet.title.toString(),
};
return { renderedHtml };
};
```
You can add more **helmet** properties to result, e.g. **meta**, **base** and so on. See https://github.com/nfl/react-helmet#server-usage.

Use regular component or generator function for client-side:

```javascript
export default (props, _railsContext) => (
<App {...props} />
);
```

Put **ReactHelmet** component somewhere in your `<App>`:
```javascript
import { Helmet } from 'react-helmet';

const App = (props) => (
<div>
<Helmet>
<title>Custom page title</title>
</Helmet>
...
</div>
);

export default App;
```
Register your generators for client and server sides:

```javascript
import ReactHelmetApp from '../ReactHelmetClientApp';

ReactOnRails.register({
ReactHelmetApp
});
```
```javascript
import ReactHelmetApp from '../ReactHelmetServerApp';

ReactOnRails.register({
ReactHelmetApp
});
```
Now when `react_component` helper will be called with **"ReactHelmetApp"** as a first argument it will return a hash instead of HTML string:
```ruby
<% react_helmet_app = react_component("ReactHelmetApp", prerender: true, props: { hello: "world" }, trace: true) %>
<% content_for :title do %>
<%= react_helmet_app['title'] %>
<% end %>

<%= react_helmet_app["componentHtml"] %>
```
So now we're able to insert received title tag to our application layout:
```ruby
<%= yield(:title) if content_for?(:title) %>
```
8 changes: 8 additions & 0 deletions docs/api/javascript-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ The best source of docs is the main [ReactOnRails.js](../../node_package/src/Rea
* find you components for rendering. Components get called with props, or you may use a
* "generator function" to return a React component or an object with the following shape:
* { renderedHtml, redirectLocation, error }.
* For server rendering, if you wish to return multiple HTML strings from a generator function,
* you may return an Object from your generator function with a single top level property of
* renderedHtml. Inside this Object, place a key called componentHtml, along with any other
* needed keys. This is useful when you using side effects libraries like react helmet.
* Your Ruby code with get this Object as a Hash containing keys componentHtml and any other
* custom keys that you added:
* { renderedHtml: { componentHtml, customKey1, customKey2 } }
* See the example in /docs/additional-reading/react-helmet.md
* @param components (key is component name, value is component)
*/
register(components)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react';
import PropTypes from 'prop-types';
import React from 'react';

export default class HelloWorld extends React.Component {
static propTypes = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,25 @@
},
"cacheDirectories": ["node_modules", "client/node_modules"],
"dependencies": {
"babel-cli": "^6.23.0",
"babel-core": "^6.23.1",
"babel-cli": "^6.24.1",
"babel-core": "^6.24.1",
"babel-loader": "^6.3.2",
"babel-runtime": "^6.23.0",
"babel-polyfill": "^6.23.0",
"babel-preset-es2015": "^6.22.0",
"babel-preset-react": "^6.23.0",
"babel-preset-stage-2": "^6.22.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"es5-shim": "^4.5.9",
"expose-loader": "^0.7.3",
"imports-loader": "^0.7.1",
"react": "^15.4.2",
"react-dom": "^15.4.2",
"react": "^15.5.4",
"react-dom": "^15.5.4",
"react-on-rails": "<%= VersionSyntaxConverter.new.rubygem_to_npm %>",
<%- if options.redux? -%>
"react-redux": "^5.0.3",
"react-redux": "^5.0.4",
"redux": "^3.6.0",
<%- end -%>
"webpack": "^2.3.1"
"webpack": "^2.3.3"
},
"devDependencies": {
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react';
import PropTypes from 'prop-types';
import React from 'react';

const HelloWorld = ({ name, updateName }) => (
<div>
Expand Down
3 changes: 2 additions & 1 deletion node_package/tests/ComponentRegistry.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import test from 'tape';
import React from 'react';
import createReactClass from 'create-react-class';

import ComponentRegistry from '../src/ComponentRegistry';

Expand All @@ -22,7 +23,7 @@ test('ComponentRegistry registers and retrieves generator function components',

test('ComponentRegistry registers and retrieves ES5 class components', (assert) => {
assert.plan(1);
const C2 = React.createClass({
const C2 = createReactClass({
render() {
return <div> WORLD </div>;
},
Expand Down
6 changes: 3 additions & 3 deletions node_package/tests/ReactOnRails.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
import test from 'tape';
import { createStore } from 'redux';
import React from 'react';

import createReactClass from 'create-react-class';
import ReactOnRails from '../src/ReactOnRails';

test('ReactOnRails render returns a virtual DOM element for component', (assert) => {
assert.plan(1);
const R1 = React.createClass({
const R1 = createReactClass({
render() {
return (
<div> WORLD </div>
Expand Down Expand Up @@ -135,7 +135,7 @@ test('clearHydratedStores', (assert) => {
return createStore(reducer, props);
}

ReactOnRails.setStore('storeGenerator', storeGenerator);
ReactOnRails.setStore('storeGenerator', storeGenerator({}));
const actual = new Map();
actual.set(storeGenerator);
assert.deepEqual(actual, ReactOnRails.stores());
Expand Down
2 changes: 1 addition & 1 deletion node_package/tests/StoreRegistry.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ test('StoreRegistry clearHydratedStores', (assert) => {
assert.plan(2);
StoreRegistry.stores().clear();

StoreRegistry.setStore('storeGenerator', storeGenerator);
StoreRegistry.setStore('storeGenerator', storeGenerator({}));
const actual = new Map();
actual.set(storeGenerator);
assert.deepEqual(actual, StoreRegistry.stores());
Expand Down
5 changes: 3 additions & 2 deletions node_package/tests/generatorFunction.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@

import test from 'tape';
import React from 'react';
import createReactClass from 'create-react-class';

import generatorFunction from '../src/generatorFunction';

test('generatorFunction: ES5 Component recognized as React.Component', (assert) => {
assert.plan(1);

const es5Component = React.createClass({
const es5Component = createReactClass({
render() {
return (<div>ES5 React Component</div>);
},
Expand Down Expand Up @@ -67,7 +68,7 @@ test('generatorFunction: pure component recognized as React.Component', (assert)
test('generatorFunction: Generator function recognized as such', (assert) => {
assert.plan(1);

const foobarComponent = React.createClass({
const foobarComponent = createReactClass({
render() {
return (<div>Component for Generator Function</div>);
},
Expand Down
Loading

0 comments on commit 78a1c3c

Please sign in to comment.