superglue 0.40.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/lib/generators/rails/scaffold_controller_generator.rb +12 -0
  3. data/lib/generators/rails/superglue_generator.rb +92 -0
  4. data/lib/generators/rails/templates/_form.json.props +13 -0
  5. data/lib/generators/rails/templates/controller.rb.tt +82 -0
  6. data/lib/generators/rails/templates/edit.json.props +14 -0
  7. data/lib/generators/rails/templates/index.json.props +13 -0
  8. data/lib/generators/rails/templates/new.json.props +15 -0
  9. data/lib/generators/rails/templates/show.json.props +7 -0
  10. data/lib/generators/rails/templates/web/edit.html.erb +12 -0
  11. data/lib/generators/rails/templates/web/edit.js +31 -0
  12. data/lib/generators/rails/templates/web/index.html.erb +7 -0
  13. data/lib/generators/rails/templates/web/index.js +49 -0
  14. data/lib/generators/rails/templates/web/new.html.erb +7 -0
  15. data/lib/generators/rails/templates/web/new.js +29 -0
  16. data/lib/generators/rails/templates/web/show.html.erb +7 -0
  17. data/lib/generators/rails/templates/web/show.js +28 -0
  18. data/lib/install/templates/web/action_creators.js +14 -0
  19. data/lib/install/templates/web/actions.js +4 -0
  20. data/lib/install/templates/web/application.js +114 -0
  21. data/lib/install/templates/web/application.json.props +27 -0
  22. data/lib/install/templates/web/application_visit.js +65 -0
  23. data/lib/install/templates/web/initializer.rb +1 -0
  24. data/lib/install/templates/web/reducer.js +72 -0
  25. data/lib/install/web.rb +66 -0
  26. data/lib/superglue/helpers.rb +552 -0
  27. data/lib/superglue/redirection.rb +30 -0
  28. data/lib/superglue.rb +38 -0
  29. data/lib/tasks/install.rake +42 -0
  30. data/test/engine_test.rb +7 -0
  31. data/test/helpers_test.rb +23 -0
  32. data/test/render_test.rb +81 -0
  33. data/test/test_helper.rb +20 -0
  34. metadata +218 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a72d44f915a1102e37b882a5492a0bae8427c68715acf5a59a809fc70cd47aba
