Skip to content
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

Fixes turbolinks loading issue #221

Merged
merged 7 commits into from
Jan 24, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
.ruby-gemset

node_modules
/examples
/tmp/examples

/node_package/lib

Expand Down
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ rakelib
ruby-lint.yml
spec
node_modules
tmp
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ AllCops:
- !ruby/regexp /old_and_unused\.rb$/
- 'spec/react_on_rails/dummy-for-generators/**/*'
- 'spec/dummy/bin/**/*'
- 'examples/**/*'
- 'tmp/examples/**/*'

Metrics/LineLength:
Max: 120
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ Note: If you have components from react-rails you want to use, then you will nee
+ [React Router](docs/additional_reading/react_router.md)
+ [Server Rendering Tips](docs/additional_reading/server_rendering_tips.md)
+ [Tips](docs/additional_reading/tips.md)
+ [Turbolinks](docs/additional_reading/turbolinks.md)
+ [Webpack Configuration](docs/additional_reading/webpack.md)
+ [Webpack Cookbook](https://christianalfoni.github.io/react-webpack-cookbook/index.html)

Expand Down
39 changes: 39 additions & 0 deletions docs/additional_reading/turbolinks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Turbolinks

* See [Turbolinks on Github](https://github.com/rails/turbolinks)
* Currently support 2.5.x of Turbolinks. We plan to update to Turbolinks 5 soon.

## Why Turbolinks?
As you switch between Rails HTML controller requests, you will only load the HTML and you will
not reload JavaScript and stylesheets. This definitely can make an app perform better, even if
the JavaScript and stylesheets are cached by the browser, as they will still require parsing.

### Install Checklist
1. Include the gem "turbolinks".
1. Included the proper "track" tags when you include the javascript and stylesheet:
```erb
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
```
1. Add turbolinks to your `application.js` file:
```javascript
//= require turbolinks
```

## Troubleshooting

You cans set a global debug flag in your `application.js` to turn on tracing of Turbolinks events
as they pertain to React on Rails.

1. Add this line to your `application.js`:
```javascript
//= require testGlobals
```
2. Initialie the global debug value of `DEBUG_TURBOLINKS` like this:
```javascript
window.DEBUG_TURBOLINKS = true;
console.log('window.DEBUG_TURBOLINKS = true;');
```

This will print out events related to the initialization of the components created with the view
helper `react_component`.
2 changes: 1 addition & 1 deletion docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ bundle && npm i && rake examples:prepare_all && rake symlink_node_package && rak
It's critical to configure your IDE/editor to ignore certain directories. Otherwise your IDE might slow to a crawl!

* /coverage
* /examples
* /tmp (takes care of /tmp/examples)
* /node_package/lib
* /node_modules
* /spec/dummy/app/assets/javascripts/generated
Expand Down
2 changes: 1 addition & 1 deletion docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Install https://github.com/svenfuchs/gem-release

```bash
# Having the examples prevents publishing
rm -rf examples
rm -rf tmp/examples
gem bump
# Or manually update the version number
cd spec/dummy
Expand Down
2 changes: 1 addition & 1 deletion lib/generators/react_on_rails/dev_tests_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def change_package_json_to_use_local_react_on_rails_module
package_json = File.join(destination_root, "client", "package.json")
old_contents = File.read(package_json)
new_contents = old_contents.gsub(/"react-on-rails": ".+",/,
'"react-on-rails": "../../..",')
'"react-on-rails": "../../../..",')
File.open(package_json, "w+") { |f| f.puts new_contents }
end

Expand Down
2 changes: 1 addition & 1 deletion node_package/scripts/symlink-node-package
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ root_path=${DIR}/../..
(cd $root_path && npm link)

# Link the example apps to use react_on_rails node_package
examplesDir=${root_path}/examples
examplesDir=${root_path}/tmp/examples
if [ -d $examplesDir ] ; then
for type in $( ls $examplesDir ); do
d=$examplesDir/${type}/client
Expand Down
52 changes: 40 additions & 12 deletions node_package/src/clientStartup.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,20 @@ import handleError from './handleError';
import isRouterResult from './isRouterResult';

const REACT_ON_RAILS_COMPONENT_CLASS_NAME = 'js-react-on-rails-component';
const turbolinksInstalled = (typeof Turbolinks !== 'undefined');

function debugTurbolinks(...msg) {
if (!window) {
return;
}

if (window.DEBUG_TURBOLINKS) {
console.log('TURBO:', ...msg);
}
}

function turbolinksInstalled() {
return (typeof Turbolinks !== 'undefined');
}

function forEachComponent(fn) {
const els = document.getElementsByClassName(REACT_ON_RAILS_COMPONENT_CLASS_NAME);
Expand All @@ -25,7 +38,7 @@ function render(el) {
const trace = JSON.parse(el.getAttribute('data-trace'));
const expectTurboLinks = JSON.parse(el.getAttribute('data-expect-turbo-links'));

if (!turbolinksInstalled && expectTurboLinks) {
if (!turbolinksInstalled() && expectTurboLinks) {
console.warn('WARNING: NO TurboLinks detected in JS, but it is in your Gemfile');
}

Expand Down Expand Up @@ -57,6 +70,8 @@ function render(el) {
}

function reactOnRailsPageLoaded() {
debugTurbolinks('reactOnRailsPageLoaded');

forEachComponent(render);
}

Expand All @@ -67,27 +82,40 @@ function unmount(el) {
}

function reactOnRailsPageUnloaded() {
debugTurbolinks('reactOnRailsPageUnloaded');
forEachComponent(unmount);
}

let ranOnce = false;

export default function clientStartup(context) {
if (ranOnce) {
const document = context.document;

// Check if server rendering
if (!document) {
return;
}

const document = context.document;
// Tried with a file local variable, but the install handler gets called twice.
if (context.__REACT_ON_RAILS_EVENT_HANDLERS_RAN_ONCE__) {
return;
}

// Install listeners when running on the client (browser)
if (typeof document !== 'undefined') {
if (!turbolinksInstalled) {
context.__REACT_ON_RAILS_EVENT_HANDLERS_RAN_ONCE__ = true;

debugTurbolinks('Adding DOMContentLoaded event to install event listeners.');

document.addEventListener('DOMContentLoaded', () => {
// Install listeners when running on the client (browser).
// We must do this check for turbolinks AFTER the document is loaded because we load the
// Webpack bundles first.

if (!turbolinksInstalled()) {
debugTurbolinks('WITHOUT TURBOLINKS: DOMContentLoaded handler installed.');
document.addEventListener('DOMContentLoaded', reactOnRailsPageLoaded);
} else {
debugTurbolinks('WITH TURBOLINKS: document page:before-unload and page:change handlers' +
' installed.');
document.addEventListener('page:before-unload', reactOnRailsPageUnloaded);
document.addEventListener('page:change', reactOnRailsPageLoaded);
}
}

ranOnce = true;
});
}
2 changes: 2 additions & 0 deletions node_package/tests/ComponentStore.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/* eslint-disable react/no-multi-comp */
/* eslint-disable react/prefer-es6-class */

import test from 'tape';
import ComponentStore from '../src/ComponentStore';
import React from 'react';
Expand Down
1 change: 1 addition & 0 deletions node_package/tests/generatorFunction.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable react/no-multi-comp */
/* eslint-disable react/prefer-es6-class */

import test from 'tape';
import React from 'react';
Expand Down
1 change: 1 addition & 0 deletions rakelib/examples.rake
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ namespace :examples do
task example_type.gen_task_name_short => example_type.clean_task_name do
mkdir_p(example_type.dir)
sh_in_dir(examples_dir, "rails new #{example_type.name} #{example_type.rails_options}")
sh_in_dir(example_type.dir, "touch .gitignore")
append_to_gemfile(example_type.gemfile, example_type.required_gems)
bundle_install_in(example_type.dir)
sh_in_dir(example_type.dir, example_type.generator_shell_commands)
Expand Down
25 changes: 17 additions & 8 deletions rakelib/run_rspec.rake
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ include ReactOnRails::TaskHelpers
namespace :run_rspec do
desc "Run RSpec for top level only"
task :gem do
run_tests_in("", rspec_args: "spec/react_on_rails")
run_tests_in("", rspec_args: File.join("spec", "react_on_rails"))
end

task dummy: ["dummy_apps:dummy_app"] do
run_tests_in("spec/dummy", env_vars: "DRIVER=selenium_firefox")
run_tests_in(File.join("spec", "dummy"), env_vars: "DRIVER=selenium_firefox")
end

# Dynamically define Rake tasks for each example app found in the examples directory
ExampleType.all.each do |example_type|
desc "Runs RSpec for #{example_type.name_pretty} only"
task example_type.rspec_task_name_short => example_type.prepare_task_name do
run_tests_in("#{File.basename(examples_dir)}/#{example_type.name}") # have to use relative path
run_tests_in(File.join(examples_dir, example_type.name)) # have to use relative path
end
end

Expand Down Expand Up @@ -52,14 +52,23 @@ task run_rspec: ["run_rspec:run_rspec"]

private

# Runs rspec in the given directory (if string is passed, assumed to be relative
# to root of the gem.
# Runs rspec in the given directory.
# If string is passed and it's not absolute, it's converted relative to root of the gem.
# TEST_ENV_COMMAND_NAME is used to make SimpleCov.command_name unique in order to
# prevent a name collision. Defaults to the given directory's name.
def run_tests_in(dir, options = {})
dir = Pathname.new(File.join(gem_root, dir)) if dir.is_a?(String)
command_name = options.fetch(:command_name, dir.basename)
if dir.is_a?(String)
path = if dir.start_with?(File::SEPARATOR)
Pathname.new(dir)
else
Pathname.new(File.join(gem_root, dir))
end
else
path = dir
end

command_name = options.fetch(:command_name, path.basename)
rspec_args = options.fetch(:rspec_args, "")
env_vars = %(#{options.fetch(env_vars, '')} COVERAGE=true TEST_ENV_COMMAND_NAME="#{command_name}")
sh_in_dir(dir, "#{env_vars} bundle exec rspec #{rspec_args}")
sh_in_dir(path.realpath, "#{env_vars} bundle exec rspec #{rspec_args}")
end
2 changes: 1 addition & 1 deletion rakelib/task_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def gem_root

# Returns the folder where examples are located
def examples_dir
File.join(gem_root, "examples")
File.join(gem_root, "tmp", "examples")
end

def dummy_app_dirs
Expand Down
2 changes: 1 addition & 1 deletion react_on_rails.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Gem::Specification.new do |s|
s.homepage = "https://github.com/shakacode/react_on_rails"
s.license = "MIT"

s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|examples|node_modules|node_package|coverage)/}) }
s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|tmp|node_modules|node_package|coverage)/}) }
s.bindir = "exe"
s.executables = s.files.grep(%r{^exe/}) { |f| File.basename(f) }
s.require_paths = ["lib"]
Expand Down
6 changes: 2 additions & 4 deletions spec/dummy/app/assets/javascripts/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require jquery_ujs
//= require testGlobals
//= require generated/client

//= require turbolinks
//
//= require generated/client
2 changes: 2 additions & 0 deletions spec/dummy/app/assets/javascripts/testGlobals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
window.DEBUG_TURBOLINKS = true;
console.log('window.DEBUG_TURBOLINKS = true;');
16 changes: 11 additions & 5 deletions spec/dummy/client/app/components/HelloWorld.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { PropTypes } from 'react';
import ReactCompat from '../utils/ReactCompat';

// Super simple example of the simplest possible React component
class HelloWorld extends React.Component {
Expand All @@ -16,15 +15,22 @@ class HelloWorld extends React.Component {
constructor(props, context) {
super(props, context);
this.state = props.helloWorldData;
this.setNameDomRef = this.setNameDomRef.bind(this);
this.handleChange = this.handleChange.bind(this);
}

_handleChange() {
const name = ReactCompat.reactFindDOMNode()(this.refs.name).value;
handleChange() {
const name = this.nameDomRef.value;
this.setState({ name });
}

setNameDomRef(nameDomNode) {
this.nameDomRef = nameDomNode;
}

render() {
console.log('HelloWorld demonstrating a call to console.log in spec/dummy/client/app/components/HelloWorld.jsx:18');
console.log('HelloWorld demonstrating a call to console.log in '
+ 'spec/dummy/client/app/components/HelloWorld.jsx:18');

const { name } = this.state;

Expand All @@ -35,7 +41,7 @@ class HelloWorld extends React.Component {
</h3>
<p>
Say hello to:
<input type="text" ref="name" defaultValue={name} onChange={::this._handleChange} />
<input type="text" ref={this.setNameDomRef} defaultValue={name} onChange={this.handleChange} />
</p>
</div>
);
Expand Down
16 changes: 12 additions & 4 deletions spec/dummy/client/app/components/HelloWorldES5.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* eslint-disable react/prefer-es6-class */

import React from 'react';
import ReactCompat from '../utils/ReactCompat';
import ReactDOM from 'react-dom';

// Super simple example of React component using React.createClass
const HelloWorldES5 = React.createClass({
Expand All @@ -8,14 +10,20 @@ const HelloWorldES5 = React.createClass({
},

getInitialState() {
//this.setNameDomRef = this.setNameDomRef.bind(this);
//this.handleChange = this.handleChange.bind(this);
return this.props.helloWorldData;
},

_handleChange() {
const name = ReactCompat.reactFindDOMNode()(this.refs.name).value;
handleChange() {
const name = this.nameDomRef.value;
this.setState({ name });
},

setNameDomRef(nameDomNode) {
this.nameDomRef = nameDomNode;
},

render() {
const { name } = this.state;

Expand All @@ -26,7 +34,7 @@ const HelloWorldES5 = React.createClass({
</h3>
<p>
Say hello to:
<input type="text" ref="name" defaultValue={name} onChange={this._handleChange} />
<input type="text" ref={this.setNameDomRef} defaultValue={name} onChange={this.handleChange} />
</p>
</div>
);
Expand Down
Loading