breezy 0.13.0 → 0.17.1

Sign up to get free protection for your applications and to get access to all the features.
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