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 +4 -4
- data/CHANGELOG.md +21 -3
- data/README.md +157 -31
- data/lib/rails_redhot/acts_as_redux.rb +30 -8
- data/lib/rails_redhot/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8f5c72d2cc5029c74afb124b5bbc728d007f4d635ad9569781cdd5b2fa3f6e25
|
4
|
+
data.tar.gz: eaca540b27f70101cefaee8d29e78ab843c12d873ee89bc382b28a0af963ee28
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3029222021c8639cba0a7d60e9684e4571bbcd391c1a8884fdf2386b0a0c0b6b28afb4e85816a9f6f6f95d50a80825de754e2dc74939ca4511f0c6eb6c57fa3b
|
7
|
+
data.tar.gz: 6a27cf8634d4ccb20e833a79352ab810d2fe674c2e077a2a9c672d993657d7eb90a9ce2af7bbbd6e72981f03f11c22b4953c92e636bea25c6c2cabe1f8b70940
|
data/CHANGELOG.md
CHANGED
@@ -1,10 +1,28 @@
|
|
1
|
-
# 0.0
|
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
|
-
|
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
|
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
|
-
|
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
|
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
|
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)
|
28
|
-
is
|
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
|
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
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
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
|
150
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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.
|
126
|
+
view_state.deep_dup.deep_symbolize_keys
|
105
127
|
) do |current_state, reducer|
|
106
|
-
reducer.call(current_state, action
|
128
|
+
reducer.call(current_state, action)
|
107
129
|
end
|
108
130
|
end
|
109
131
|
|
data/lib/rails_redhot/version.rb
CHANGED
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.
|
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:
|
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.
|
64
|
+
rubygems_version: 3.4.6
|
65
65
|
signing_key:
|
66
66
|
specification_version: 4
|
67
67
|
summary: REDux pattern for HOTwire == Redhot
|