breezy 0.12.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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