From 629435f19bdf96b06426a5a686f6c3121dd0c99f Mon Sep 17 00:00:00 2001 From: Hugo Chevalier Date: Sun, 27 May 2018 05:12:06 +0200 Subject: [PATCH 01/12] Document hydration for XHR-substituted components --- .../javascripts/jquery_rails_manifest.js | 2 + .../app/views/layouts/application.html.erb | 2 + .../views/pages/_xhr_refresh_partial.html.erb | 9 ++ .../app/views/pages/xhr_refresh.html.erb | 74 +++++++++++++++ spec/dummy/app/views/pages/xhr_refresh.js.erb | 6 ++ spec/dummy/app/views/shared/_header.erb | 3 + .../app/components/HelloWorldRehydratable.js | 93 +++++++++++++++++++ .../client/app/startup/clientRegistration.jsx | 2 + .../client/app/startup/serverRegistration.jsx | 2 + spec/dummy/config/initializers/assets.rb | 2 +- spec/dummy/config/routes.rb | 1 + spec/dummy/spec/system/integration_spec.rb | 27 ++++++ 12 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 spec/dummy/app/assets/javascripts/jquery_rails_manifest.js create mode 100644 spec/dummy/app/views/pages/_xhr_refresh_partial.html.erb create mode 100644 spec/dummy/app/views/pages/xhr_refresh.html.erb create mode 100644 spec/dummy/app/views/pages/xhr_refresh.js.erb create mode 100644 spec/dummy/client/app/components/HelloWorldRehydratable.js diff --git a/spec/dummy/app/assets/javascripts/jquery_rails_manifest.js b/spec/dummy/app/assets/javascripts/jquery_rails_manifest.js new file mode 100644 index 000000000..31a96fd2d --- /dev/null +++ b/spec/dummy/app/assets/javascripts/jquery_rails_manifest.js @@ -0,0 +1,2 @@ +//= require jquery +//= require jquery_ujs diff --git a/spec/dummy/app/views/layouts/application.html.erb b/spec/dummy/app/views/layouts/application.html.erb index 52912406f..ea1a09207 100644 --- a/spec/dummy/app/views/layouts/application.html.erb +++ b/spec/dummy/app/views/layouts/application.html.erb @@ -16,6 +16,8 @@ <%= javascript_pack_tag('vendor-bundle', 'data-turbolinks-track': true) %> <%= javascript_pack_tag('app-bundle', 'data-turbolinks-track': true) %> + <%= yield :other_javascript_tags %> + <%= csrf_meta_tags %> diff --git a/spec/dummy/app/views/pages/_xhr_refresh_partial.html.erb b/spec/dummy/app/views/pages/_xhr_refresh_partial.html.erb new file mode 100644 index 000000000..862aaf31d --- /dev/null +++ b/spec/dummy/app/views/pages/_xhr_refresh_partial.html.erb @@ -0,0 +1,9 @@ +<%= react_component('HelloWorld', props: { helloWorldData: { name: 'HelloWorld' } }, + prerender: true, + trace: true, + id: "HelloWorld-react-component-0") %> + +<%= react_component('HelloWorldRehydratable', props: { helloWorldData: { name: 'HelloWorldRehydratable' } }, + prerender: true, + trace: true, + id: 'HelloWorldRehydratable-react-component-1') %> diff --git a/spec/dummy/app/views/pages/xhr_refresh.html.erb b/spec/dummy/app/views/pages/xhr_refresh.html.erb new file mode 100644 index 000000000..d083dbdf3 --- /dev/null +++ b/spec/dummy/app/views/pages/xhr_refresh.html.erb @@ -0,0 +1,74 @@ +<% provide :other_javascript_tags do %> + <%= javascript_include_tag 'jquery_rails_manifest', defer: true %> +<% end %> + +
+ <%= render partial: 'xhr_refresh_partial' %> +
+ +
+ Click to refresh components through XHR (first component event handlers won't work anymore)
+ <%= form_tag '/xhr_refresh', method: :get, remote: true, format: :js do %> + <%= submit_tag 'Refresh', id: 'refresh', name: 'refresh' %> + <% end %> +
+
+ +

React Rails Client Rehydration

+

+ This example demonstrates client side manual rehydration after a component replacement through XHR.

+ + The "Refresh" button on this page will trigger an asynchrounous refresh of component-container content.
+ Components will be prerendered by the server and inserted in the DOM (spec/dummy/app/views/pages/xhr_refresh.js.erb)
+ No client rehydration will occur, preventing any event handler to be correctly attached

+ + Thus, the onChange handler of the HelloWorld component won't trigger whereas the one from HellowWorldRehydratable will, thanks to the "hydrate" javscript event dispacthed from xhr_refresh.js.erb
+

+ +
+

Setup

+
    +
  1. + Create component source: spec/dummy/client/app/components/HelloWorldHydratable.jsx +
  2. +
  3. + Expose the HelloWorldHydratable Component: spec/dummy/client/app/startup/serverRegistration.jsx and spec/dummy/client/app/startup/clientRegistration.jsx +
    +
    +      import HelloWorldHydratable from '../components/HelloWorldHydratable';
    +      import ReactOnRails from 'react-on-rails';
    +      ReactOnRails.register({ HelloWorldHydratable });
    +    
    +
  4. +
  5. + Place the component on the view: spec/dummy/app/views/pages/xhr_refresh.html.erb, making sure it has a parent node easily selectable +
    +
    +      
    + <%%= react_component("HelloWorldHydratable", props: { helloWorldData: { name: 'HelloWorld' } }, prerender: true, trace: true, id: "HelloWorldHydratable-react-component-0") %> +
    +
    +
  6. +
  7. + Have a remote form allow to get xhr_request.js.erb +
    +
    +      <%%= form_tag '/xhr_refresh', method: :get, remote: true, format: :js do %>
    +        <%%= submit_tag 'Refresh' %>
    +      <%% end %>
    +    
    +
  8. +
  9. + In your xhr_request.js.erb, replace your container content and dispatch the 'hydrate' event that will be caught by HelloWorldHydratable event handler +
    +
    +      var container = document.getElementById('component-container');
    +      <%% new_component = react_component("HelloWorldHydratable", props: { helloWorldData: { name: 'HelloWorld' } }, prerender: true, trace: true, id: "HelloWorldHydratable-react-component-0") %>
    +      container.innerHTML = "<%%= escape_javascript(new_component) %>";
    +
    +      var event = document.createEvent('Event');
    +      event.initEvent('hydrate', true, true);
    +      document.dispatchEvent(event);
    +    
    +
  10. +
diff --git a/spec/dummy/app/views/pages/xhr_refresh.js.erb b/spec/dummy/app/views/pages/xhr_refresh.js.erb new file mode 100644 index 000000000..0473f504a --- /dev/null +++ b/spec/dummy/app/views/pages/xhr_refresh.js.erb @@ -0,0 +1,6 @@ +var container = document.getElementById('component-container'); +container.innerHTML = "<%= escape_javascript(render partial: 'xhr_refresh_partial') %>"; + +var event = document.createEvent('Event'); +event.initEvent('hydrate', true, true); +document.dispatchEvent(event); diff --git a/spec/dummy/app/views/shared/_header.erb b/spec/dummy/app/views/shared/_header.erb index 604b074bb..39fa1a1b3 100644 --- a/spec/dummy/app/views/shared/_header.erb +++ b/spec/dummy/app/views/shared/_header.erb @@ -56,6 +56,9 @@
  • <%= link_to "render_js only example", render_js_path %>
  • +
  • + <%= link_to "XHR Refresh", xhr_refresh_path %> +
  • <%= link_to "One Page with Many Examples at Once", root_path %>
  • diff --git a/spec/dummy/client/app/components/HelloWorldRehydratable.js b/spec/dummy/client/app/components/HelloWorldRehydratable.js new file mode 100644 index 000000000..8a5cc2657 --- /dev/null +++ b/spec/dummy/client/app/components/HelloWorldRehydratable.js @@ -0,0 +1,93 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import ReactOnRails from 'react-on-rails'; +import RailsContext from './RailsContext'; + +class HelloWorldRehydratable extends React.Component { + + static propTypes = { + helloWorldData: PropTypes.shape({ + name: PropTypes.string, + }).isRequired, + railsContext: PropTypes.object, + }; + + // Not necessary if we only call super, but we'll need to initialize state, etc. + constructor(props) { + super(props); + this.state = props.helloWorldData; + this.setNameDomRef = this.setNameDomRef.bind(this); + this.handleChange = this.handleChange.bind(this); + this.forceClientHydration = this.forceClientHydration.bind(this) + } + + componentDidMount() { + document.addEventListener('hydrate', this.forceClientHydration); + } + + componentWillUnmount() { + document.removeEventListener('hydrate', this.forceClientHydration); + } + + forceClientHydration() { + const registeredComponentName = 'HelloWorldRehydratable'; + const { railsContext } = this.props; + + // Target all instances of the component in the DOM + const match = document.querySelectorAll(`[id^=${registeredComponentName}-react-component-]`); + // Not all browsers support forEach on NodeList so we go with a classic for-loop + for (let i = 0; i < match.length; ++i) { + const component = match[i]; + + // Get component specification