breezy 0.13.0 → 0.17.1

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/lib/breezy.rb +3 -3
  3. data/lib/breezy/helpers.rb +523 -0
  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 +12 -5
  7. data/lib/generators/rails/templates/_form.json.props +13 -0
  8. data/lib/generators/rails/templates/controller.rb.tt +0 -28
  9. data/lib/generators/rails/templates/edit.json.props +7 -12
  10. data/lib/generators/rails/templates/index.json.props +1 -2
  11. data/lib/generators/rails/templates/new.json.props +12 -3
  12. data/lib/generators/rails/templates/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 +5 -2
  25. data/lib/install/templates/web/application_visit.js +65 -0
  26. data/lib/install/templates/web/initializer.rb +1 -0
  27. data/lib/install/templates/web/reducer.js +62 -9
  28. data/lib/install/web.rb +10 -28
  29. metadata +16 -13
  30. data/app/views/breezy/response.html.erb +0 -0
  31. data/lib/generators/rails/templates/web/base.jsx +0 -11
  32. data/lib/generators/rails/templates/web/form.jsx +0 -31
  33. data/test/breezy_test.rb +0 -121
@@ -1,12 +1,14 @@
1
- import {
2
- CLEAR_FORM_ERRORS
3
- } from './actions'
4
-
5
- export function clearFormErrors(pageKey) {
6
- return {
7
- type: CLEAR_FORM_ERRORS,
8
- payload: {
9
- pageKey,
10
- }
11
- }
12
- }
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
+ // }
@@ -1 +1,4 @@
1
- export const CLEAR_FORM_ERRORS = 'CLEAR_FORM_ERRORS'
1
+ // Example:
2
+ //
3
+ // export const CLEAR_FORM_ERRORS = 'CLEAR_FORM_ERRORS'
4
+ export const REHYDRATE = 'persist/REHYDRATE'
@@ -4,61 +4,187 @@ import reduceReducers from 'reduce-reducers'
4
4
  import thunk from 'redux-thunk'
5
5
  import { Provider } from 'react-redux'
6
6
  import { render } from 'react-dom'
7
- import createHistory from 'history/createBrowserHistory'
8
- import Breezy from '@jho406/breezy'
9
- import Nav from '@jho406/breezy/dist/NavComponent'
10
- import applicationReducer from './reducer'
7
+ import { createBrowserHistory, createMemoryHistory } from 'history'
8
+ import { start } from '@jho406/breezy'
9
+ import Nav from '@jho406/breezy/components/NavComponent'
10
+ import { ujsHandlers } from '@jho406/breezy/utils'
11
+ import { persistStore, persistReducer } from 'redux-persist'
12
+ import storage from 'redux-persist/lib/storage'
13
+ import { applicationRootReducer, applicationPagesReducer } from './reducer'
14
+ import { buildVisitAndRemote } from './application_visit'
11
15
 
