datastar 1.0.0.beta.3 → 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: 55282db68817596fd27726daa87179e08fcddb3b8deaea5826161d54f7a850cb
4
- data.tar.gz: '0864d609f3afa0c950d357f396973b7882c6f834f56c112e4765e3dfc7add400'
3
+ metadata.gz: cc457ac67196befbcb6a6c1510b970e81e728261dff0987d42d7760e358ad7fe
4
+ data.tar.gz: 5dcf48dc09ee9adb4c08f53d2ef63df316a34929ca6d3bcf0d15cf0d27083540
5
5
  SHA512:
6
- metadata.gz: 1712f02ea8b4d5def9d04381062f35135eb3213863b63536a35d8322b3880efb917acfb5dde900be7181ea2576b825791b301bd392a649bd799784aa93fbf28d
7
- data.tar.gz: 4e268347627b41ec8c7506891adbf7c95f32d10d16c4ed3f5e33192f624660456d9a1712dee71921af18390c7998f1755a48a2cc73701a298036fce8ab1a48de
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`
@@ -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
@@ -4,63 +4,54 @@
4
4
  module Datastar
5
5
  module Consts
6
6
  DATASTAR_KEY = 'datastar'
7
- VERSION = '1.0.0-beta.11'
7
+ VERSION = '1.0.0-RC.13'
8
8
 
9
9
  # The default duration for retrying SSE on connection reset. This is part of the underlying retry mechanism of SSE.
10
10
  DEFAULT_SSE_RETRY_DURATION = 1000
11
11
 
12
- # Should fragments be merged using the ViewTransition API?
13
- DEFAULT_FRAGMENTS_USE_VIEW_TRANSITIONS = false
12
+ # Should elements be patched using the ViewTransition API?
13
+ DEFAULT_ELEMENTS_USE_VIEW_TRANSITIONS = false
14
14
 
15
- # Should a given set of signals merge if they are missing?
16
- DEFAULT_MERGE_SIGNALS_ONLY_IF_MISSING = false
15
+ # Should a given set of signals patch if they are missing?
16
+ DEFAULT_PATCH_SIGNALS_ONLY_IF_MISSING = false
17
17
 
18
- # Should script element remove itself after execution?
19
- DEFAULT_EXECUTE_SCRIPT_AUTO_REMOVE = true
18
+ module ElementPatchMode
20
19
 
21
- # The default attributes for <script/> element use when executing scripts. It is a set of key-value pairs delimited by a newline \\n character.}
22
- DEFAULT_EXECUTE_SCRIPT_ATTRIBUTES = 'type module'
23
-
24
- module FragmentMergeMode
25
-
26
- # Morphs the fragment into the existing element using idiomorph.
27
- MORPH = 'morph'
20
+ # Morphs the element into the existing element.
21
+ OUTER = 'outer';
28
22
 
29
23
  # Replaces the inner HTML of the existing element.
30
- INNER = 'inner'
24
+ INNER = 'inner';
31
25
 
32
- # Replaces the outer HTML of the existing element.
33
- OUTER = 'outer'
26
+ # Removes the existing element.
27
+ REMOVE = 'remove';
34
28
 
35
- # Prepends the fragment to the existing element.
36
- PREPEND = 'prepend'
29
+ # Replaces the existing element with the new element.
30
+ REPLACE = 'replace';
37
31
 
38
- # Appends the fragment to the existing element.
39
- APPEND = 'append'
32
+ # Prepends the element inside to the existing element.
33
+ PREPEND = 'prepend';
40
34
 
41
- # Inserts the fragment before the existing element.
42
- BEFORE = 'before'
35
+ # Appends the element inside the existing element.
36
+ APPEND = 'append';
43
37
 
44
- # Inserts the fragment after the existing element.
45
- AFTER = 'after'
38
+ # Inserts the element before the existing element.
39
+ BEFORE = 'before';
46
40
 
47
- # Upserts the attributes of the existing element.
48
- UPSERT_ATTRIBUTES = 'upsertAttributes'
41
+ # Inserts the element after the existing element.
42
+ AFTER = 'after';
49
43
  end
50
44
 
51
- # The mode in which a fragment is merged into the DOM.
52
- 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
53
48
 
54
49
  # Dataline literals.
55
50
  SELECTOR_DATALINE_LITERAL = 'selector'
56
- MERGE_MODE_DATALINE_LITERAL = 'mergeMode'
57
- FRAGMENTS_DATALINE_LITERAL = 'fragments'
51
+ MODE_DATALINE_LITERAL = 'mode'
52
+ ELEMENTS_DATALINE_LITERAL = 'elements'
58
53
  USE_VIEW_TRANSITION_DATALINE_LITERAL = 'useViewTransition'
59
54
  SIGNALS_DATALINE_LITERAL = 'signals'
60
55
  ONLY_IF_MISSING_DATALINE_LITERAL = 'onlyIfMissing'
61
- PATHS_DATALINE_LITERAL = 'paths'
62
- SCRIPT_DATALINE_LITERAL = 'script'
63
- ATTRIBUTES_DATALINE_LITERAL = 'attributes'
64
- AUTO_REMOVE_DATALINE_LITERAL = 'autoRemove'
65
56
  end
66
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
  #
@@ -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,20 +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::USE_VIEW_TRANSITION_DATALINE_LITERAL => Consts::DEFAULT_FRAGMENTS_USE_VIEW_TRANSITIONS,
21
- 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,
22
21
  }.freeze
23
22
 
24
- # ATTRIBUTE_DEFAULTS = {
25
- # 'type' => 'module'
26
- # }.freeze
27
- ATTRIBUTE_DEFAULTS = Consts::DEFAULT_EXECUTE_SCRIPT_ATTRIBUTES
28
- .split("\n")
29
- .map { |attr| attr.split(' ') }
30
- .to_h
31
- .freeze
23
+ SIGNAL_SEPARATOR = '.'
32
24
 
33
25
  attr_reader :signals
34
26
 
@@ -45,59 +37,71 @@ module Datastar
45
37
  @stream << MSG_END
46
38
  end
47
39
 
48
- def merge_fragments(fragments, options = BLANK_OPTIONS)
49
- # Support Phlex components
50
- # And Rails' #render_in interface
51
- fragments = if fragments.respond_to?(:render_in)
52
- fragments.render_in(view_context)
53
- elsif fragments.respond_to?(:call)
54
- fragments.call(view_context:)
55
- else
56
- 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)
57
44
  end
58
45
 
59
- fragment_lines = fragments.to_s.split("\n")
46
+ element_lines = rendered_elements.flat_map do |el|
47
+ el.to_s.split("\n")
48
+ end
60
49
 
61
- buffer = +"event: datastar-merge-fragments\n"
50
+ buffer = +"event: datastar-patch-elements\n"
62
51
  build_options(options, buffer)
63
- fragment_lines.each { |line| buffer << "data: fragments #{line}\n" }
52
+ element_lines.each { |line| buffer << "data: #{Consts::ELEMENTS_DATALINE_LITERAL} #{line}\n" }
64
53
 
65
54
  write(buffer)
66
55
  end
67
56
 
68
- def remove_fragments(selector, options = BLANK_OPTIONS)
69
- buffer = +"event: datastar-remove-fragments\n"
70
- build_options(options, buffer)
71
- buffer << "data: selector #{selector}\n"
72
- 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
+ )
73
65
  end
74
66
 
75
- def merge_signals(signals, options = BLANK_OPTIONS)
76
- signals = JSON.dump(signals) unless signals.is_a?(String)
77
-
78
- buffer = +"event: datastar-merge-signals\n"
67
+ def patch_signals(signals, options = BLANK_OPTIONS)
68
+ buffer = +"event: datastar-patch-signals\n"
79
69
  build_options(options, buffer)
80
- 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
81
77
  write(buffer)
82
78
  end
83
79
 
84
80
  def remove_signals(paths, options = BLANK_OPTIONS)
85
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
86
86
 
87
- buffer = +"event: datastar-remove-signals\n"
88
- build_options(options, buffer)
89
- paths.each { |path| buffer << "data: paths #{path}\n" }
90
- write(buffer)
87
+ patch_signals(signals, options)
91
88
  end
92
89
 
93
90
  def execute_script(script, options = BLANK_OPTIONS)
94
- buffer = +"event: datastar-execute-script\n"
95
- build_options(options, buffer)
96
- scripts = script.to_s.split("\n")
97
- scripts.each do |sc|
98
- 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}")
99
97
  end
100
- 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)
101
105
  end
102
106
 
103
107
  def redirect(url)
@@ -113,6 +117,18 @@ module Datastar
113
117
 
114
118
  attr_reader :view_context, :stream
115
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
+
116
132
  def build_options(options, buffer)
117
133
  options.each do |k, v|
118
134
  k = camelize(k)
@@ -121,8 +137,13 @@ module Datastar
121
137
  buffer << "#{sse_key}: #{v}\n" unless v == default_value
122
138
  elsif v.is_a?(Hash)
123
139
  v.each do |kk, vv|
124
- default_value = ATTRIBUTE_DEFAULTS[kk.to_s]
125
- 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"
126
147
  end
127
148
  else
128
149
  default_value = OPTION_DEFAULTS[k]
@@ -131,8 +152,34 @@ module Datastar
131
152
  end
132
153
  end
133
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
+
134
162
  def camelize(str)
135
163
  str.to_s.split('_').map.with_index { |word, i| i == 0 ? word : word.capitalize }.join
136
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
137
184
  end
138
185
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Datastar
4
- VERSION = '1.0.0.beta.3'
4
+ VERSION = '1.0.0.pre.1'
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: datastar
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.beta.3
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-06-02 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
@@ -23,6 +23,34 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
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: []