breezy 0.12.0 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +5 -5
  2. data/lib/breezy.rb +6 -9
  3. data/lib/breezy/helpers.rb +541 -11
  4. data/lib/breezy/redirection.rb +30 -0
  5. data/lib/breezy/xhr_headers.rb +0 -10
  6. data/lib/generators/rails/breezy_generator.rb +13 -6
  7. data/lib/generators/rails/templates/_form.json.props +13 -0
  8. data/lib/generators/rails/templates/controller.rb.tt +0 -3
  9. data/lib/generators/rails/templates/edit.json.props +14 -0
  10. data/lib/generators/rails/templates/{index.js.props → index.json.props} +1 -2
  11. data/lib/generators/rails/templates/new.json.props +15 -0
  12. data/lib/generators/rails/templates/{show.js.props → show.json.props} +0 -2
  13. data/lib/generators/rails/templates/web/edit.html.erb +7 -0
  14. data/lib/generators/rails/templates/web/edit.jsx +21 -28
  15. data/lib/generators/rails/templates/web/index.html.erb +7 -0
  16. data/lib/generators/rails/templates/web/index.jsx +10 -7
  17. data/lib/generators/rails/templates/web/new.html.erb +7 -0
  18. data/lib/generators/rails/templates/web/new.jsx +18 -25
  19. data/lib/generators/rails/templates/web/show.html.erb +7 -0
  20. data/lib/generators/rails/templates/web/show.jsx +3 -4
  21. data/lib/install/templates/web/action_creators.js +14 -12
  22. data/lib/install/templates/web/actions.js +4 -1
  23. data/lib/install/templates/web/application.js +173 -47
  24. data/lib/install/templates/web/application.json.props +26 -0
  25. data/lib/install/templates/web/application_visit.js +65 -0
  26. data/lib/install/templates/web/initializer.rb +1 -15
  27. data/lib/install/templates/web/reducer.js +62 -9
  28. data/lib/install/web.rb +23 -61
  29. data/lib/tasks/install.rake +11 -1
  30. data/test/helpers_test.rb +9 -17
  31. data/test/render_test.rb +19 -95
  32. data/test/test_helper.rb +1 -1
  33. metadata +25 -28
  34. data/app/views/breezy/response.html.erb +0 -0
  35. data/lib/breezy/configuration.rb +0 -35
  36. data/lib/breezy/render.rb +0 -37
  37. data/lib/generators/rails/templates/edit.js.props +0 -16
  38. data/lib/generators/rails/templates/new.js.props +0 -4
  39. data/lib/generators/rails/templates/web/base.jsx +0 -11
  40. data/lib/generators/rails/templates/web/form.jsx +0 -31
  41. data/lib/install/templates/web/babelrc +0 -35
  42. data/test/breezy_test.rb +0 -125
  43. data/test/configuration_test.rb +0 -36
