rails_redhot 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5be8aff1e9314532378f476e76fe310b84bfe386abe0ba18c33ec60820696d24
4
- data.tar.gz: f77470f8cec6b501c56fb0102e1b63242b0a11a4c453d4bbe3b3068b148b88fe
3
+ metadata.gz: 8f5c72d2cc5029c74afb124b5bbc728d007f4d635ad9569781cdd5b2fa3f6e25
4
+ data.tar.gz: eaca540b27f70101cefaee8d29e78ab843c12d873ee89bc382b28a0af963ee28
5
5
  SHA512:
6
- metadata.gz: adbb2a15f3bcffe7b910b05217d5e564125915bd39710347b527a863462fb7bef50951358d0b2d31829994410abca865f88f183abe09e09e0aef8be56956a5bf
7
- data.tar.gz: 922d4eac6e294a8f25ac027eca82c457115884758fc785289c79867785618aa0f7a788975e5f66fcb4139bfd819cca39b4b263d6d7d8df989a6cdc03bec8f97d
6
+ metadata.gz: 3029222021c8639cba0a7d60e9684e4571bbcd391c1a8884fdf2386b0a0c0b6b28afb4e85816a9f6f6f95d50a80825de754e2dc74939ca4511f0c6eb6c57fa3b
7
+ data.tar.gz: 6a27cf8634d4ccb20e833a79352ab810d2fe674c2e077a2a9c672d993657d7eb90a9ce2af7bbbd6e72981f03f11c22b4953c92e636bea25c6c2cabe1f8b70940
data/CHANGELOG.md CHANGED
@@ -1,10 +1,28 @@
1
- # 0.0.2 - December 10, 2021
1
+ # 0.2.0 - March 14, 2023
2
+
3
+ - Demo application: Use Ruby 3.2.0, Rails 7.0.4.2
4
+ - Added simplecov and improve testcoverage
5
+ - Added reducer errors (ActiveModel::Errors), separate from the models own error object
6
+ - Use deep_dup before dispatching an action to make sure the original action is never
7
+ modified by reducer methods. Use deep_symbolize_keys on an action for convenience
8
+ - Added after_change callback
9
+
10
+ # 0.1.1 - April 14, 2022
11
+
12
+ - Fix calling flatten! multiple times erased initial state
13
+ - Documentation updates
14
+ - Demo application: Use Ruby 3.1.2, Rails 7.0.2
2
15
 
3
- [0.0.2]: https://github.com/easydatawarehousing/rails_redhot/compare/v0.0.1...v0.0.2
16
+ # 0.1.0 - December 30, 2021
17
+
18
+ - Documentation updates
19
+ - Use Rails 7.0.0
20
+
21
+ # 0.0.2 - December 10, 2021
4
22
 
5
23
  - Added testset
6
24
  - Polished demo application
7
25
 
8
26
  # 0.0.1 - November 16, 2021
9
27
 
10
- - Initial release, not pulished to rubygems
28
+ - Initial release, not published to rubygems
data/README.md CHANGED
@@ -1,34 +1,76 @@
1
1
  # RailsRedhot gem
2
- __REDux pattern for HOTwire == Redhot__
2
+ ### __REDux pattern for HOTwire == Redhot__
3
3
  Single page applications using redux (react) are very popular.
4
4
  And with good reason, redux makes maintaining the current state of the app easy.
5
5
  For instance when building some kind of editor, every action of the user is added
6
6
  to the redux store. All actions can be reduced to the determine the current state of
7
7
  the editor. Views are rendered using the current state.
8
- Or when building a search page for a webshop. Whenever the user selects a category
8
+ Or when building a complex search page for a webshop. Whenever the user selects a category
9
9
  or price range to filter on this can be an action for the redux store. If the user
10
10
  hits the back button the last action can be deleted and the current state
11
11
  regenerated by reducing all remaining actions. The user only sees the last filter
12
12
  being reverted to what it was before.
13
13
 
