Skip to content

Commit

Permalink
Merge pull request #117 from robrobbins/ujs-api
Browse files Browse the repository at this point in the history
Expose the React mounting and un-mounting mechanisms
  • Loading branch information
Robert Mosolgo committed Jan 29, 2015
2 parents 8c0e0b4 + 07dcbcb commit 97ae83f
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 47 deletions.
114 changes: 67 additions & 47 deletions lib/assets/javascripts/react_ujs.js
@@ -1,69 +1,89 @@
// Unobtrusive scripting adapter for React
(function(document, window, React) {
var CLASS_NAME_ATTR = 'data-react-class';
var PROPS_ATTR = 'data-react-props';
/*globals React, Turbolinks*/

// Unobtrusive scripting adapter for React
(function(document, window) {
// jQuery is optional. Use it to support legacy browsers.
var $ = (typeof jQuery !== 'undefined') && jQuery;

var findReactDOMNodes = function() {
var SELECTOR = '[' + CLASS_NAME_ATTR + ']';
if ($) {
return $(SELECTOR);
} else {
return document.querySelectorAll(SELECTOR);
}
};

var mountReactComponents = function() {
var nodes = findReactDOMNodes();
for (var i = 0; i < nodes.length; ++i) {
var node = nodes[i];
var className = node.getAttribute(CLASS_NAME_ATTR);
// Assume className is simple and can be found at top-level (window).
// Fallback to eval to handle cases like 'My.React.ComponentName'.
var constructor = window[className] || eval.call(window, className);
var propsJson = node.getAttribute(PROPS_ATTR);
var props = propsJson && JSON.parse(propsJson);
React.render(React.createElement(constructor, props), node);
}
};

var unmountReactComponents = function() {
var nodes = findReactDOMNodes();
for (var i = 0; i < nodes.length; ++i) {
React.unmountComponentAtNode(nodes[i]);
var $ = (typeof window.jQuery !== 'undefined') && window.jQuery;

// create the namespace
window.ReactRailsUJS = {
CLASS_NAME_ATTR: 'data-react-class',
PROPS_ATTR: 'data-react-props',
// helper method for the mount and unmount methods to find the
// `data-react-class` DOM elements
findDOMNodes: function() {
// we will use fully qualified paths as we do not bind the callbacks
var selector = '[' + window.ReactRailsUJS.CLASS_NAME_ATTR + ']';

if ($) {
return $(selector);
} else {
return document.querySelectorAll(selector);
}
},

mountComponents: function() {
var nodes = window.ReactRailsUJS.findDOMNodes();

for (var i = 0; i < nodes.length; ++i) {
var node = nodes[i];
var className = node.getAttribute(window.ReactRailsUJS.CLASS_NAME_ATTR);

// Assume className is simple and can be found at top-level (window).
// Fallback to eval to handle cases like 'My.React.ComponentName'.
var constructor = window[className] || eval.call(window, className);
var propsJson = node.getAttribute(window.ReactRailsUJS.PROPS_ATTR);
var props = propsJson && JSON.parse(propsJson);

React.render(React.createElement(constructor, props), node);
}
},

unmountComponents: function() {
var nodes = window.ReactRailsUJS.findDOMNodes();

for (var i = 0; i < nodes.length; ++i) {
var node = nodes[i];

React.unmountComponentAtNode(node);
// now remove the `data-react-class` wrapper as well
node.parentElement && node.parentElement.removeChild(node);
}
}
};

var handleTurbolinksEvents = function() {
// functions not exposed publicly
function handleTurbolinksEvents () {
var handleEvent;

if ($) {
handleEvent = function(eventName, callback) {
$(document).on(eventName, callback);
}
};

} else {
handleEvent = function(eventName, callback) {
document.addEventListener(eventName, callback);
}
};
}
handleEvent('page:change', mountReactComponents);
handleEvent('page:receive', unmountReactComponents);
};
handleEvent('page:change', window.ReactRailsUJS.mountComponents);
handleEvent('page:receive', window.ReactRailsUJS.unmountComponents);
}

var handleNativeEvents = function() {
if ($) {
$(mountReactComponents);
$(window).unload(unmountReactComponents);
function handleNativeEvents() {
if ($) {
$(window.ReactRailsUJS.mountComponents);
$(window).unload(window.ReactRailsUJS.unmountComponents);

} else {
document.addEventListener('DOMContentLoaded', mountReactComponents);
window.addEventListener('unload', unmountReactComponents);
document.addEventListener('DOMContentLoaded', window.ReactRailsUJS.mountComponents);
window.addEventListener('unload', window.ReactRailsUJS.unmountComponents);
}
};
}

if (typeof Turbolinks !== 'undefined' && Turbolinks.supported) {
handleTurbolinksEvents();
} else {
handleNativeEvents();
}
})(document, window, React);
})(document, window);
23 changes: 23 additions & 0 deletions test/view_helper_test.rb
Expand Up @@ -48,6 +48,29 @@ class ViewHelperTest < ActionDispatch::IntegrationTest
assert html.include?('class="test"')
assert html.include?('data-foo="1"')
end

test 'ujs object present on the global React object and has our methods' do
visit '/pages/1'
assert page.has_content?('Hello Bob')

# the exposed ujs object is present
ujs_present = page.evaluate_script('typeof ReactRailsUJS === "object";')
assert_equal(ujs_present, true)

# it contains the constants
class_name_present = page.evaluate_script('ReactRailsUJS.CLASS_NAME_ATTR === "data-react-class";')
assert_equal(class_name_present, true)
props_present = page.evaluate_script('ReactRailsUJS.PROPS_ATTR === "data-react-props";')
assert_equal(props_present, true)

#it contains the methods
find_dom_nodes_present = page.evaluate_script('typeof ReactRailsUJS.findDOMNodes === "function";')
assert_equal(find_dom_nodes_present, true)
mount_components_present = page.evaluate_script('typeof ReactRailsUJS.mountComponents === "function";')
assert_equal(mount_components_present, true)
unmount_components_present = page.evaluate_script('typeof ReactRailsUJS.unmountComponents === "function";')
assert_equal(unmount_components_present, true)
end

test 'react_ujs works with rendered HTML' do
visit '/pages/1'
Expand Down

0 comments on commit 97ae83f

Please sign in to comment.