4
+ data.tar.gz: 00cb36e39bbc5d47001c3c6ad9a588204e5ccf27deabbce41ddae66a1ba83ac5
5
+ SHA512:
6
+ metadata.gz: dfa4b3fc28f855eca34ff1533050afd0abf800d7106ffdac25ee44e051e530546990138cae9b33b78526264a9235692a0f878011ea46da84baef8a4278f49280
7
+ data.tar.gz: a2198132a6bf2d004745ddb1be7b49de05a21571dd55b507c0d8f09928b06771ebcefb25271b9d84ff9f88303e3f359fea9f6c197ff40db907eb3bf65e75620f
@@ -0,0 +1,12 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/rails/scaffold_controller/scaffold_controller_generator'
3
+
4
+ module Rails
5
+ module Generators
6
+ class ScaffoldControllerGenerator
7
+ source_paths << File.expand_path('../templates', __FILE__)
8
+
9
+ hook_for :superglue, type: :boolean, default: true
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,92 @@
1
+ require 'rails/generators/named_base'
2
+ require 'rails/generators/resource_helpers'
3
+
4
+ module Rails
5
+ module Generators
6
+ class SuperglueGenerator < NamedBase # :nodoc:
7
+ include Rails::Generators::ResourceHelpers
8
+
9
+ source_root File.expand_path('../templates', __FILE__)
10
+
11
+ argument :attributes, type: :array, default: [], banner: 'field:type field:type'
12
+
13
+ def create_root_folder
14
+ path = File.join('app/views', controller_file_path)
15
+ empty_directory path unless File.directory?(path)
16
+ end
17
+
18
+ def copy_view_files
19
+ %w(index show new edit).each do |view|
20
+ @action_name = view
21
+ filename = filename_with_extensions(view)
22
+ template filename, File.join('app/views', controller_file_path, filename)
23
+ end
24
+ template '_form.json.props', File.join('app/views', controller_file_path, '_form.json.props')
25
+
26
+ %w(index show new edit).each do |view|
27
+ @action_name = view
28
+ filename = filename_with_js_extensions(view)
29
+ template 'web/' + filename, File.join('app/views', controller_file_path, filename)
30
+ end
31
+
32
+ %w(index show new edit).each do |view|
33
+ @action_name = view
34
+ filename = filename_with_html_extensions(view)
35
+ template 'web/' + filename, File.join('app/views', controller_file_path, filename)
36
+ end
37
+
38
+ %w(index show new edit).each do |view|
39
+ append_mapping(view)
40
+ end
41
+ end
42
+
43
+
44
+ protected
45
+ def append_mapping(action)
46
+ app_js = 'app/javascript/packs/application.js'
47
+
48
+ component_name = [plural_table_name, action].map(&:camelcase).join
49
+
50
+ inject_into_file app_js, after: "from '@thoughtbot/superglue'" do
51
+ "\nimport #{component_name} from 'views/#{controller_file_path}/#{action}'"
52
+ end
53
+
54
+ inject_into_file app_js, after: 'identifierToComponentMapping = {' do
55
+ "\n '#{[controller_file_path, action].join('/')}': #{component_name},"
56
+ end
57
+ end
58
+
59
+ def action_name
60
+ @action_name
61
+ end
62
+
63
+ def attributes_names
64
+ [:id] + super
65
+ end
66
+
67
+ def filename_with_extensions(name)
68
+ [name, :json, :props] * '.'
69
+ end
70
+
71
+ def filename_with_js_extensions(name)
72
+ [name, :js] * '.'
73
+ end
74
+
75
+ def filename_with_html_extensions(name)
76
+ [name, :html, :erb] * '.'
77
+ end
78
+
79
+ def attributes_list_with_timestamps
80
+ attributes_list(attributes_names + %w(created_at updated_at))
81
+ end
82
+
83
+ def attributes_list(attributes = attributes_names)
84
+ if self.attributes.any? {|attr| attr.name == 'password' && attr.type == :digest}
85
+ attributes = attributes.reject {|name| %w(password password_confirmation).include? name}
86
+ end
87
+
88
+ attributes
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,13 @@
1
+ html = form_with(model: @<%= model_resource_name %>, local: true) do |form|
2
+ inner = "".html_safe
3
+
4
+ <%- attributes.each do |attr| -%>
5
+ inner << form.label(:<%= attr.column_name %>)
6
+ inner << form.<%= attr.field_type %>(:<%= attr.column_name %>)
7
+ <%- end -%>
8
+ inner << form.submit
9
+
10
+ inner
11
+ end
12
+
13
+ json.html html
@@ -0,0 +1,82 @@
1
+ <% if namespaced? -%>
2
+ require_dependency "<%= namespaced_path %>/application_controller"
3
+
4
+ <% end -%>
5
+ <% module_namespacing do -%>
6
+ class <%= controller_class_name %>Controller < ApplicationController
7
+ before_action :set_<%= singular_table_name %>, only: [:show, :edit, :update, :destroy]
8
+
9
+ # GET <%= route_url %>
10
+ def index
11
+ @<%= plural_table_name %> = <%= orm_class.all(class_name) %>
12
+ end
13
+
14
+ # GET <%= route_url %>/1
15
+ def show
16
+ end
17
+
18
+ # GET <%= route_url %>/new
19
+ def new
20
+ @<%= singular_table_name %> = <%= orm_class.build(class_name) %>
21
+ end
22
+
23
+ # GET <%= route_url %>/1/edit
24
+ def edit
25
+ end
26
+
27
+ # POST <%= route_url %>
28
+ def create
29
+ @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %>
30
+
31
+ if @<%= orm_instance.save %>
32
+ <%- if options[:platform] == 'mobile' -%>
33
+ redirect_to <%= index_helper %>_url, notice: <%= "'#{human_name} was successfully created.'" %>
34
+ <%- else -%>
35
+ redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully created.'" %>
36
+ <%- end -%>
37
+ else
38
+ response.set_header("content-location", new_<%= singular_table_name %>_path)
39
+ render :new
40
+ end
41
+ end
42
+
43
+ # PATCH/PUT <%= route_url %>/1
44
+ def update
45
+ if @<%= orm_instance.update("#{singular_table_name}_params") %>
46
+ <%- if options[:platform] == 'mobile' -%>
47
+ redirect_to <%= index_helper %>_url, notice: <%= "'#{human_name} was successfully updated.'" %>
48
+ <%- else -%>
49
+ redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully updated.'" %>
50
+ <%- end -%>
51
+ else
52
+ response.set_header("content-location", edit_<%= singular_table_name %>_path(@<%= singular_table_name %>))
53
+ render :edit
54
+ end
55
+ end
56
+
57
+ # DELETE <%= route_url %>/1
58
+ def destroy
59
+ @<%= orm_instance.destroy %>
60
+ <%- if options[:platform] == 'mobile' -%>
61
+ redirect_to <%= index_helper %>_url, notice: <%= "'#{human_name} was successfully destroyed.'" %>
62
+ <%- else -%>
63
+ redirect_to <%= index_helper %>_url, notice: <%= "'#{human_name} was successfully destroyed.'" %>
64
+ <%- end -%>
65
+ end
66
+
67
+ private
68
+ # Use callbacks to share common setup or constraints between actions.
69
+ def set_<%= singular_table_name %>
70
+ @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
71
+ end
72
+
73
+ # Only allow a trusted parameter "white list" through.
74
+ def <%= "#{singular_table_name}_params" %>
75
+ <%- if attributes_names.empty? -%>
76
+ params.fetch(:<%= singular_table_name %>, {})
77
+ <%- else -%>
78
+ params.require(:<%= singular_table_name %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>)
79
+ <%- end -%>
80
+ end
81
+ end
82
+ <% end -%>
@@ -0,0 +1,14 @@
1
+ if @post.errors.any?
2
+ content = {
3
+ explanation: "#{pluralize(@<%= singular_table_name %>.errors.count, "error")} prohibited this post from being saved:",
4
+ messages: @<%= singular_table_name %>.errors.full_messages.map{|msg| {body: msg}}
5
+ }
6
+
7
+ flash.now[:form_error] = content
8
+ end
9
+
10
+ json.form(partial: 'form') do
11
+ end
12
+
13
+ json.<%= singular_table_name%>_path <%= singular_table_name%>_path(@<%=singular_table_name%>)
14
+ json.<%= plural_table_name %>_path <%= plural_table_name %>_path
@@ -0,0 +1,13 @@
1
+ json.<%= plural_table_name %> do
2
+ json.array! @<%= plural_table_name %> do |<%= singular_table_name %>|
3
+ <%- attributes_list_with_timestamps.each do |attr| -%>
4
+ json.<%=attr%> <%= singular_table_name %>.<%=attr%>
5
+ <%- end -%>
6
+ json.edit_<%=singular_table_name%>_path edit_<%=singular_table_name%>_path(<%=singular_table_name%>)
7
+ json.<%=singular_table_name%>_path <%=singular_table_name%>_path(<%=singular_table_name%>)
8
+ json.delete_<%=singular_table_name%>_path <%=singular_table_name%>_path(<%=singular_table_name%>)
9
+ end
10
+ end
11
+
12
+
13
+ json.new_<%= singular_table_name %>_path new_<%= singular_table_name %>_path
@@ -0,0 +1,15 @@
1
+ if @post.errors.any?
2
+ content = {
3
+ explanation: "#{pluralize(@<%= singular_table_name %>.errors.count, "error")} prohibited this post from being saved:",
4
+ messages: @<%= singular_table_name %>.errors.full_messages.map{|msg| {body: msg}}
5
+ }
6
+
7
+ flash.now[:form_error] = content
8
+ end
9
+
10
+ json.form(partial: 'form') do
11
+ end
12
+
13
+ json.<%= plural_table_name %>_path <%= plural_table_name %>_path
14
+
15
+
@@ -0,0 +1,7 @@
1
+ <%- attributes_list_with_timestamps.each do |attr|-%>
2
+ json.<%=attr%> @<%= singular_table_name %>.<%=attr%>
3
+ <%- end -%>
4
+
5
+
6
+ json.<%= plural_table_name %>_path <%= plural_table_name %>_path
7
+ json.edit_<%= singular_table_name %>_path edit_<%= singular_table_name %>_path(@<%= singular_table_name %>)
@@ -0,0 +1,12 @@
1
+ <%% initial_state = controller.render_to_string(@virtual_path ,formats: [:json], locals: local_assigns, layout: true) %>
2
+
3
+ <script type="text/javascript">
4
+ window.SUPERGLUE_INITIAL_PAGE_STATE=<%%= initial_state.html_safe %>;
5
+ </script>
6
+
7
+ <div id="app">
8
+ <%%# If you need SSR follow instructions at %>
9
+ <%%# https://github.com/thoughtbot/superglue/blob/main/docs/recipes/server-side-rendering.md %>
10
+ <%%# and uncomment the following line %>
11
+ <%%#= Humid.render(initial_state).html_safe %>
12
+ </div>
@@ -0,0 +1,31 @@
1
+ import React from 'react'
2
+ import RailsTag from '@thoughtbot/superglue/components/RailsTag'
3
+ // import * as actionCreators from 'javascript/packs/action_creators'
4
+ // import {useDispatch} from 'react-redux'
5
+
6
+ export default function <%= plural_table_name.camelize %>Edit ({
7
+ // visit,
8
+ // remote,
9
+ form,
10
+ flash,
11
+ <%= singular_table_name.camelize(:lower) %>Path,
12
+ <%= plural_table_name.camelize(:lower) %>Path,
13
+ }) {
14
+ const error = flash.form_error
15
+
16
+ const messagesEl = error && (
17
+ <div id="error_explanation">
18
+ <h2>{ error.explanation }</h2>
19
+ <ul>{ error.messages.map(({body})=> <li key={body}>{body}</li>) }</ul>
20
+ </div>
21
+ )
22
+
23
+ return (
24
+ <div>
25
+ {messagesEl}
26
+ <RailsTag {...form} data-sg-visit={true}/>
27
+ <a href={<%= singular_table_name.camelize(:lower) %>Path} data-sg-visit={true}>Show</a>
28
+ <a href={<%= plural_table_name.camelize(:lower) %>Path} data-sg-visit={true}>Back</a>
29
+ </div>
30
+ )
31
+ }
@@ -0,0 +1,7 @@
1
+ <%% initial_state = controller.render_to_string(@virtual_path ,formats: [:json], locals: local_assigns, layout: true) %>
2
+
3
+ <script type="text/javascript">
4
+ window.SUPERGLUE_INITIAL_PAGE_STATE=<%%= initial_state.html_safe %>;
5
+ </script>
6
+
7
+ <div id="app"></div>
@@ -0,0 +1,49 @@
1
+ import React from 'react'
2
+ // import * as actionCreators from 'javascript/packs/action_creators'
3
+ // import {useDispatch} from 'react-redux'
4
+
5
+ export default function <%= plural_table_name.camelize %>Index({
6
+ // visit,
7
+ // remote,
8
+ flash,
9
+ new<%= singular_table_name.camelize %>Path,
10
+ <%= plural_table_name.camelize(:lower) %> = [],
11
+ }) {
12
+ const <%= singular_table_name.camelize(:lower) %>Items = <%= plural_table_name.camelize(:lower) %>.map((<%= singular_table_name.camelize(:lower) %>, key) => {
13
+ return (
14
+ <tr key={<%= singular_table_name.camelize(:lower) %>.id}>
15
+ <%- attributes_list.select{|attr| attr != :id }.each do |attr| -%>
16
+ <td>{<%=singular_table_name.camelize(:lower)%>.<%=attr.camelize(:lower)%>}</td>
17
+ <%- end -%>
18
+ <td><a href={ <%=singular_table_name%>.<%=singular_table_name.camelize(:lower)%>Path } data-sg-visit={true}>Show</a></td>
19
+ <td><a href={ <%=singular_table_name%>.edit<%=singular_table_name.camelize%>Path } data-sg-visit={true}>Edit</a></td>
20
+ <td><a href={ <%=singular_table_name%>.delete<%=singular_table_name.camelize%>Path }data-sg-visit={true} data-sg-method={"DELETE"}>Delete</a></td>
21
+ </tr>
22
+ )
23
+ })
24
+
25
+ return (
26
+ <div>
27
+ <p id="notice">{flash.notice}</p>
28
+
29
+ <h1><%= plural_table_name.capitalize %></h1>
30
+
31
+ <table>
32
+ <thead>
33
+ <%- attributes_list.select{|attr| attr != :id }.each do |attr| -%>
34
+ <tr><th><%=attr.capitalize%></th></tr>
35
+ <%- end -%>
36
+ <tr>
37
+ <th colSpan="3"></th>
38
+ </tr>
39
+ </thead>
40
+
41
+ <tbody>
42
+ {<%= singular_table_name %>Items}
43
+ </tbody>
44
+ </table>
45
+ <br />
46
+ <a href={new<%= singular_table_name.camelize %>Path} data-sg-visit={true}>New <%= singular_table_name.capitalize %></a>
47
+ </div>
48
+ )
49
+ }
@@ -0,0 +1,7 @@
1
+ <%% initial_state = controller.render_to_string(@virtual_path ,formats: [:json], locals: local_assigns, layout: true) %>
2
+
3
+ <script type="text/javascript">
4
+ window.SUPERGLUE_INITIAL_PAGE_STATE=<%%= initial_state.html_safe %>;
5
+ </script>
6
+
7
+ <div id="app"></div>
@@ -0,0 +1,29 @@
1
+ import React from 'react'
2
+ import RailsTag from '@thoughtbot/superglue/components/RailsTag'
3
+ // import * as actionCreators from 'javascript/packs/action_creators'
4
+ // import { useDispatch } from 'react-redux'
5
+
6
+ export default function <%= plural_table_name.camelize %>New({
7
+ // visit,
8
+ // remote
9
+ form,
10
+ flash,
11
+ <%= plural_table_name.camelize(:lower) %>Path,
12
+ }) {
13
+ const error = flash.form_error
14
+
15
+ const messagesEl = error && (
16
+ <div id="error_explanation">
17
+ <h2>{ error.explanation }</h2>
18
+ <ul>{ error.messages.map(({body})=> <li key={body}>{body}</li>) }</ul>
19
+ </div>
20
+ )
21
+
22
+ return (
23
+ <div>
24
+ {messagesEl}
25
+ <RailsTag {...form} data-sg-visit={true}/>
26
+ <a href={<%= plural_table_name.camelize(:lower) %>Path} data-sg-visit={true}>Back</a>
27
+ </div>
28
+ )
29
+ }
@@ -0,0 +1,7 @@
1
+ <%% initial_state = controller.render_to_string(@virtual_path ,formats: [:json], locals: local_assigns, layout: true) %>
2
+
3
+ <script type="text/javascript">
4
+ window.SUPERGLUE_INITIAL_PAGE_STATE=<%%= initial_state.html_safe %>;
5
+ </script>
6
+
7
+ <div id="app"></div>
@@ -0,0 +1,28 @@
1
+ import React from 'react'
2
+ // import * as actionCreators from 'javascript/packs/action_creators'
3
+ // import {useDispatch} from 'react-redux'
4
+
5
+ export default function <%= plural_table_name.camelize %>Show({
6
+ // visit,
7
+ // remote,
8
+ flash,
9
+ <%- attributes_list_with_timestamps.select{|attr| attr != :id }.each do |attr| -%>
10
+ <%=attr.camelize(:lower)%>,
11
+ <%- end -%>
12
+ edit<%= singular_table_name.camelize %>Path,
13
+ <%= plural_table_name.camelize(:lower) %>Path
14
+ }) {
15
+ return (
16
+ <div>
17
+ <p id="notice">{flash && flash.notice}</p>
18
+ <%- attributes_list_with_timestamps.select{|attr| attr != :id }.each do |attr| -%>
19
+ <p>
20
+ <strong><%= attr.capitalize %>:</strong>
21
+ {<%=attr.camelize(:lower)%>}
22
+ </p>
23
+ <%- end -%>
24
+ <a href={ edit<%= singular_table_name.camelize %>Path } data-sg-visit={true}>Edit</a>
25
+ <a href={ <%= plural_table_name.camelize(:lower) %>Path } data-sg-visit={true}>Back</a>
26
+ </div>
27
+ )
28
+ }
@@ -0,0 +1,14 @@
1
+ // Example:
2
+ //
3
+ // import {
4
+ // CLEAR_FORM_ERRORS
5
+ // } from './actions'
6
+ //
7
+ // export function clearFormErrors(pageKey) {
8
+ // return {
9
+ // type: CLEAR_FORM_ERRORS,
10
+ // payload: {
11
+ // pageKey,
12
+ // }
13
+ // }
14
+ // }
@@ -0,0 +1,4 @@
1
+ // Example:
2
+ //
3
+ // export const CLEAR_FORM_ERRORS = 'CLEAR_FORM_ERRORS'
4
+ export const REHYDRATE = 'persist/REHYDRATE'
@@ -0,0 +1,114 @@
1
+ import React from 'react';
2
+ import { combineReducers, createStore, applyMiddleware, compose } from 'redux';
3
+ import reduceReducers from 'reduce-reducers';
4
+ import thunk from 'redux-thunk';
5
+ import { Provider } from 'react-redux';
6
+ import { render } from 'react-dom';
7
+ import { ApplicationBase, fragmentMiddleware } from '@thoughtbot/superglue';
8
+ import { persistStore, persistReducer } from 'redux-persist';
9
+ import storage from 'redux-persist/lib/storage';
10
+ import { applicationRootReducer, applicationPagesReducer } from './reducer';
11
+ import { buildVisitAndRemote } from './application_visit';
12
+
13
+ // Mapping between your props template to Component, you must add to this
14
+ // to register any new page level component you create. If you are using the
15
+ // scaffold, it will auto append the identifers for you.
16
+ //
17
+ // e.g {'posts/new': PostNew}
18
+ const identifierToComponentMapping = {
19
+ };
20
+
21
+ if (typeof window !== "undefined") {
22
+ document.addEventListener("DOMContentLoaded", function () {
23
+ const appEl = document.getElementById("app");
24
+ const location = window.location;
25
+
26
+ if (appEl) {
27
+ render(
28
+ <Application
29
+ appEl={appEl}
30
+ // The base url prefixed to all calls made by the `visit`
31
+ // and `remote` thunks.
32
+ baseUrl={location.origin}
33
+ // The global var SUPERGLUE_INITIAL_PAGE_STATE is set by your erb
34
+ // template, e.g., index.html.erb
35
+ initialPage={window.SUPERGLUE_INITIAL_PAGE_STATE}
36
+ // The initial path of the page, e.g., /foobar
37
+ path={location.pathname + location.search + location.hash}
38
+ buildVisitAndRemote={buildVisitAndRemote}
39
+ />,
40
+ appEl
41
+ );
42
+ }
43
+ });
44
+ }
45
+
46
+ export default class Application extends ApplicationBase {
47
+ mapping() {
48
+ return identifierToComponentMapping;
49
+ }
50
+
51
+ visitAndRemote(navRef, store) {
52
+ return buildVisitAndRemote(navRef, store);
53
+ }
54
+
55
+ buildStore(initialState, { superglue: superglueReducer, pages: pagesReducer }) {
56
+ // Create the store
57
+ // See `./reducer.js` for an explaination of the two included reducers
58
+ const composeEnhancers =
59
+ (this.hasWindow && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||
60
+ compose;
61
+ const reducer = this.wrapWithPersistReducer(
62
+ reduceReducers(
63
+ combineReducers({
64
+ superglue: superglueReducer,
65
+ pages: reduceReducers(pagesReducer, applicationPagesReducer),
66
+ }),
67
+ applicationRootReducer
68
+ )
69
+ );
70
+ const store = createStore(
71
+ reducer,
72
+ initialState,
73
+ composeEnhancers(applyMiddleware(thunk, fragmentMiddleware))
74
+ );
75
+
76
+ if (this.hasWindow) {
77
+ // Persist the store using Redux-Persist
78
+ persistStore(store);
79
+ }
80
+
81
+ return store;
82
+ }
83
+
84
+ wrapWithPersistReducer(reducers) {
85
+ // Redux Persist settings
86
+ // The key is set to the stringified JS asset path to remove the need for
87
+ // migrations when hydrating.
88
+ if (!this.hasWindow) {
89
+ return reducers;
90
+ }
91
+ const prefix = "superglue";
92
+ const persistKey =
93
+ prefix +
94
+ this.props.initialPage.assets
95
+ .filter((asset) => asset.endsWith(".js"))
96
+ .join(",");
97
+ const persistConfig = {
98
+ key: persistKey,
99
+ storage,
100
+ };
101
+
102
+ // Remove older storage items that were used by previous JS assets
103
+ if (this.hasWindow) {
104
+ const storedKeys = Object.keys(localStorage);
105
+ storedKeys.forEach((key) => {
106
+ if (key.startsWith(`persist:${prefix}`) && key !== persistKey) {
107
+ localStorage.removeItem(key);
108
+ }
109
+ });
110
+ }
111
+
112
+ return persistReducer(persistConfig, reducers);
113
+ }
114
+ }
@@ -0,0 +1,27 @@
1
+ path = request.format.json? ? param_to_search_path(params[:props_at]) : nil
2
+
3
+ json.data(search: path) do
4
+ yield json
5
+ end
6
+
7
+ json.component_identifier local_assigns[:virtual_path_of_template]
8
+ json.defers json.deferred!
9
+ json.fragments json.fragments!
10
+ json.assets [
11
+ asset_pack_path('application.js'),
12
+ asset_path('application.css')
13
+ ]
14
+
15
+ if protect_against_forgery?
16
+ json.csrf_token form_authenticity_token
17
+ end
18
+
19
+ if path
20
+ json.action 'graft'
21
+ json.path search_path_to_camelized_param(path)
22
+ end
23
+
24
+ json.restore_strategy 'fromCacheAndRevisitInBackground'
25
+
26
+ json.rendered_at Time.now.to_i
27
+ json.flash flash.to_h
@@ -0,0 +1,65 @@
1
+ import { visit, remote } from '@thoughtbot/superglue/action_creators'
2
+
3
+ export function buildVisitAndRemote(ref, store) {
4
+ const appRemote = (...args) => {
5
+ return store.dispatch(remote(...args))
6
+ }
7
+
8
+ const appVisit = (...args) => {
9
+ // Do something before
10
+ // e.g, show loading state, you can access the current pageKey
11
+ // via store.getState().superglue.currentPageKey
12
+ let { action } = args
13
+
14
+ return store
15
+ .dispatch(visit(...args))
16
+ .then((meta) => {
17
+ // The assets fingerprints changed, instead of transitioning
18
+ // just go to the URL directly to retrieve new assets
19
+ if (meta.needsRefresh) {
20
+ window.location = meta.url
21
+ return
22
+ }
23
+
24
+ ref.current.navigateTo(meta.pageKey, {
25
+ action: meta.suggestedAction,
26
+ })
27
+
28
+ // always return meta
29
+ return meta
30
+ })
31
+ .finally(() => {
32
+ // Do something after
33
+ // e.g, hide loading state, you can access the changed pageKey
34
+ // via getState().superglue.currentPageKey
35
+ })
36
+ .catch((err) => {
37
+ const response = err.response
38
+
39
+ if (!response) {
40
+ console.error(err)
41
+ return
42
+ }
43
+
44
+ if (response.ok) {
45
+ // err gets thrown, but if the response is ok,
46
+ // it must be an html body that
47
+ // superglue can't parse, just go to the location
48
+ window.location = response.url
49
+ } else {
50
+ if (response.status >= 400 && response.status < 500) {
51
+ window.location = '/400.html'
52
+ return
53
+ }
54
+
55
+ if (response.status >= 500) {
56
+ window.location = '/500.html'
57
+ return
58
+ }
59
+ }
60
+ })
61
+ }
62
+
63
+ return { visit: appVisit, remote: appRemote }
64
+ }
65
+