16
+ if(typeof window !== 'undefined' ) {
17
+ document.addEventListener("DOMContentLoaded", function() {
18
+ const appEl = document.getElementById('app')
19
+ const location = window.location
12
20
 
13
- // Mapping between your props template to Component
14
- // e.g {'posts/new': PostNew}
15
- const identifierToComponentMapping = {
21
+ if (appEl) {
22
+ render(
23
+ <Application
24
+ appEl={appEl}
25
+ // The base url is an optional prefix to all calls made by the `visit`
26
+ // and `remote` thunks.
27
+ baseUrl={''}
28
+ // The global var BREEZY_INITIAL_PAGE_STATE is set by your erb
29
+ // template, e.g., index.html.erb
30
+ initialPage={window.BREEZY_INITIAL_PAGE_STATE}
31
+ // The initial path of the page, e.g., /foobar
32
+ path={location.pathname + location.search + location.hash}
33
+ />, appEl)
34
+ }
35
+ })
16
36
  }
17
37
 
18
- const history = createHistory({})
19
- const initialPage = window.BREEZY_INITIAL_PAGE_STATE
20
- const baseUrl = ''
21
-
22
- //The Nav is pretty bare bones
23
- //Feel free to replace the implementation
24
- const {reducer, initialState, initialPageKey, connect} = Breezy.start({
25
- window,
26
- initialPage,
27
- baseUrl,
28
- history
29
- })
30
-
31
- const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
32
-
33
- const {
34
- breezy: breezyReducer,
35
- pages: pagesReducer,
36
- } = reducer
37
-
38
- const store = createStore(
39
- combineReducers({
40
- breezy: breezyReducer,
41
- pages: reduceReducers(pagesReducer, applicationReducer),
42
- }),
43
- initialState,
44
- composeEnhancers(applyMiddleware(thunk))
45
- )
46
-
47
- connect(store)
48
-
49
- class App extends React.Component {
38
+ export default class Application extends React.Component {
39
+ constructor(props) {
40
+ super(props)
41
+ this.hasWindow = typeof window !== 'undefined'
42
+
43
+ // Mapping between your props template to Component, you must add to this
44
+ // to register any new page level component you create. If you are using the
45
+ // scaffold, it will auto append the identifers for you.
46
+ //
47
+ // e.g {'posts/new': PostNew}
48
+ this.identifierToComponentMapping = {
49
+ }
50
+
51
+ // Create a navigator Ref for UJS attributes and to enhance the base `visit`
52
+ // and `visit` thunks
53
+ this.navigatorRef = React.createRef()
54
+
55
+ // Start Breezy and return an object to prepare the Redux store
56
+ const breezy = start({
57
+ initialPage: this.props.initialPage,
58
+ baseUrl: this.props.baseUrl,
59
+ path: this.props.path,
60
+ fetch: this.hasWindow ? window.fetch : undefined,
61
+ })
62
+ this.breezy = breezy
63
+
64
+ // Build the store and pass Breezy's provided reducer to be combined with
65
+ // your reducers located at `application_reducer.js`
66
+ const {initialState, reducer} = breezy
67
+ this.store = this.buildStore(initialState, reducer)
68
+
69
+ // Fire initial events and populate the store
70
+ breezy.prepareStore(this.store)
71
+
72
+ // Build visit and remote thunks
73
+ // Your modified `visit` and `remote` will get passed below to the
74
+ // NavComponent then to your components through the provided
75
+ // mapDispatchToProps.
76
+ //
77
+ // You can access them via `this.props.visit` or `this.props.remote`. In
78
+ // your page components
79
+ const {visit, remote} = buildVisitAndRemote(this.navigatorRef, this.store)
80
+ this.visit = visit
81
+ this.remote = remote
82
+ }
83
+
84
+ componentDidMount() {
85
+ const { appEl } = this.props
86
+ // Create the ujs event handlers. You can change the ujsAttributePrefix
87
+ // in the event the data attribute conflicts with another.
88
+ this.ujsHandlers = ujsHandlers({
89
+ visit: this.visit,
90
+ remote: this.remote,
91
+ store: this.store,
92
+ ujsAttributePrefix: 'data-bz'
93
+ })
94
+ const {onClick, onSubmit} = this.ujsHandlers
95
+
96
+ appEl.addEventListener('click', onClick)
97
+ appEl.addEventListener('submit', onSubmit)
98
+ }
99
+
100
+ componentWillUnmount() {
101
+ const { appEl } = this.props
102
+ const {onClick, onSubmit} = this.ujsHandlers
103
+
104
+ appEl.removeEventListener('click', onClick)
105
+ appEl.removeEventListener('submit', onSubmit)
106
+ this.breezy.stop()
107
+ }
108
+
109
+ buildStore(initialState, {breezy: breezyReducer, pages: pagesReducer}) {
110
+ // Create the store
111
+ // See `./reducer.js` for an explaination of the two included reducers
112
+ const composeEnhancers = (this.hasWindow && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose;
113
+ const reducer = this.wrapWithPersistReducer(
114
+ reduceReducers(
115
+ combineReducers({
116
+ breezy: breezyReducer,
117
+ pages: reduceReducers(pagesReducer, applicationPagesReducer),
118
+ }),
119
+ applicationRootReducer
120
+ )
121
+ )
122
+ const store = createStore(
123
+ reducer,
124
+ initialState,
125
+ composeEnhancers(applyMiddleware(thunk))
126
+ )
127
+
128
+ if(this.hasWindow) {
129
+ // Persist the store using Redux-Persist
130
+ persistStore(store)
131
+ }
132
+
133
+ return store
134
+ }
135
+
136
+ wrapWithPersistReducer(reducers) {
137
+ // Redux Persist settings
138
+ // The key is set to the stringified JS asset path to remove the need for
139
+ // migrations when hydrating.
140
+ if (!this.hasWindow) {
141
+ return reducers
142
+ }
143
+ const prefix = 'breezy'
144
+ const persistKey = prefix + this.props.initialPage.assets.filter( asset => asset.endsWith('.js')).join(",")
145
+ const persistConfig = {
146
+ key: persistKey,
147
+ storage,
148
+ }
149
+
150
+ // Remove older storage items that were used by previous JS assets
151
+ if (this.hasWindow) {
152
+ const storedKeys = Object.keys(localStorage)
153
+ storedKeys.forEach((key) => {
154
+ if (key.startsWith(`persist:${prefix}`) && key !== persistKey) {
155
+ localStorage.removeItem(key)
156
+ }
157
+ })
158
+ }
159
+
160
+ return persistReducer(persistConfig, reducers)
161
+ }
162
+
163
+ createHistory() {
164
+ if(this.hasWindow) {
165
+ // This is used for client side rendering
166
+ return createBrowserHistory({})
167
+ } else {
168
+ // This is used for server side rendering
169
+ return createMemoryHistory({})
170
+ }
171
+ }
172
+
50
173
  render() {
51
- return <Provider store={store}>
174
+ const history = this.createHistory()
175
+
176
+ // The Nav component is pretty bare and can be inherited from for custom
177
+ // behavior or replaced with your own.
178
+ return <Provider store={this.store}>
52
179
  <Nav
53
- store={store}
54
- mapping={this.props.mapping}
180
+ store={this.store}
181
+ ref={this.navigatorRef}
182
+ visit={this.visit}
183
+ remote={this.remote}
184
+ mapping={this.identifierToComponentMapping}
55
185
  history={history}
56
- initialPageKey={initialPageKey}
186
+ initialPageKey={this.breezy.initialPageKey}
57
187
  />
58
188
  </Provider>
59
189
  }
60
190
  }
61
-
62
- document.addEventListener("DOMContentLoaded", function() {
63
- render(<App mapping={identifierToComponentMapping}/>, document.getElementById('app'))
64
- })
@@ -1,10 +1,10 @@
1
- path = param_to_search_path(params[:bzq])
1
+ path = request.format.json? ? param_to_search_path(params[:bzq]) : nil
2
2
 
3
3
  json.data(search: path) do
4
4
  yield json
5
5
  end
6
6
 
7
- json.component_identifier "#{params[:controller]}/#{params[:action]}"
7
+ json.component_identifier local_assigns[:virtual_path_of_template]
8
8
  json.defers json.deferred!
9
9
  json.fragments json.fragments!
10
10
  json.assets [
@@ -21,3 +21,6 @@ if path
21
21
  json.path search_path_to_camelized_param(path)
22
22
  end
23
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
+
@@ -0,0 +1 @@
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
@@ -2,25 +2,6 @@ require "webpacker/configuration"
2
2
 
3
3
  babel_config = Rails.root.join("babel.config.js")
4
4
 
5
- def append_js_tags
6
- app_html = 'app/views/layouts/application.html.erb'
7
- js_tag = <<-JS_TAG
8
-
9
- <script type="text/javascript">
10
- window.BREEZY_INITIAL_PAGE_STATE=<%= @initial_state.html_safe %>;
11
- </script>
12
- JS_TAG
13
-
14
- inject_into_file app_html, after: '<head>' do
15
- js_tag
16
- end
17
-
18
- inject_into_file app_html, after: '<body>' do
19
- "\n <div id='app'></div>"
20
- end
21
- end
22
-
23
- #TODO: add member_key
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
@@ -31,11 +12,6 @@ def add_member_methods
31
12
  def self.member_by(attr, value)
32
13
  find_by(Hash[attr, value])
33
14
  end
34
-
35
- def self.member_key
36
- "id"
37
- end
38
-
39
15
  RUBY
40
16
  end
41
17
  end
@@ -67,20 +43,26 @@ copy_file "#{__dir__}/templates/web/action_creators.js", "#{Webpacker.config.sou
67
43
  say "Copying actions.js file to #{Webpacker.config.source_entry_path}"
68
44
  copy_file "#{__dir__}/templates/web/actions.js", "#{Webpacker.config.source_entry_path}/actions.js"
69
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
+
49
+ say "Copying Breezy initializer"
50
+ copy_file "#{__dir__}/templates/web/initializer.rb", "config/initializers/breezy.rb"
51
+
70
52
  say "Copying application.json.props"
71
53
  copy_file "#{__dir__}/templates/web/application.json.props", "app/views/layouts/application.json.props"
72
54
 
73
- say "Appending js tags to your application.html.erb"
74
- append_js_tags
75
-
76
55
  say "Adding required member methods to ApplicationRecord"
77
56
  add_member_methods
78
57
 
79
58
  say "Installing React, Redux, and Breezy"
80
- 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"
81
60
 
82
61
  say "Updating webpack config to include .jsx file extension and resolved_paths"
83
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
84
66
  insert_into_file Webpacker.config.config_path, "'app/views', 'app/components'", after: /resolved_paths: \[/
85
67
 
86
68
  say "Webpacker now supports breezy.js 🎉", :green