datastar 1.0.0.beta.2 → 1.0.0.pre.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 23de694fa3da7ad6bd9cc78a02851ec22ab16f5d16e475beb9bc175569fbce02
4
- data.tar.gz: eab01bb873a350c595dcdaddddb722ea2a69aef584b86f1bab5309f61764a699
3
+ metadata.gz: cc457ac67196befbcb6a6c1510b970e81e728261dff0987d42d7760e358ad7fe
4
+ data.tar.gz: 5dcf48dc09ee9adb4c08f53d2ef63df316a34929ca6d3bcf0d15cf0d27083540
5
5
  SHA512:
6
- metadata.gz: 3f386d1039d1a9fc53b698ce9d4390f595dcf1dfec1166c03d7bb1fa17ec87c062089386151cc7d1ea6117e4b8350d6d819458215b3f305bc3e756f1a691dba2
7
- data.tar.gz: 88e7e8505d1640151bcd494aca0000b3ee5d935f0ad426b937372d6c93b8191e5a2b4838e0ed86a58eeab11e5a5e66e92a92841967812f4095564ebc39adbf36
6
+ metadata.gz: edba4b371362360de36fcfb7379a80b96ac5743c7d765a264bf8dbd21a223147e44677d05fea02f1136d8db5aa84579af943e3a0593662d4cecdbfe5155df96a
7
+ data.tar.gz: fa551cb816ca6cfe0ea46ec53c9b1d51256009c6eb378b77df51a1376a297ec3fea97bebd26ef8dab330d857f366bbbacf32dd7e1a6c20d97b9e097942ee5301
data/README.md CHANGED
@@ -44,7 +44,7 @@ There are two ways to use this gem in HTTP handlers:
44
44
  #### One-off update:
45
45
 
46
46
  ```ruby
47
- datastar.merge_fragments(%(<h1 id="title">Hello, World!</h1>))
47
+ datastar.patch_elements(%(<h1 id="title">Hello, World!</h1>))
48
48
  ```
49
49
  In this mode, the response is closed after the fragment is sent.
50
50
 
@@ -52,11 +52,11 @@ In this mode, the response is closed after the fragment is sent.
52
52
 
53
53
  ```ruby
54
54
  datastar.stream do |sse|
55
- sse.merge_fragments(%(<h1 id="title">Hello, World!</h1>))
55
+ sse.patch_elements(%(<h1 id="title">Hello, World!</h1>))
56
56
  # Streaming multiple updates
57
57
  100.times do |i|
58
58
  sleep 1
59
- sse.merge_fragments(%(<h1 id="title">Hello, World #{i}!</h1>))
59
+ sse.patch_elements(%(<h1 id="title">Hello, World #{i}!</h1>))
60
60
  end
61
61
  end
62
62
  ```
@@ -72,14 +72,14 @@ Their updates are linearized and sent to the browser as they are produced.
72
72
  datastar.stream do |sse|
73
73
  100.times do |i|
74
74
  sleep 1