14
+ ### What is redux?
15
+ ```
16
+ It is a store containing a list of changes.
17
+ All changes combined determine the current view state through one or more reducer functions.
18
+ The current view state is also stored.
19
+ If a new change arrives it only needs to be applied to the current view state.
20
+ To undo a change apply all but the last action again to rebuild te view state.
21
+
22
+ For example a view contains a number counting the likes for an article.
23
+ There are two buttons: increase- and decrease the likes counter.
24
+ Clicking on a button adds an action to the store.
25
+ The action is passed on to all reducer functions,
26
+ each function passes the new computed state on to the next function:
27
+ - If the counter value is nil in the current state set it to zero
28
+ - If the action was 'increase' then increment the counter value
29
+ - If the action was 'decrease' then decrement the counter value
30
+ - If the counter value is lower than zero set it to zero
31
+ Save the new state in the store.
32
+ Render the view showing the updated counter value.
33
+ ```
34
+
35
+ ### What are the advantages of redux?
36
+ From the [redux website](https://redux.js.org/) (minus the part about plugins):
37
+ ```
38
+ Predictable
39
+ Redux helps you write applications that behave consistently,
40
+ run in different environments (client, server, and native)
41
+ and are easy to test.
42
+
43
+ Centralized
44
+ Centralizing your application's state and logic enables powerful capabilities
45
+ like undo/redo, state persistence, and much more.
46
+
47
+ Debuggable
48
+ The Redux DevTools make it easy to trace when, where, why
49
+ and how your application's state changed.
50
+ Redux's architecture lets you log changes, use 'time-travel debugging'
51
+ and even send complete error reports to a server.
52
+ ```
53
+
54
+ ### Remove complexity
14
55
  Sometimes the actions of the user in the frontend should be sent to a backend
15
56
  application. For instance when actions of multiple users should be kept in-sync.
16
- Often command-query-responsibility-separation (CQRS) is used for this purpose.
17
- These solutions can become very complex
57
+ In react applications command-query-responsibility-separation (CQRS) is often
58
+ used for this purpose. These solutions can become very complex
18
59
  ([example](https://medium.com/resolvejs/resolve-redux-backend-ebcfc79bbbea),
19
- scroll down a bit for a full example).
60
+ scroll down a bit for a full architecture picture).
20
61
 
21
62
  The Hotwire (Html Over The Wire) approach does an excellent job of removing the need
22
- to build single page apps. Hotwire will be the
63
+ to build single page apps. Hotwire is the
23
64
  [default tool](https://world.hey.com/dhh/the-time-is-right-for-hotwire-ecdb9b33)
24
65
  for frontend development in Rails 7.
25
66
  However when using hotwire the responsibilty of maintaining frontend state entirely
26
67
  falls to the backend application. So when building your editor or search page
27
- you need a way to keep track of that state. The redux (also known as flux) pattern
28
- is still very useful for this purpose.
68
+ you need a way to keep track of that state. The redux (also known as flux- or observer-)
69
+ pattern is very useful for this purpose.
29
70
 
71
+ ### Hotwire
30
72
  This gem aims to combine html-over-the-wire approach with the redux pattern to
31
- radically reduce complexity of the overall application.
73
+ radically reduce overall complexity of an application.
32
74
  (At least when compared to for instance react+cqrs application stacks.)
33
75
  Only four components are required:
34
76
 
@@ -39,9 +81,16 @@ Only four components are required:
39
81
  4. Reducers, a set of functions (provided by you) that translate actions to changes in
40
82
  state. The state can be used again in step 1
41
83
 
42
- Common actions (undo, redo, flatten actions to initial state) are provided by this gem.
43
- Combined with turbo frames for rendering partial page updates this makes it easy to
44
- create a very smooth user experience.
84
+ ### Benefits
85
+
86
+ - Straightforward workflow
87
+ - Common actions (undo, redo, flatten actions to initial state) are provided by this gem.
88
+ Combined with turbo frames for rendering partial page updates this makes it easy to
89
+ create a very smooth user experience
90
+ - You can create a store of attributes within a single ActiveRecord model.
91
+ In a Single Page App (SPA) lots of settings may be needed for a good user
92
+ experience. It may be a lot of work to store these in multiple models.
93
+ A redux store can hold an arbitrary amount of attributes
45
94
 
46
95
  ## Usage
47
96
  ### Model
@@ -60,15 +109,17 @@ class Foobar < ApplicationRecord
60
109
  private
61
110
 
62
111
  def my_redux_reducers
63
- ->(state, action) {
64
- case action[:type]
65
- when :add
66
- state[:total] += 1
67
- when :remove
68
- state[:total] -= 1
69
- end
70
- state
71
- },
112
+ [
113
+ ->(state, action) {
114
+ case action[:type]
115
+ when :add
116
+ state[:total] += 1
117
+ when :remove
118
+ state[:total] -= 1
119
+ end
120
+ state
121
+ }
122
+ ]
72
123
  end
73
124
  end
74
125
  ```
@@ -79,7 +130,7 @@ Or specify your own reducer method:
79
130
  acts_as_redux :my_redux, reducers: :my_list_of_reducers
80
131
 
81
132
  def my_list_of_reducers
82
- # ...
133
+ # ...
83
134
  ```
84
135
 
85
136
  ### Undo/redo
@@ -99,7 +150,8 @@ In the controller action use the `undo!` method to perform the action.
99
150
  For redoing actions the similar methods `redo?`, `redo_action` and `redo!` are available.
100
151
 
101
152
  ### Flatten
102
- You can 'save' the current state. Essentially this flattens the list of actions to the initial state.
153
+ You can 'save' the current state. Essentially this copies the current view state to the initial state
154
+ and truncates the list of actions. Redo and undo are not possible until new actions are added.
103
155
  Methods `flatten?` and `flatten!` can be used in a view and controller:
104
156
 
105
157
  ```ruby
@@ -112,7 +164,7 @@ Methods `flatten?` and `flatten!` can be used in a view and controller:
112
164
  ```
113
165
 
114
166
  ### Sequence ID
115
- As a convenience a sequence ID id available which should always return a unique id
167
+ As a convenience a sequence ID id is available which should always return a unique id
116
168
  (within the context of the model instance). To get the next sequence id use `next_seq_id`,
117
169
  to get the current sequence value use `seq_id`.
118
170
  You could use a sequence in a reducer function to make sure every added item is assigned a unique id.
@@ -143,11 +195,86 @@ on the reducer functions you have implemented.
143
195
  For a full working example see the demo applications [view](test/dummy/app/views/foobars/_editor.html.erb)
144
196
  and [controller](test/dummy/app/controllers/foobars_controller.rb).
145
197
 
198
+ ### Adding errors
199
+ Just like you can add validation errors on a model, you can add errors inside your reducer methods.
200
+ There is an ActiveModel::Errors object for redux errors. It is separate from the one for the model and
201
+ can be accessed via `reduce_errors`. Use it like this:
202
+
203
+ ```ruby
204
+ def my_redux_reducers
205
+ @my_redux_reducers ||= [
206
+ -> (state, action) {
207
+ case action[:type]&.to_sym
208
+ when :add
209
+ if action[:item].length <= 6
210
+ state[:items] << { id: next_seq_id, value: CGI.escape(action[:item]) }
211
+ else
212
+ reduce_errors.add(:item, :too_long, { count: 6 })
213
+ end
214
+ end
215
+
216
+ state
217
+ }
218
+ ]
219
+ end
220
+ ```
221
+
222
+ Since ActiveModel does not know anything about attributes living inside your redux store
223
+ using `reduce_errors.full_messages` won't work. You can create you own error to message translation or
224
+ supply `:base` as the attribute name plus a message.
225
+ See the [Rails documentation](https://api.rubyonrails.org/classes/ActiveModel/Errors.html#method-i-add).
226
+
227
+ ```ruby
228
+ reduce_errors.add(:base, message: 'Item should have between 1 and 6 characters')
229
+ ```
230
+
231
+ If any reduce error is present the `dispatch!` method will return false.
232
+ To check if there are any errors present (to prevent saving the model) use:
233
+
234
+ ```ruby
235
+ @foobar.reduce_valid?
236
+ ```
237
+
238
+ In the controller you may want to reload the model if the dispatch action gave an error,
239
+ so the old state is rendered.
240
+
241
+ ### After change callback
242
+ Sometimes you want to do something after the redux store has changed.
243
+ For instance to manipulate the view state based on all entries
244
+ (the reducer methods only handle one action at the time).
245
+ This callback method is called after `dispatch!`, `undo!` and `redo!`.
246
+ In the model:
247
+
248
+ ```ruby
249
+ class AnotherFooBar < ApplicationRecord
250
+ include RailsRedhot::ActsAsRedux
251
+
252
+ acts_as_redux :another_redux_store, after_change: :my_after_change_actions
253
+
254
+ private
255
+
256
+ def another_redux_store_reducers
257
+ [
258
+ -> (state, _action) {
259
+ state[:items] ||= []
260
+ state
261
+ },
262
+ # ...
263
+ ]
264
+ end
265
+
266
+ def my_after_change_actions
267
+ # Do something with the view_state
268
+ # view_state[:items].each { do_something }
269
+ end
270
+ end
271
+ ```
272
+
146
273
  ## Security
147
- Care must be taken to not introduce any vulnerabilities.
274
+ Care must be taken to not introduce any vulnerabilities!
148
275
  When passing values from the request to the reducer functions treat any string or complex
149
- values as potential candidates for SQL injection. Either sanitize the value or `CGI.escape`
150
- a string before adding it to the redux store.
276
+ values as potential candidates for SQL injection. Either sanitize or `CGI.escape`
277
+ strings before adding them to the redux store.
151
278
 
152
279
  ## Installation
153
280
  Add this line to your application's Gemfile:
@@ -175,7 +302,7 @@ cd rails_redhot
175
302
  bundle install
176
303
  cd test/dummy
177
304
  rails db:setup
178
- rails server
305
+ bin/dev
179
306
  ```
180
307
 
181
308
  Then open the [application](http://localhost:3000/foobars).
@@ -193,10 +320,9 @@ The gem is available as open source under the terms of the [MIT License](https:/
193
320
 
194
321
  ## Remarks
195
322
 
196
- - Developed using Ruby 3.0.3
197
323
  - This gem is not designed to handle very large lists of actions and state.
198
- When calling `undo` the state is rebuilt from scratch
199
- If the list of actions to process is large this would become slow.
324
+ When calling `undo` the state is rebuilt from scratch,
325
+ if the list of actions to process is large this would become slow.
200
326
  One would need add 'savepoints' that regularly save the state and rebuild
201
327
  the current state from that point forward
202
328
  - Stricly speaking, hotwire is not needed for this gem to work. Just using
@@ -8,6 +8,8 @@ module RailsRedhot
8
8
  class_methods do
9
9
  def acts_as_redux(store_name, options = {})
10
10
  reducers = options.key?(:reducers) ? options[:reducers] : "#{store_name}_reducers".to_sym
11
+ reducer_errors = nil
12
+ reducer_after_change = options[:after_change]
11
13
 
12
14
  store(store_name, accessors: [ :initial_state, :state, :actions, :head, :seq_id ], coder: JSON)
13
15
 
@@ -43,8 +45,7 @@ module RailsRedhot
43
45
  self.state = initial_state
44
46
  if head > -1
45
47
  actions[0..head].each { |action| perform_reduce(action) }
46
- else
47
- perform_reduce(initial_state)
48
+ self.send(reducer_after_change) if reducer_after_change
48
49
  end
49
50
  true
50
51
  else
@@ -56,6 +57,7 @@ module RailsRedhot
56
57
  if redo?
57
58
  self.head += 1
58
59
  perform_reduce(actions[head])
60
+ self.send(reducer_after_change) if reducer_after_change
59
61
  true
60
62
  else
61
63
  false
@@ -63,13 +65,21 @@ module RailsRedhot
63
65
  end
64
66
 
65
67
  define_method('flatten!') do
66
- self.initial_state = state
68
+ self.initial_state = view_state
67
69
  self.state = nil
68
70
  self.head = -1
69
71
  self.actions = []
70
72
  true
71
73
  end
72
74
 
75
+ define_method('reduce_errors') do
76
+ reducer_errors
77
+ end
78
+
79
+ define_method('reduce_valid?') do
80
+ reducer_errors.details.empty?
81
+ end
82
+
73
83
  define_method('next_seq_id') do
74
84
  self.seq_id += 1
75
85
  end
@@ -80,19 +90,29 @@ module RailsRedhot
80
90
 
81
91
  self.actions << action
82
92
  self.head += 1
83
- perform_reduce(action)
84
- true
93
+ perform_reduce(action.deep_dup.deep_symbolize_keys)
94
+ self.send(reducer_after_change) if reducer_after_change
95
+ reduce_valid?
85
96
  end
86
97
 
87
98
  # private
88
99
 
100
+ define_method('reset_reduce_errors') do
101
+ reducer_errors = ActiveModel::Errors.new(self)
102
+ end
103
+
89
104
  define_method('load_store') do
90
105
  self.initial_state ||= {}
91
106
  # self.state is initially nil: no need to store state twice when there are no actions
92
107
  self.head ||= -1
93
108
  self.actions ||= []
94
109
  self.seq_id ||= 0
95
- perform_reduce({}) if state.blank? && initial_state.blank?
110
+
111
+ if state.blank? && initial_state.blank?
112
+ perform_reduce({})
113
+ else
114
+ reset_reduce_errors
115
+ end
96
116
  end
97
117
 
98
118
  define_method('all_reducers') do
@@ -100,10 +120,12 @@ module RailsRedhot
100
120
  end
101
121
 
102
122
  define_method('perform_reduce') do |action|
123
+ reset_reduce_errors
124
+
103
125
  self.state = all_reducers.reduce(
104
- view_state.dup.deep_symbolize_keys
126
+ view_state.deep_dup.deep_symbolize_keys
105
127
  ) do |current_state, reducer|
106
- reducer.call(current_state, action.deep_symbolize_keys)
128
+ reducer.call(current_state, action)
107
129
  end
108
130
  end
109
131
 
@@ -1,4 +1,4 @@
1
1
  module RailsRedhot
2
2
  # Gem version
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_redhot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivo Herweijer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-30 00:00:00.000000000 Z
11
+ date: 2023-03-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -61,7 +61,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
61
61
  - !ruby/object:Gem::Version
62
62
  version: '0'
63
63
  requirements: []
64
- rubygems_version: 3.2.32
64
+ rubygems_version: 3.4.6
65
65
  signing_key:
66
66
  specification_version: 4
67
67
  summary: REDux pattern for HOTwire == Redhot