@@ -0,0 +1,26 @@
1
+ path = request.format.json? ? param_to_search_path(params[:bzq]) : 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.rendered_at Time.now.to_i
25
+ json.flash flash.to_h
26
+
@@ -0,0 +1,65 @@
1
+ import { visit, remote } from '@jho406/breezy/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().breezy.currentPageKey
12
+ return store
13
+ .dispatch(visit(...args))
14
+ .then((meta) => {
15
+ // The assets fingerprints changed, instead of transitioning
16
+ // just go to the URL directly to retrieve new assets
17
+ if (meta.needsRefresh) {
18
+ window.location = meta.url
19
+ return
20
+ }
21
+
22
+ // There can only be one visit at a time, if `canNavigate` is false,
23
+ // then this request will be saved to the store but should be ignored
24
+ // for a more recent visit.
25
+ if (meta.canNavigate) {
26
+ ref.current.navigateTo(meta.pageKey, {
27
+ action: meta.suggestedAction,
28
+ })
29
+ }
30
+ })
31
+ .finally(() => {
32
+ // Do something after
33
+ // e.g, hide loading state, you can access the changed pageKey
34
+ // via getState().breezy.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
+ // breezy 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
+
@@ -1,15 +1 @@
1
- require 'breezy_template/core_ext'
2
-
3
- Breezy.configure do |config|
4
- # Configure breezy.js to refresh the browser when sprockets or
5
- # webpacker asset fingerpint changes. This is similar to Turbolink's
6
- # `data-turbolinks-track`.
7
- #
8
- # Note that this file was generated without sprockets JS tracking.
9
- # If you need to change this behavior, add it like so:
10
- #
11
- # config.track_sprockets_assets = ['application.js', 'application.css']
12
- config.track_sprockets_assets = ['application.css']
13
-
14
- config.track_pack_assets = ['application.js']
15
- end
1
+ require 'props_template/core_ext'
@@ -1,17 +1,70 @@
1
+ // Example:
2
+ //
3
+ // import {
4
+ // CLEAR_FORM_ERRORS
5
+ // } from './actions'
6
+ // import produce from "immer"
7
+ //
8
+ // export default function (state = {}, action) {
9
+ // switch(action.type) {
10
+ // case CLEAR_FORM_ERRORS: {
11
+ // const {pageKey} = action.payload
12
+ //
13
+ // return produce(state, draft => {
14
+ // const currentPage = draft[pageKey]
15
+ // delete currentPage.errors
16
+ // })
17
+ // }
18
+ // default:
19
+ // return state
20
+ // }
21
+ // }
22
+
1
23
  import {
2
- CLEAR_FORM_ERRORS
24
+ REHYDRATE,
3
25
  } from './actions'
4
- import produce from "immer"
5
26
 
6
- export default function (state = {}, action) {
27
+ // The applicationPageReducer is for cross page reducers
28
+ // Its common to add to this. You'll typically have to pass a pageKey to the
29
+ // action payload to distinguish the current page
30
+ //
31
+ // The pageKey is passed through the props in your component. Access it like
32
+ // this: `this.props.pageKey` then dispatch it in an action
33
+ export const applicationPagesReducer = (state = {}, action) => {
34
+ switch(action.type) {
35
+ default:
36
+ return state
37
+ }
38
+ }
39
+
40
+ // The applicationRootReducer is for app wide reducers
41
+ // Its rare to be adding to this. Included out of the box ix a reducer for
42
+ // Redux Persist.
43
+ //
44
+ // The REHYDRATE reducer is generated by Breezy and is needed to persist state
45
+ // on any changes made to the initial state that gets injected into
46
+ // window.BREEZY_INITIAL_PAGE_STATE.
47
+ export const applicationRootReducer = (state = {}, action) => {
7
48
  switch(action.type) {
8
- case CLEAR_FORM_ERRORS: {
9
- const {pageKey} = action.payload
49
+ case REHYDRATE: {
50
+ if (action.payload) {
51
+ const {
52
+ pages: hydratedPages
53
+ } = action.payload
54
+ const { pages } = state
55
+ const nextPages = { ...pages, ...hydratedPages }
56
+
57
+ for (const key in pages) {
58
+ if (pages[key] && hydratedPages[key] &&
59
+ pages[key].renderedAt > hydratedPages[key].renderedAt) {
60
+ nextPages[key] = { ...pages[key] }
61
+ }
62
+ }
10
63
 
11
- return produce(state, draft => {
12
- const currentPage = draft[pageKey]
13
- delete currentPage.errors
14
- })
64
+ return { ...state, pages: nextPages }
65
+ } else {
66
+ return state
67
+ }
15
68
  }
16
69
  default:
17
70
  return state
@@ -1,26 +1,7 @@
1
1
  require "webpacker/configuration"
2
2
 
3
- babelrc = Rails.root.join(".babelrc")
4
3
  babel_config = Rails.root.join("babel.config.js")
5
4
 
6
- def append_js_tags
7
- app_html = 'app/views/layouts/application.html.erb'
8
- js_tag = <<-JS_TAG
9
-
10
- <script type="text/javascript">
11
- window.BREEZY_INITIAL_PAGE_STATE=<%= breezy_snippet %>;
12
- </script>
13
- JS_TAG
14
-
15
- inject_into_file app_html, after: '<head>' do
16
- js_tag
17
- end
18
-
19
- inject_into_file app_html, after: '<body>' do
20
- "\n <div id='app'></div>"
21
- end
22
- end
23
-
24
5
  def add_member_methods
25
6
  inject_into_file "app/models/application_record.rb", after: "class ApplicationRecord < ActiveRecord::Base\n" do
26
7
  <<-RUBY
@@ -29,51 +10,26 @@ def add_member_methods
29
10
  end
30
11
 
31
12
  def self.member_by(attr, value)
32
- find_by(Hash[attr, val])
13
+ find_by(Hash[attr, value])
33
14
  end
34
-
35
15
  RUBY
36
16
  end
37
17
  end
38
18
 
39
- if File.exist?(babelrc)
40
- react_babelrc = JSON.parse(File.read(babelrc))
41
- react_babelrc["presets"] ||= []
42
- react_babelrc["plugins"] ||= []
43
-
44
- react_babelrc["plugins"].push(["module-resolver", {
45
- "root": ["./app"],
46
- "alias": {
47
- "views": "./app/views",
48
- "components": "./app/components",
49
- "javascript": "./app/javascript"
19
+ say "Copying module-resolver preset to your babel.config.js"
20
+ resolver_snippet = <<~JAVASCRIPT
21
+ [
22
+ require('babel-plugin-module-resolver').default, {
23
+ "root": ["./app"],
24
+ "alias": {
25
+ "views": "./app/views",
26
+ "components": "./app/components",
27
+ "javascript": "./app/javascript"
28
+ }
50
29
  }
51
- }])
52
-
53
- say "Copying module-resolver preset to your .babelrc file"
54
-
55
- File.open(babelrc, "w") do |f|
56
- f.puts JSON.pretty_generate(react_babelrc)
57
- end
58
- elsif File.exist?(babel_config)
59
- say "Copying module-resolver preset to your babel.config.js"
60
- resolver_snippet = <<~PLUGIN
61
- [
62
- require('babel-plugin-module-resolver').default, {
63
- "root": ["./app"],
64
- "alias": {
65
- "views": "./app/views",
66
- "components": "./app/components",
67
- "javascript": "./app/javascript"
68
- }
69
- }
70
- ],
71
- PLUGIN
72
- insert_into_file "babel.config.js", resolver_snippet, after: /plugins: \[\n/
73
- else
74
- say "Copying .babelrc to app root directory"
75
- copy_file "#{__dir__}/templates/web/babelrc", ".babelrc"
76
- end
30
+ ],
31
+ JAVASCRIPT
32
+ insert_into_file "babel.config.js", resolver_snippet, after: /plugins: \[\n/
77
33
 
78
34
  say "Copying application.js file to #{Webpacker.config.source_entry_path}"
79
35
  copy_file "#{__dir__}/templates/web/application.js", "#{Webpacker.config.source_entry_path}/application.js"
@@ -87,20 +43,26 @@ copy_file "#{__dir__}/templates/web/action_creators.js", "#{Webpacker.config.sou
87
43
  say "Copying actions.js file to #{Webpacker.config.source_entry_path}"
88
44
  copy_file "#{__dir__}/templates/web/actions.js", "#{Webpacker.config.source_entry_path}/actions.js"
89
45
 
46
+ say "Copying application_visit.js file to #{Webpacker.config.source_entry_path}"
47
+ copy_file "#{__dir__}/templates/web/application_visit.js", "#{Webpacker.config.source_entry_path}/application_visit.js"
48
+
90
49
  say "Copying Breezy initializer"
91
50
  copy_file "#{__dir__}/templates/web/initializer.rb", "config/initializers/breezy.rb"
92
51
 
93
- say "Appending js tags to your application.html.erb"
94
- append_js_tags
52
+ say "Copying application.json.props"
53
+ copy_file "#{__dir__}/templates/web/application.json.props", "app/views/layouts/application.json.props"
95
54
 
96
55
  say "Adding required member methods to ApplicationRecord"
97
56
  add_member_methods
98
57
 
99
58
  say "Installing React, Redux, and Breezy"
100
- run "yarn add babel-plugin-module-resolver babel-preset-react formik history prop-types react-redux redux-thunk redux reduce-reducers react react-dom immer @jho406/breezy --save"
59
+ run "yarn add babel-plugin-module-resolver history@\"^4\" html-react-parser@\"^0.13\" react-redux redux-thunk redux redux-persist reduce-reducers immer @jho406/breezy --save"
101
60
 
102
61
  say "Updating webpack config to include .jsx file extension and resolved_paths"
103
62
  insert_into_file Webpacker.config.config_path, " - .jsx\n", after: /extensions:\n/
63
+ # For newer webpacker
64
+ insert_into_file Webpacker.config.config_path, "'app/views', 'app/components'", after: /additional_paths: \[/
65
+ # For older webpacker
104
66
  insert_into_file Webpacker.config.config_path, "'app/views', 'app/components'", after: /resolved_paths: \[/
105
67
 
106
68
  say "Webpacker now supports breezy.js 🎉", :green
@@ -10,6 +10,16 @@ namespace :breezy do
10
10
  end
11
11
  end
12
12
 
13
+ desc "Verifies if any version of react is in package.json"
14
+ task :verify_react do
15
+ package_json = JSON.parse(File.read(Rails.root.join("package.json")))
16
+
17
+ if package_json['dependencies']['react'].nil?
18
+ $stderr.puts "React not installed. Did you run `rails webpacker:install:react`?"
19
+ $stderr.puts "Exiting!" && exit!
20
+ end
21
+ end
22
+
13
23
  desc "Verifies webpacker has been installed"
14
24
  task "verify_webpacker" do
15
25
  begin
@@ -23,7 +33,7 @@ namespace :breezy do
23
33
 
24
34
  namespace :install do
25
35
  desc "Install everything needed for breezy web"
26
- task 'web' => ["breezy:verify_webpacker", "webpacker:verify_install"] do
36
+ task 'web' => ["breezy:verify_webpacker", "webpacker:verify_install", "breezy:verify_react"] do
27
37
  template = File.expand_path("../install/web.rb", __dir__)
28
38
  exec "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{template}"
29
39
  end
@@ -2,30 +2,22 @@ require 'test_helper'
2
2
 
3
3
  class HelpersTest < ActiveSupport::TestCase
4
4
  include Breezy::Helpers
5
- attr_reader :request
6
5
 
7
- class Request
8
- attr_reader :params
9
- def initialize(params = {})
10
- @params = params
11
- end
12
- end
13
-
14
- test 'breezy_filter returns a valid bzq param' do
15
- @request = Request.new({:bzq => 'foo.bar.baz_baz'})
6
+ test 'clean_bzq returns nil if qry is nil' do
7
+ qry = nil
16
8
 
17
- assert_equal breezy_filter, 'foo.bar.baz_baz'
9
+ assert_nil param_to_search_path(qry)
18
10
  end
19
11
 
20
- test 'breezy_filter removes invalid bzq param chars' do
21
- @request = Request.new({:bzq => 'foo.bar/?)()-'})
12
+ test 'clean_bzq returns a refined qry' do
13
+ qry = 'foo...bar/?)()-'
22
14
 
23
- assert_equal breezy_filter, 'foo.bar'
15
+ assert_equal param_to_search_path(qry), ['foo', 'bar']
24
16
  end
25
17
 
26
- test 'breezy_filter return nil when no params are present' do
27
- @request = Request.new({})
18
+ test 'camelize_path' do
19
+ path = ['foo_bar', 'foo_bar=1', 'foo_baz_roo']
28
20
 
29
- assert_nil breezy_filter
21
+ assert_equal search_path_to_camelized_param(path), 'fooBar.fooBar=1.fooBazRoo'
30
22
  end
31
23
  end
@@ -4,14 +4,14 @@ class RenderController < TestController
4
4
  require 'action_view/testing/resolvers'
5
5
 
6
6
  append_view_path(ActionView::FixtureResolver.new(
7
- 'render/action.js.breezy' => 'json.author "john smith"',
8
- 'render/action.html.erb' => 'john smith',
9
- 'render/implied_render_with_breezy.js.breezy' => 'json.author "john smith"',
10
- 'render/implied_render_with_breezy.html.erb' => 'john smith',
7
+ 'render/simple_render_with_breezy.json.props' => 'json.author "john smith"',
8
+ 'render/simple_render_with_breezy_with_bad_layout.json.props' => 'json.author "john smith"',
9
+ 'layouts/application.json.props' => 'json.data {yield json}',
10
+ 'layouts/does_not_exist.html.erb' => '',
11
11
  'layouts/application.html.erb' => <<~HTML
12
12
  <html>
13
13
  <head>
14
- <script><%= breezy_snippet %></script>
14
+ <script><%= @initial_state.strip.html_safe %></script>
15
15
  </head>
16
16
  <body><%=yield%></body>
17
17
  </html>
@@ -20,21 +20,18 @@ class RenderController < TestController
20
20
 
21
21
  layout 'application'
22
22
 
23
- before_action :use_breezy, only: [:simple_render_with_breezy, :implied_render_with_breezy]
24
-
25
23
  def render_action
26
24
  render :action
27
25
  end
28
26
 
29
27
  def simple_render_with_breezy
30
- render :action
31
- end
32
-
33
- def implied_render_with_breezy
28
+ @initial_state = render_to_string(formats: [:json], layout: true)
29
+ render inline: '', layout: true
34
30
  end
35
31
 
36
- def render_action_with_breezy_false
37
- render :action
32
+ def simple_render_with_breezy_with_bad_layout
33
+ @initial_state = render_to_string(formats: [:json], layout: 'does_not_exist')
34
+ render inline: '', layout: true
38
35
  end
39
36
 
40
37
  def form_authenticity_token
@@ -50,108 +47,35 @@ class RenderTest < ActionController::TestCase
50
47
  if Rails.version >= '6'
51
48
  # In rails 6, the fixture orders the templates based on their appearance in the handler
52
49
  # This doesn't happen IRL, so I'm going to explicitly set the handler here.
53
- #
50
+ #
54
51
  # Note that the original is the following
55
52
  # @controller.lookup_context.handlers = [:raw, :breezy, :erb, :js, :html, :builder, :ruby]
56
- @controller.lookup_context.handlers = [:breezy, :erb]
53
+ @controller.lookup_context.handlers = [:props, :erb]
57
54
  end
58
-
59
- Breezy.configuration.track_sprockets_assets = ['app.js']
60
- Breezy.configuration.track_pack_assets = ['app.js']
61
- end
62
-
63
- teardown do
64
- Breezy.configuration.track_sprockets_assets = []
65
- Breezy.configuration.track_pack_assets = []
66
- end
67
-
68
- test "render action via get" do
69
- get :render_action
70
- assert_normal_render 'john smith'
71
55
  end
72
56
 
73
57
  test "simple render with breezy" do
74
58
  get :simple_render_with_breezy
75
- assert_breezy_html({author: "john smith"}, screen: 'render/action')
76
- end
77
-
78
- test "implied render with breezy" do
79
- get :implied_render_with_breezy
80
- assert_breezy_html({author: "john smith"}, screen: 'render/implied_render_with_breezy')
81
- end
82
-
83
- test "simple render with breezy via get js" do
84
- @request.accept = 'application/javascript'
85
- get :simple_render_with_breezy
86
- assert_breezy_js({author: "john smith"})
87
- end
88
-
89
- test "render action via xhr and get js" do
90
- @request.accept = 'application/javascript'
91
- get :simple_render_with_breezy, xhr: true
92
- assert_breezy_js({author: "john smith"})
93
- end
94
-
95
- test "render with breezy false" do
96
- get :render_action_with_breezy_false
97
- assert_normal_render("john smith")
98
- end
99
-
100
- test "render with breezy false via xhr get" do
101
- @request.accept = 'text/html'
102
- get :render_action_with_breezy_false, xhr: true
103
- assert_normal_render("john smith")
104
- end
105
-
106
- test "render action via xhr and put" do
107
- @request.accept = 'text/html'
108
- put :render_action, xhr: true
109
- assert_normal_render 'john smith'
110
- end
111
59
 
112
- private
113
-
114
- def assert_breezy_html(content, opts={})
115
60
  assert_response 200
116
-
117
61
  rendered = <<~HTML
118
62
  <html>
119
63
  <head>
120
- <script>(function(){var fragments={};var lastFragmentName;var lastFragmentPath;var cache={};var defers=[];return ({"data":#{content.to_json},"screen":"#{opts[:screen]}","fragments":fragments,"privateOpts":{"csrfToken":"secret","assets":["/app.js"],"lastFragmentName":lastFragmentName,"lastFragmentPath":lastFragmentPath,"defers":defers}});})();</script>
64
+ <script>{"data":{"author":"john smith"}}</script>
121
65
  </head>
122
66
  <body></body>
123
67
  </html>
124
68
  HTML
125
69
 
126
70
  assert_equal rendered, @response.body
127
- assert_equal 'text/html', @response.content_type
128
- end
129
-
130
- def assert_breezy_js(content)
131
- assert_response 200
132
- assert_equal '(function(){var fragments={};var lastFragmentName;var lastFragmentPath;var cache={};var defers=[];return ({"data":' + content.to_json + ',"screen":"render/action","fragments":fragments,"privateOpts":{"csrfToken":"secret","assets":["/app.js"],"lastFragmentName":lastFragmentName,"lastFragmentPath":lastFragmentPath,"defers":defers}});})()', @response.body
133
- assert_equal 'text/javascript', @response.content_type
134
- end
135
-
136
- def assert_breezy_replace_js(content)
137
- assert_response 200
138
- assert_equal 'Breezy.replace((function(){return ({"data":' + content.to_json + ',"csrfToken":"secret","assets":["/app.js"]});})());', @response.body
139
- assert_equal 'text/javascript', @response.content_type
71
+ assert_equal 'text/html', @response.media_type
140
72
  end
141
73
 
142
- def assert_normal_render(content)
143
- assert_response 200
144
-
145
- rendered = <<~HTML
146
- <html>
147
- <head>
148
- <script></script>
149
- </head>
150
- <body>#{content}</body>
151
- </html>
152
- HTML
74
+ test "simple render when the layout doesn't exist" do
75
+ err = assert_raise ActionView::MissingTemplate do |e|
76
+ get :simple_render_with_breezy_with_bad_layout
77
+ end
153
78
 
154
- assert_equal rendered, @response.body
155
- assert_equal 'text/html', @response.content_type
79
+ assert_equal(true, err.message.starts_with?('Missing template layouts/does_not_exist with {:locale=>[:en], :formats=>[:json], :variants=>[], :handlers=>[:props, :erb]}.'))
156
80
  end
157
81
  end