75
- sse.merge_fragments(%(<h1 id="slow">#{i}!</h1>))
75
+ sse.patch_elements(%(<h1 id="slow">#{i}!</h1>))
76
76
  end
77
77
  end
78
78
 
79
79
  datastar.stream do |sse|
80
80
  1000.times do |i|
81
81
  sleep 0.1
82
- sse.merge_fragments(%(<h1 id="fast">#{i}!</h1>))
82
+ sse.patch_elements(%(<h1 id="fast">#{i}!</h1>))
83
83
  end
84
84
  end
85
85
  ```
@@ -90,48 +90,73 @@ See the [examples](https://github.com/starfederation/datastar/tree/main/examples
90
90
 
91
91
  All these methods are available in both the one-off and the streaming modes.
92
92
 
93
- #### `merge_fragments`
94
- See https://data-star.dev/reference/sse_events#datastar-merge-fragments
93
+ #### `patch_elements`
94
+ See https://data-star.dev/reference/sse_events#datastar-patch-elements
95
95
 
96
96
  ```ruby
97
- sse.merge_fragments(%(<div id="foo">\n<span>hello</span>\n</div>))
97
+ sse.patch_elements(%(<div id="foo">\n<span>hello</span>\n</div>))
98
98
 
99
99
  # or a Phlex view object
100
- sse.merge_fragments(UserComponet.new)
100
+ sse.patch_elements(UserComponent.new)
101
101
 
102
102
  # Or pass options
103
- sse.merge_fragments(
103
+ sse.patch_elements(
104
104
  %(<div id="foo">\n<span>hello</span>\n</div>),
105
- merge_mode: 'append'
105
+ mode: 'append'
106
106
  )
107
107
  ```
108
108
 
109
- #### `remove_fragments`
110
- See https://data-star.dev/reference/sse_events#datastar-remove-fragments
109
+ You can patch multiple elements at once by passing an array of elements (or components):
111
110
 
112
111
  ```ruby
113
- sse.remove_fragments('#users')
112
+ sse.patch_elements([
113
+ %(<div id="foo">\n<span>hello</span>\n</div>),
114
+ %(<div id="bar">\n<span>world</span>\n</div>)
115
+ ])
116
+ ```
117
+
118
+ #### `remove_elements`
119
+
120
+ Sugar on top of `#patch_elements`
121
+ See https://data-star.dev/reference/sse_events#datastar-patch-elements
122
+
123
+ ```ruby
124
+ sse.remove_elements('#users')
114
125
  ```
115
126
 
116
- #### `merge_signals`
117
- See https://data-star.dev/reference/sse_events#datastar-merge-signals
127
+ #### `patch_signals`
128
+ See https://data-star.dev/reference/sse_events#datastar-patch-signals
118
129
 
119
130
  ```ruby
120
- sse.merge_signals(count: 4, user: { name: 'John' })
131
+ sse.patch_signals(count: 4, user: { name: 'John' })
121
132
  ```
122
133
 
123
134
  #### `remove_signals`
124
- See https://data-star.dev/reference/sse_events#datastar-remove-signals
135
+
136
+ Sugar on top of `#patch_signals`
125
137
 
126
138
  ```ruby
127
139
  sse.remove_signals(['user.name', 'user.email'])
128
140
  ```
129
141
 
130
142
  #### `execute_script`
131
- See https://data-star.dev/reference/sse_events#datastar-execute-script
143
+
144
+ Sugar on top of `#patch_elements`. Appends a temporary `<script>` tag to the DOM, which will execute the script in the browser.
132
145
 
133
146
  ```ruby
134
- sse.execute_scriprt(%(alert('Hello World!'))
147
+ sse.execute_script(%(alert('Hello World!'))
148
+ ```
149
+
150
+ Pass `attributes` that will be added to the `<script>` tag:
151
+
152
+ ```ruby
153
+ sse.execute_script(%(alert('Hello World!')), attributes: { type: 'text/javascript' })
154
+ ```
155
+
156
+ These script tags are automatically removed after execution, so they can be used to run one-off scripts in the browser. Pass `auto_remove: false` if you want to keep the script tag in the DOM.
157
+
158
+ ```ruby
159
+ sse.execute_script(%(alert('Hello World!')), auto_remove: false)
135
160
  ```
136
161
 
137
162
  #### `signals`
@@ -165,7 +190,7 @@ end
165
190
  Register server-side code to run when the connection is closed by the client
166
191
 
167
192
  ```ruby
168
- datastar.on_client_connect do
193
+ datastar.on_client_disconnect do
169
194
  puts 'A user has disconnected connected'
170
195
  end
171
196
  ```
@@ -178,7 +203,7 @@ Register server-side code to run when the connection is closed by the server.
178
203
  Ie when the served is done streaming without errors.
179
204
 
180
205
  ```ruby
181
- datastar.on_server_connect do
206
+ datastar.on_server_disconnect do
182
207
  puts 'Server is done streaming'
183
208
  end
184
209
  ```
@@ -264,25 +289,25 @@ datastar.stream do |sse|
264
289
  10.times do |i|
265
290
  sleep 1
266
291
  tpl = render_to_string('events/user', layout: false, locals: { name: "David #{i}" })
267
- sse.merge_fragments tpl
292
+ sse.patch_elements tpl
268
293
  end
269
294
  end
270
295
  ```
271
296
 
272
297
  ### Rendering Phlex components
273
298
 
274
- `#merge_fragments` supports [Phlex](https://www.phlex.fun) component instances.
299
+ `#patch_elements` supports [Phlex](https://www.phlex.fun) component instances.
275
300
 
276
301
  ```ruby
277
- sse.merge_fragments(UserComponent.new(user: User.first))
302
+ sse.patch_elements(UserComponent.new(user: User.first))
278
303
  ```
279
304
 
280
305
  ### Rendering ViewComponent instances
281
306
 
282
- `#merge_fragments` also works with [ViewComponent](https://viewcomponent.org) instances.
307
+ `#patch_elements` also works with [ViewComponent](https://viewcomponent.org) instances.
283
308
 
284
309
  ```ruby
285
- sse.merge_fragments(UserViewComponent.new(user: User.first))
310
+ sse.patch_elements(UserViewComponent.new(user: User.first))
286
311
  ```
287
312
 
288
313
  ### Rendering `#render_in(view_context)` interfaces
@@ -302,7 +327,7 @@ end
302
327
  ```
303
328
 
304
329
  ```ruby
305
- sse.merge_fragments MyComponent.new('Joe')
330
+ sse.patch_elements MyComponent.new('Joe')
306
331
  ```
307
332
 
308
333
 
@@ -338,6 +363,12 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
338
363
 
339
364
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
340
365
 
366
+ ### Building
367
+
368
+ To build `consts.rb` file from template, run Docker and run `make task build`
369
+
370
+ The template is located at `build/consts_ruby.gtpl`.
371
+
341
372
  ## Contributing
342
373
 
343
374
  Bug reports and pull requests are welcome on GitHub at https://github.com/starfederation/datastar.
data/examples/test.ru CHANGED
@@ -35,21 +35,17 @@ run do |env|
35
35
  sse.signals['events'].each do |event|
36
36
  type = event.delete('type')
37
37
  case type
38
- when 'mergeSignals'
39
- arg = event.delete('signals')
40
- sse.merge_signals(arg, event)
41
- when 'removeSignals'
42
- arg = event.delete('paths')
43
- sse.remove_signals(arg, event)
38
+ when 'patchSignals'
39
+ arg = event.delete('signals') || event.delete('signals-raw')
40
+ sse.patch_signals(arg, event)
44
41
  when 'executeScript'
45
42
  arg = event.delete('script')
46
43
  sse.execute_script(arg, event)
47
- when 'mergeFragments'
48
- arg = event.delete('fragments')
49
- sse.merge_fragments(arg, event)
50
- when 'removeFragments'
51
- arg = event.delete('selector')
52
- sse.remove_fragments(arg, event)
44
+ when 'patchElements'
45
+ arg = event.delete('elements')
46
+ sse.patch_elements(arg, event)
47
+ else
48
+ raise "Unknown event type: #{type}"
53
49
  end
54
50
  end
55
51
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'thread'
4
+ require 'logger'
4
5
 
5
6
  module Datastar
6
7
  # The default executor based on Ruby threads
@@ -30,17 +31,19 @@ module Datastar
30
31
  # You'd normally do this on app initialization
31
32
  # For example in a Rails initializer
32
33
  class Configuration
33
- NOOP_CALLBACK = ->(_error) {}
34
34
  RACK_FINALIZE = ->(_view_context, response) { response.finish }
35
35
  DEFAULT_HEARTBEAT = 3
36
36
 
37
- attr_accessor :executor, :error_callback, :finalize, :heartbeat
37
+ attr_accessor :executor, :error_callback, :finalize, :heartbeat, :logger
38
38
 
39
39
  def initialize
40
40
  @executor = ThreadExecutor.new
41
- @error_callback = NOOP_CALLBACK
42
41
  @finalize = RACK_FINALIZE
43
42
  @heartbeat = DEFAULT_HEARTBEAT
43
+ @logger = Logger.new(STDOUT)
44
+ @error_callback = proc do |e|
45
+ @logger.error("#{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}")
46
+ end
44
47
  end
45
48
 
46
49
  def on_error(callable = nil, &block)
@@ -4,67 +4,54 @@
4
4
  module Datastar
5
5
  module Consts
6
6
  DATASTAR_KEY = 'datastar'
7
- VERSION = '1.0.0-beta.5'
8
-
9
- # The default duration for settling during fragment merges. Allows for CSS transitions to complete.
10
- DEFAULT_FRAGMENTS_SETTLE_DURATION = 300
7
+ VERSION = '1.0.0-RC.13'
11
8
 
12
9
  # The default duration for retrying SSE on connection reset. This is part of the underlying retry mechanism of SSE.
13
10
  DEFAULT_SSE_RETRY_DURATION = 1000
14
11
 
15
- # Should fragments be merged using the ViewTransition API?
16
- DEFAULT_FRAGMENTS_USE_VIEW_TRANSITIONS = false
17
-
18
- # Should a given set of signals merge if they are missing?
19
- DEFAULT_MERGE_SIGNALS_ONLY_IF_MISSING = false
12
+ # Should elements be patched using the ViewTransition API?
13
+ DEFAULT_ELEMENTS_USE_VIEW_TRANSITIONS = false
20
14
 
21
- # Should script element remove itself after execution?
22
- DEFAULT_EXECUTE_SCRIPT_AUTO_REMOVE = true
15
+ # Should a given set of signals patch if they are missing?
16
+ DEFAULT_PATCH_SIGNALS_ONLY_IF_MISSING = false
23
17
 
24
- # The default attributes for <script/> element use when executing scripts. It is a set of key-value pairs delimited by a newline \\n character.}
25
- DEFAULT_EXECUTE_SCRIPT_ATTRIBUTES = 'type module'
18
+ module ElementPatchMode
26
19
 
27
- module FragmentMergeMode
28
-
29
- # Morphs the fragment into the existing element using idiomorph.
30
- MORPH = 'morph'
20
+ # Morphs the element into the existing element.
21
+ OUTER = 'outer';
31
22
 
32
23
  # Replaces the inner HTML of the existing element.
33
- INNER = 'inner'
24
+ INNER = 'inner';
34
25
 
35
- # Replaces the outer HTML of the existing element.
36
- OUTER = 'outer'
26
+ # Removes the existing element.
27
+ REMOVE = 'remove';
37
28
 
38
- # Prepends the fragment to the existing element.
39
- PREPEND = 'prepend'
29
+ # Replaces the existing element with the new element.
30
+ REPLACE = 'replace';
40
31
 
41
- # Appends the fragment to the existing element.
42
- APPEND = 'append'
32
+ # Prepends the element inside to the existing element.
33
+ PREPEND = 'prepend';
43
34
 
44
- # Inserts the fragment before the existing element.
45
- BEFORE = 'before'
35
+ # Appends the element inside the existing element.
36
+ APPEND = 'append';
46
37
 
47
- # Inserts the fragment after the existing element.
48
- AFTER = 'after'
38
+ # Inserts the element before the existing element.
39
+ BEFORE = 'before';
49
40
 
50
- # Upserts the attributes of the existing element.
51
- UPSERT_ATTRIBUTES = 'upsertAttributes'
41
+ # Inserts the element after the existing element.
42
+ AFTER = 'after';
52
43
  end
53
44
 
54
- # The mode in which a fragment is merged into the DOM.
55
- DEFAULT_FRAGMENT_MERGE_MODE = FragmentMergeMode::MORPH
45
+
46
+ # The mode in which an element is patched into the DOM.
47
+ DEFAULT_ELEMENT_PATCH_MODE = ElementPatchMode::OUTER
56
48
 
57
49
  # Dataline literals.
58
50
  SELECTOR_DATALINE_LITERAL = 'selector'
59
- MERGE_MODE_DATALINE_LITERAL = 'mergeMode'
60
- SETTLE_DURATION_DATALINE_LITERAL = 'settleDuration'
61
- FRAGMENTS_DATALINE_LITERAL = 'fragments'
51
+ MODE_DATALINE_LITERAL = 'mode'
52
+ ELEMENTS_DATALINE_LITERAL = 'elements'
62
53
  USE_VIEW_TRANSITION_DATALINE_LITERAL = 'useViewTransition'
63
54
  SIGNALS_DATALINE_LITERAL = 'signals'
64
55
  ONLY_IF_MISSING_DATALINE_LITERAL = 'onlyIfMissing'
65
- PATHS_DATALINE_LITERAL = 'paths'
66
- SCRIPT_DATALINE_LITERAL = 'script'
67
- ATTRIBUTES_DATALINE_LITERAL = 'attributes'
68
- AUTO_REMOVE_DATALINE_LITERAL = 'autoRemove'
69
56
  end
70
57
  end
@@ -10,20 +10,21 @@ module Datastar
10
10
  # datastar = Datastar.new(request:, response:, view_context: self)
11
11
  #
12
12
  # # One-off fragment response
13
- # datastar.merge_fragments(template)
13
+ # datastar.patch_elements(template)
14
14
  #
15
15
  # # Streaming response with multiple messages
16
16
  # datastar.stream do |sse|
17
- # sse.merge_fragments(template)
17
+ # sse.patch_elements(template)
18
18
  # 10.times do |i|
19
19
  # sleep 0.1
20
- # sse.merge_signals(count: i)
20
+ # sse.patch_signals(count: i)
21
21
  # end
22
22
  # end
23
23
  #
24
24
  class Dispatcher
25
25
  BLANK_BODY = [].freeze
26
26
  SSE_CONTENT_TYPE = 'text/event-stream'
27
+ SSE_ACCEPT_EXP = /text\/event-stream/
27
28
  HTTP_ACCEPT = 'HTTP_ACCEPT'
28
29
  HTTP1 = 'HTTP/1.1'
29
30
 
@@ -72,7 +73,7 @@ module Datastar
72
73
  # Check if the request accepts SSE responses
73
74
  # @return [Boolean]
74
75
  def sse?
75
- @request.get_header(HTTP_ACCEPT) == SSE_CONTENT_TYPE
76
+ !!(@request.get_header(HTTP_ACCEPT).to_s =~ SSE_ACCEPT_EXP)
76
77
  end
77
78
 
78
79
  # Register an on-connect callback
@@ -119,47 +120,48 @@ module Datastar
119
120
  @signals ||= parse_signals(request).freeze
120
121
  end
121
122
 
122
- # Send one-off fragments to the UI
123
- # See https://data-star.dev/reference/sse_events#datastar-merge-fragments
123
+ # Send one-off elements to the UI
124
+ # See https://data-star.dev/reference/sse_events#datastar-patch-elements
124
125
  # @example
125
126
  #
126
- # datastar.merge_fragments(%(<div id="foo">\n<span>hello</span>\n</div>\n))
127
+ # datastar.patch_elements(%(<div id="foo">\n<span>hello</span>\n</div>\n))
127
128
  # # or a Phlex view object
128
- # datastar.merge_fragments(UserComponet.new)
129
+ # datastar.patch_elements(UserComponet.new)
129
130
  #
130
- # @param fragments [String, #call(view_context: Object) => Object] the HTML fragment or object
131
+ # @param elements [String, #call(view_context: Object) => Object] the HTML elements or object
131
132
  # @param options [Hash] the options to send with the message
132
- def merge_fragments(fragments, options = BLANK_OPTIONS)
133
+ def patch_elements(elements, options = BLANK_OPTIONS)
133
134
  stream_no_heartbeat do |sse|
134
- sse.merge_fragments(fragments, options)
135
+ sse.patch_elements(elements, options)
135
136
  end
136
137
  end
137
138
 
138
- # One-off remove fragments from the UI
139
- # See https://data-star.dev/reference/sse_events#datastar-remove-fragments
139
+ # One-off remove elements from the UI
140
+ # Sugar on top of patch-elements with mode: 'remove'
141
+ # See https://data-star.dev/reference/sse_events#datastar-patch-elements
140
142
  # @example
141
143
  #
142
- # datastar.remove_fragments('#users')
144
+ # datastar.remove_elements('#users')
143
145
  #
144
146
  # @param selector [String] a CSS selector for the fragment to remove
145
147
  # @param options [Hash] the options to send with the message
146
- def remove_fragments(selector, options = BLANK_OPTIONS)
148
+ def remove_elements(selector, options = BLANK_OPTIONS)
147
149
  stream_no_heartbeat do |sse|
148
- sse.remove_fragments(selector, options)
150
+ sse.remove_elements(selector, options)
149
151
  end
150
152
  end
151
153
 
152
- # One-off merge signals in the UI
153
- # See https://data-star.dev/reference/sse_events#datastar-merge-signals
154
+ # One-off patch signals in the UI
155
+ # See https://data-star.dev/reference/sse_events#datastar-patch-signals
154
156
  # @example
155
157
  #
156
- # datastar.merge_signals(count: 1, toggle: true)
158
+ # datastar.patch_signals(count: 1, toggle: true)
157
159
  #
158
- # @param signals [Hash] signals to merge
160
+ # @param signals [Hash, String] signals to merge
159
161
  # @param options [Hash] the options to send with the message
160
- def merge_signals(signals, options = BLANK_OPTIONS)
162
+ def patch_signals(signals, options = BLANK_OPTIONS)
161
163
  stream_no_heartbeat do |sse|
162
- sse.merge_signals(signals, options)
164
+ sse.patch_signals(signals, options)
163
165
  end
164
166
  end
165
167
 
@@ -209,9 +211,9 @@ module Datastar
209
211
  #
210
212
  # datastar.stream do |sse|
211
213
  # total = 300
212
- # sse.merge_fragments(%(<progress data-signal-progress="0" id="progress" max="#{total}" data-attr-value="$progress">0</progress>))
214
+ # sse.patch_elements(%(<progress data-signal-progress="0" id="progress" max="#{total}" data-attr-value="$progress">0</progress>))
213
215
  # total.times do |i|
214
- # sse.merge_signals(progress: i)
216
+ # sse.patch_signals(progress: i)
215
217
  # end
216
218
  # end
217
219
  #
@@ -14,6 +14,8 @@ module Datastar
14
14
  initializer 'datastar' do |_app|
15
15
  Datastar.config.finalize = FINALIZE
16
16
 
17
+ Datastar.config.logger = Rails.logger
18
+
17
19
  Datastar.config.executor = if config.active_support.isolation_level == :fiber
18
20
  require 'datastar/rails_async_executor'
19
21
  RailsAsyncExecutor.new
@@ -4,7 +4,7 @@ require 'json'
4
4
 
5
5
  module Datastar
6
6
  class ServerSentEventGenerator
7
- MSG_END = "\n\n"
7
+ MSG_END = "\n"
8
8
 
9
9
  SSE_OPTION_MAPPING = {
10
10
  'eventId' => 'id',
@@ -15,21 +15,12 @@ module Datastar
15
15
 
16
16
  OPTION_DEFAULTS = {
17
17
  'retry' => Consts::DEFAULT_SSE_RETRY_DURATION,
18
- Consts::AUTO_REMOVE_DATALINE_LITERAL => Consts::DEFAULT_EXECUTE_SCRIPT_AUTO_REMOVE,
19
- Consts::MERGE_MODE_DATALINE_LITERAL => Consts::DEFAULT_FRAGMENT_MERGE_MODE,
20
- Consts::SETTLE_DURATION_DATALINE_LITERAL => Consts::DEFAULT_FRAGMENTS_SETTLE_DURATION,
21
- Consts::USE_VIEW_TRANSITION_DATALINE_LITERAL => Consts::DEFAULT_FRAGMENTS_USE_VIEW_TRANSITIONS,
22
- Consts::ONLY_IF_MISSING_DATALINE_LITERAL => Consts::DEFAULT_MERGE_SIGNALS_ONLY_IF_MISSING,
18
+ Consts::MODE_DATALINE_LITERAL => Consts::DEFAULT_ELEMENT_PATCH_MODE,
19
+ Consts::USE_VIEW_TRANSITION_DATALINE_LITERAL => Consts::DEFAULT_ELEMENTS_USE_VIEW_TRANSITIONS,
20
+ Consts::ONLY_IF_MISSING_DATALINE_LITERAL => Consts::DEFAULT_PATCH_SIGNALS_ONLY_IF_MISSING,
23
21
  }.freeze
24
22
 
25
- # ATTRIBUTE_DEFAULTS = {
26
- # 'type' => 'module'
27
- # }.freeze
28
- ATTRIBUTE_DEFAULTS = Consts::DEFAULT_EXECUTE_SCRIPT_ATTRIBUTES
29
- .split("\n")
30
- .map { |attr| attr.split(' ') }
31
- .to_h
32
- .freeze
23
+ SIGNAL_SEPARATOR = '.'
33
24
 
34
25
  attr_reader :signals
35
26
 
@@ -46,59 +37,71 @@ module Datastar
46
37
  @stream << MSG_END
47
38
  end
48
39
 
49
- def merge_fragments(fragments, options = BLANK_OPTIONS)
50
- # Support Phlex components
51
- # And Rails' #render_in interface
52
- fragments = if fragments.respond_to?(:render_in)
53
- fragments.render_in(view_context)
54
- elsif fragments.respond_to?(:call)
55
- fragments.call(view_context:)
56
- else
57
- fragments.to_s
40
+ def patch_elements(elements, options = BLANK_OPTIONS)
41
+ elements = Array(elements).compact
42
+ rendered_elements = elements.map do |element|
43
+ render_element(element)
58
44
  end
59
45
 
60
- fragment_lines = fragments.to_s.split("\n")
46
+ element_lines = rendered_elements.flat_map do |el|
47
+ el.to_s.split("\n")
48
+ end
61
49
 
62
- buffer = +"event: datastar-merge-fragments\n"
50
+ buffer = +"event: datastar-patch-elements\n"
63
51
  build_options(options, buffer)
64
- fragment_lines.each { |line| buffer << "data: fragments #{line}\n" }
52
+ element_lines.each { |line| buffer << "data: #{Consts::ELEMENTS_DATALINE_LITERAL} #{line}\n" }
65
53
 
66
54
  write(buffer)
67
55
  end
68
56
 
69
- def remove_fragments(selector, options = BLANK_OPTIONS)
70
- buffer = +"event: datastar-remove-fragments\n"
71
- build_options(options, buffer)
72
- buffer << "data: selector #{selector}\n"
73
- write(buffer)
57
+ def remove_elements(selector, options = BLANK_OPTIONS)
58
+ patch_elements(
59
+ nil,
60
+ options.merge(
61
+ Consts::MODE_DATALINE_LITERAL => Consts::ElementPatchMode::REMOVE,
62
+ selector:
63
+ )
64
+ )
74
65
  end
75
66
 
76
- def merge_signals(signals, options = BLANK_OPTIONS)
77
- signals = JSON.dump(signals) unless signals.is_a?(String)
78
-
79
- buffer = +"event: datastar-merge-signals\n"
67
+ def patch_signals(signals, options = BLANK_OPTIONS)
68
+ buffer = +"event: datastar-patch-signals\n"
80
69
  build_options(options, buffer)
81
- buffer << "data: signals #{signals}\n"
70
+ case signals
71
+ when Hash
72
+ signals = JSON.dump(signals)
73
+ buffer << "data: signals #{signals}\n"
74
+ when String
75
+ multi_data_lines(signals, buffer, Consts::SIGNALS_DATALINE_LITERAL)
76
+ end
82
77
  write(buffer)
83
78
  end
84
79
 
85
80
  def remove_signals(paths, options = BLANK_OPTIONS)
86
81
  paths = [paths].flatten
82
+ signals = paths.each.with_object({}) do |path, acc|
83
+ parts = path.split(SIGNAL_SEPARATOR)
84
+ set_nested_value(acc, parts, nil)
85
+ end
87
86
 
88
- buffer = +"event: datastar-remove-signals\n"
89
- build_options(options, buffer)
90
- paths.each { |path| buffer << "data: paths #{path}\n" }
91
- write(buffer)
87
+ patch_signals(signals, options)
92
88
  end
93
89
 
94
90
  def execute_script(script, options = BLANK_OPTIONS)
95
- buffer = +"event: datastar-execute-script\n"
96
- build_options(options, buffer)
97
- scripts = script.to_s.split("\n")
98
- scripts.each do |sc|
99
- buffer << "data: script #{sc}\n"
91
+ options = camelize_keys(options)
92
+ auto_remove = options.key?('autoRemove') ? options.delete('autoRemove') : true
93
+ attributes = options.delete('attributes') || BLANK_OPTIONS
94
+ script_tag = +"<script"
95
+ attributes.each do |k, v|
96
+ script_tag << %( #{camelize(k)}="#{v}")
100
97
  end
101
- write(buffer)
98
+ script_tag << %( data-effect="el.remove()") if auto_remove
99
+ script_tag << ">#{script}</script>"
100
+
101
+ options[Consts::SELECTOR_DATALINE_LITERAL] = 'body'
102
+ options[Consts::MODE_DATALINE_LITERAL] = Consts::ElementPatchMode::APPEND
103
+
104
+ patch_elements(script_tag, options)
102
105
  end
103
106
 
104
107
  def redirect(url)
@@ -114,6 +117,18 @@ module Datastar
114
117
 
115
118
  attr_reader :view_context, :stream
116
119
 
120
+ # Support Phlex components
121
+ # And Rails' #render_in interface
122
+ def render_element(element)
123
+ if element.respond_to?(:render_in)
124
+ element.render_in(view_context)
125
+ elsif element.respond_to?(:call)
126
+ element.call(view_context:)
127
+ else
128
+ element
129
+ end
130
+ end
131
+
117
132
  def build_options(options, buffer)
118
133
  options.each do |k, v|
119
134
  k = camelize(k)
@@ -122,8 +137,13 @@ module Datastar
122
137
  buffer << "#{sse_key}: #{v}\n" unless v == default_value
123
138
  elsif v.is_a?(Hash)
124
139
  v.each do |kk, vv|
125
- default_value = ATTRIBUTE_DEFAULTS[kk.to_s]
126
- buffer << "data: #{k} #{kk} #{vv}\n" unless vv == default_value
140
+ buffer << "data: #{k} #{kk} #{vv}\n"
141
+ end
142
+ elsif v.is_a?(Array)
143
+ if k == Consts::SELECTOR_DATALINE_LITERAL
144
+ buffer << "data: #{k} #{v.join(', ')}\n"
145
+ else
146
+ buffer << "data: #{k} #{v.join(' ')}\n"
127
147
  end
128
148
  else
129
149
  default_value = OPTION_DEFAULTS[k]
@@ -132,8 +152,34 @@ module Datastar
132
152
  end
133
153
  end
134
154
 
155
+ def camelize_keys(options)
156
+ options.each.with_object({}) do |(key, value), acc|
157
+ value = camelize_keys(value) if value.is_a?(Hash)
158
+ acc[camelize(key)] = value
159
+ end
160
+ end
161
+
135
162
  def camelize(str)
136
163
  str.to_s.split('_').map.with_index { |word, i| i == 0 ? word : word.capitalize }.join
137
164
  end
165
+
166
+ # Take a string, split it by newlines,
167
+ # and write each line as a separate data line
168
+ def multi_data_lines(data, buffer, key)
169
+ lines = data.to_s.split("\n")
170
+ lines.each do |line|
171
+ buffer << "data: #{key} #{line}\n"
172
+ end
173
+ end
174
+
175
+ def set_nested_value(hash, path, value)
176
+ # Navigate to the parent hash using all but the last segment
177
+ parent = path[0...-1].reduce(hash) do |current_hash, key|
178
+ current_hash[key] ||= {}
179
+ end
180
+
181
+ # Set the final key to the value
182
+ parent[path.last] = value
183
+ end
138
184
  end
139
185
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Datastar
4
- VERSION = '1.0.0.beta.2'
4
+ VERSION = '1.0.0.pre.1'
5
5
  end
metadata CHANGED
@@ -1,28 +1,56 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: datastar
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.beta.2
4
+ version: 1.0.0.pre.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ismael Celis
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-02-12 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rack
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
16
+ - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '3.0'
18
+ version: 3.1.14
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
- - - "~>"
23
+ - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '3.0'
25
+ version: 3.1.14
26
+ - !ruby/object:Gem::Dependency
27
+ name: json
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: logger
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
26
54
  email:
27
55
  - ismaelct@gmail.com
28
56
  executables: []
@@ -64,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
64
92
  - !ruby/object:Gem::Version
65
93
  version: '0'
66
94
  requirements: []
67
- rubygems_version: 3.6.3
95
+ rubygems_version: 3.6.9
68
96
  specification_version: 4
69
97
  summary: Ruby SDK for Datastar. Rack-compatible.
70
98
  test_files: []