turbo_live 0.1.1 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +208 -16
- data/app/channels/components_channel.rb +2 -2
- data/app/controllers/turbo_live/components_controller.rb +1 -1
- data/examples/countdown_component.rb +23 -0
- data/examples/counter_component.rb +26 -0
- data/examples/showcase_component.rb +41 -0
- data/examples/tic_tac_toe_component.rb +93 -0
- data/lib/turbo_live/component.rb +18 -11
- data/lib/turbo_live/renderer.rb +6 -3
- data/lib/turbo_live/version.rb +1 -1
- data/lib/turbo_live.rb +4 -0
- data/package-lock.json +2 -2
- data/package.json +2 -2
- data/src/js/controllers/turbo_live_controller.js +87 -67
- metadata +9 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5c9a249ff75e9eed862b6d2ab3402d5d867368b4d43e5e1f6679ac9bcabfb3eb
|
4
|
+
data.tar.gz: 9d77ef656d61442697be5fb9f060145193636f439e971dd62af36b019354dc22
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6efe19e261f10de079be70b72404fc441d02e34cb795a8a6a632a3390d5afaeb603c2bf43754ade0fafda03013cf947d9bc284fd0b2945e25306e1a73cb66890
|
7
|
+
data.tar.gz: 4abda82e8d71fbcc3275cfec205d0ea4197c814710b7f12e5cae69a5e359455996e6eb207b0035be6e6ad8156aafc6797c854f33d4f25a93e22f9222e8913011
|
data/README.md
CHANGED
@@ -1,39 +1,231 @@
|
|
1
1
|
# TurboLive
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
TurboLive is a Ruby gem that enables the creation of async, progressively enhanced, live components for Ruby applications. It works seamlessly over both WebSockets and HTTPS, providing real-time interactivity with graceful degradation.
|
4
|
+
|
5
|
+
## Table of Contents
|
6
|
+
|
7
|
+
- [Installation](#installation)
|
8
|
+
- [Setup](#setup)
|
9
|
+
- [Usage](#usage)
|
10
|
+
- [Creating a Component](#creating-a-component)
|
11
|
+
- [Model State](#model-state)
|
12
|
+
- [View](#view)
|
13
|
+
- [Update](#update)
|
14
|
+
- [Events](#events)
|
15
|
+
- [Manual Events](#manual-events)
|
16
|
+
- [Timed Events](#timed-events)
|
17
|
+
- [Examples](#examples)
|
18
|
+
- [Performance Considerations](#performance-considerations)
|
19
|
+
- [Testing](#testing)
|
20
|
+
- [Troubleshooting](#troubleshooting)
|
21
|
+
- [Contributing](#contributing)
|
22
|
+
- [Changelog](#changelog)
|
23
|
+
- [License](#license)
|
6
24
|
|
7
25
|
## Installation
|
8
26
|
|
9
|
-
|
27
|
+
Add it to your project with:
|
28
|
+
|
29
|
+
```console
|
30
|
+
bundle add 'turbo_live'
|
31
|
+
```
|
10
32
|
|
11
|
-
|
33
|
+
Or install it yourself using:
|
12
34
|
|
13
|
-
```
|
14
|
-
|
35
|
+
```console
|
36
|
+
gem install turbo_live
|
15
37
|
```
|
16
38
|
|
17
|
-
|
39
|
+
### JavaScript
|
40
|
+
|
41
|
+
TurboLive ships a JavaScript component that comes as an npm package. You can pin it with importmaps or install it as an npm package depending on your asset pipeline:
|
42
|
+
|
43
|
+
For importmaps:
|
18
44
|
|
19
|
-
```
|
20
|
-
|
45
|
+
```console
|
46
|
+
bin/importmap pin @radioactive-labs/turbo-live
|
47
|
+
```
|
48
|
+
|
49
|
+
For npm:
|
50
|
+
|
51
|
+
```console
|
52
|
+
npm install @radioactive-labs/turbo-live
|
53
|
+
```
|
54
|
+
|
55
|
+
## Setup
|
56
|
+
|
57
|
+
### Stimulus Controller
|
58
|
+
|
59
|
+
TurboLive uses a Stimulus controller to manage interactions. In your `app/javascript/controllers/index.js`:
|
60
|
+
|
61
|
+
```diff
|
62
|
+
import { application } from "controllers/application"
|
63
|
+
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
|
64
|
+
+import * as turboLive from "@radioactive-labs/turbo-live"
|
65
|
+
|
66
|
+
eagerLoadControllersFrom("controllers", application)
|
67
|
+
+turboLive.registerControllers(application)
|
68
|
+
```
|
69
|
+
|
70
|
+
### ActionCable (Optional)
|
71
|
+
|
72
|
+
TurboLive supports WebSockets using ActionCable with automatic failover to HTTPS. If you have ActionCable set up and would like to benefit from better performance, you can set up the integration.
|
73
|
+
|
74
|
+
In `app/javascript/channels/index.js`:
|
75
|
+
|
76
|
+
```diff
|
77
|
+
+import consumer from "./consumer"
|
78
|
+
+import * as turboLive from "@radioactive-labs/turbo-live"
|
79
|
+
+
|
80
|
+
+turboLive.registerChannels(consumer)
|
81
|
+
```
|
82
|
+
|
83
|
+
Then in your `app/javascript/application.js`:
|
84
|
+
|
85
|
+
```diff
|
86
|
+
import "@hotwired/turbo-rails"
|
87
|
+
import "controllers"
|
88
|
+
+import "channels"
|
21
89
|
```
|
22
90
|
|
23
91
|
## Usage
|
24
92
|
|
25
|
-
|
93
|
+
A TurboLive component is a self-contained, interactive unit of a web application that can update in real-time without full page reloads. Components follow [The Elm Architecture](https://guide.elm-lang.org/architecture/) pattern.
|
94
|
+
|
95
|
+
### Creating a Component
|
96
|
+
|
97
|
+
To create a TurboLive component, inherit from `TurboLive::Component`:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
class MyComponent < TurboLive::Component
|
101
|
+
# Component logic goes here
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
### Model State
|
106
|
+
|
107
|
+
Define state variables using the `state` method:
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
class MyComponent < TurboLive::Component
|
111
|
+
state :count, Integer do |value|
|
112
|
+
value || 0
|
113
|
+
end
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
> Note: State variables can only be primitive objects and basic collections.
|
118
|
+
|
119
|
+
### View
|
120
|
+
|
121
|
+
Define the component's HTML structure in the `view` method:
|
26
122
|
|
27
|
-
|
123
|
+
```ruby
|
124
|
+
def view
|
125
|
+
div do
|
126
|
+
button(**on(click: :increment)) { "+" }
|
127
|
+
span { count }
|
128
|
+
button(**on(click: :decrement)) { "-" }
|
129
|
+
end
|
130
|
+
end
|
131
|
+
```
|
132
|
+
|
133
|
+
Components are [phlex](https://www.phlex.fun/) views, allowing you to write HTML in Ruby.
|
134
|
+
|
135
|
+
### Update
|
28
136
|
|
29
|
-
|
137
|
+
Handle events in the `update` method:
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
def update(input)
|
141
|
+
case input
|
142
|
+
in [:increment]
|
143
|
+
self.count += 1
|
144
|
+
in [:decrement]
|
145
|
+
self.count -= 1
|
146
|
+
end
|
147
|
+
end
|
148
|
+
```
|
30
149
|
|
31
|
-
|
150
|
+
## Events
|
151
|
+
|
152
|
+
Events are transmitted to the server using the currently active transport (HTTP or WebSockets).
|
153
|
+
|
154
|
+
### Manual Events
|
155
|
+
|
156
|
+
Use the `on` method to set up manually triggered events:
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
button(**on(click: :decrement)) { "-" }
|
160
|
+
```
|
161
|
+
|
162
|
+
You can also emit compound events that carry extra data:
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
button(**on(click: [:change_value, 1])) { "+" }
|
166
|
+
```
|
167
|
+
|
168
|
+
> Note: Currently, only `:click` and `:change` events are supported.
|
169
|
+
|
170
|
+
### Timed Events
|
171
|
+
|
172
|
+
Use the `every` method to set up recurring events:
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
def view
|
176
|
+
div do
|
177
|
+
h1 { countdown }
|
178
|
+
every(1000, :tick) if countdown > 0
|
179
|
+
end
|
180
|
+
end
|
181
|
+
```
|
182
|
+
|
183
|
+
## Examples
|
184
|
+
|
185
|
+
See the [/examples](/examples) folder in for detailed component examples including Counter, Countdown, Showcase and Tic-Tac-Toe components.
|
186
|
+
|
187
|
+
## Performance Considerations
|
188
|
+
|
189
|
+
- Use fine-grained components to minimize the amount of data transferred and rendered.
|
190
|
+
- Implement debouncing for frequently triggered events.
|
191
|
+
- Consider using background jobs for heavy computations to keep the UI responsive.
|
192
|
+
|
193
|
+
## Testing
|
194
|
+
|
195
|
+
TurboLive components can be tested using standard Rails testing tools. Here's a basic example:
|
196
|
+
|
197
|
+
```ruby
|
198
|
+
require "test_helper"
|
199
|
+
|
200
|
+
class CounterComponentTest < ActiveSupport::TestCase
|
201
|
+
test "increments count" do
|
202
|
+
component = CounterComponent.new
|
203
|
+
assert_equal 0, component.count
|
204
|
+
component.update([:increment])
|
205
|
+
assert_equal 1, component.count
|
206
|
+
end
|
207
|
+
end
|
208
|
+
```
|
209
|
+
|
210
|
+
## Troubleshooting
|
211
|
+
|
212
|
+
Common issues and their solutions:
|
213
|
+
|
214
|
+
1. **Component not updating**: Ensure that your `update` method is correctly handling the event and modifying the state.
|
215
|
+
2. **WebSocket connection failing**: Check your ActionCable configuration and ensure that your server supports WebSocket connections.
|
216
|
+
3. **JavaScript errors**: Make sure you've correctly set up the TurboLive JavaScript integration in your application.
|
217
|
+
3. **My timed events won't go away**: Due to the use of morphing, there might be instances where your some meta attributes are not removed.
|
218
|
+
|
219
|
+
For more issues, please check our [FAQ](https://github.com/radioactive-labs/turbo_live/wiki/FAQ) or open an issue on GitHub.
|
32
220
|
|
33
221
|
## Contributing
|
34
222
|
|
35
|
-
|
223
|
+
We welcome contributions to TurboLive! Please see our [Contributing Guidelines](CONTRIBUTING.md) for more information on how to get started.
|
224
|
+
|
225
|
+
## Changelog
|
226
|
+
|
227
|
+
See the [CHANGELOG.md](CHANGELOG.md) file for details on each release.
|
36
228
|
|
37
229
|
## License
|
38
230
|
|
39
|
-
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
231
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -7,8 +7,8 @@ module TurboLive
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def receive(params)
|
10
|
-
stream = Renderer.render params
|
11
|
-
ActionCable.server.broadcast(stream_name, stream)
|
10
|
+
stream = Renderer.render params.symbolize_keys
|
11
|
+
ActionCable.server.broadcast(stream_name, stream) if stream
|
12
12
|
end
|
13
13
|
|
14
14
|
protected
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class CountdownComponent < TurboLive::Component
|
2
|
+
state :countdown, Integer
|
3
|
+
|
4
|
+
def view
|
5
|
+
div do
|
6
|
+
if countdown.nil?
|
7
|
+
button(**on(click: :start)) { "Start!" }
|
8
|
+
else
|
9
|
+
h1 { countdown }
|
10
|
+
every(1000, :countdown) if countdown >= 1
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def update(input)
|
16
|
+
case input
|
17
|
+
in [:countdown]
|
18
|
+
self.countdown -= 1
|
19
|
+
in [:start]
|
20
|
+
self.countdown = 1000
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class CounterComponent < TurboLive::Component
|
2
|
+
state :count, Integer do |value|
|
3
|
+
value || 0
|
4
|
+
end
|
5
|
+
|
6
|
+
def view
|
7
|
+
div do
|
8
|
+
button(**on(click: :increment)) { "+" }
|
9
|
+
span { " Clicked: #{count} " }
|
10
|
+
button(**on(click: :decrement)) { "-" }
|
11
|
+
plain " "
|
12
|
+
button(**on(click: :reset)) { "Reset" }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def update(input)
|
17
|
+
case input
|
18
|
+
in [:decrement]
|
19
|
+
self.count -= 1
|
20
|
+
in [:increment]
|
21
|
+
self.count += 1
|
22
|
+
in [:reset]
|
23
|
+
self.count = 0
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class ShowcaseComponent < TurboLive::Component
|
2
|
+
state :component, Symbol
|
3
|
+
|
4
|
+
def view
|
5
|
+
div class: "container" do
|
6
|
+
div class: "left-column" do
|
7
|
+
h2 { "Components" }
|
8
|
+
ul do
|
9
|
+
li { button(**on(click: [:change_component, :counter])) { "Counter" } }
|
10
|
+
li { button(**on(click: [:change_component, :countdown])) { "Countdown" } }
|
11
|
+
li { button(**on(click: [:change_component, :tic_tac_toe])) { "TicTacToe" } }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
div class: "right-column" do
|
15
|
+
div class: "card" do
|
16
|
+
render selected_component.new
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def update(input)
|
23
|
+
case input
|
24
|
+
in [[:change_component, component]]
|
25
|
+
self.component = component
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def selected_component
|
32
|
+
case component
|
33
|
+
when :countdown
|
34
|
+
CountdownComponent
|
35
|
+
when :tic_tac_toe
|
36
|
+
TicTacToeComponent
|
37
|
+
else
|
38
|
+
CounterComponent
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
class TicTacToeComponent < TurboLive::Component
|
2
|
+
state :board, Array do |value|
|
3
|
+
value || Array.new(9, nil)
|
4
|
+
end
|
5
|
+
|
6
|
+
state :current_player, String do |value|
|
7
|
+
value || "X"
|
8
|
+
end
|
9
|
+
|
10
|
+
state :winner, String do |value|
|
11
|
+
value || nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def view
|
15
|
+
div(class: "tic-tac-toe-container") do
|
16
|
+
render_board
|
17
|
+
render_status
|
18
|
+
render_reset_button
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def render_board
|
23
|
+
div(class: "board") do
|
24
|
+
board.each_with_index do |cell, index|
|
25
|
+
button(
|
26
|
+
class: "cell",
|
27
|
+
**on(click: [:make_move, index]),
|
28
|
+
disabled: cell || winner,
|
29
|
+
"data-value": cell
|
30
|
+
) { cell || " ".html_safe }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def render_status
|
36
|
+
div(class: "status") do
|
37
|
+
if winner
|
38
|
+
"Winner: #{winner}"
|
39
|
+
elsif board.compact.length == 9
|
40
|
+
"It's a draw!"
|
41
|
+
else
|
42
|
+
"Current player: #{current_player}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def render_reset_button
|
48
|
+
button(**on(click: :reset_game)) { "Reset Game" }
|
49
|
+
end
|
50
|
+
|
51
|
+
def update(input)
|
52
|
+
case input
|
53
|
+
in [[:make_move, index]]
|
54
|
+
make_move(index)
|
55
|
+
in [:reset_game]
|
56
|
+
reset_game
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def make_move(index)
|
63
|
+
return if board[index] || winner
|
64
|
+
|
65
|
+
new_board = board.dup
|
66
|
+
new_board[index] = current_player
|
67
|
+
self.board = new_board
|
68
|
+
self.winner = check_winner
|
69
|
+
self.current_player = (current_player == "X") ? "O" : "X" unless winner
|
70
|
+
end
|
71
|
+
|
72
|
+
def check_winner
|
73
|
+
winning_combinations = [
|
74
|
+
[0, 1, 2], [3, 4, 5], [6, 7, 8], # Rows
|
75
|
+
[0, 3, 6], [1, 4, 7], [2, 5, 8], # Columns
|
76
|
+
[0, 4, 8], [2, 4, 6] # Diagonals
|
77
|
+
]
|
78
|
+
|
79
|
+
winning_combinations.each do |combo|
|
80
|
+
if board[combo[0]] && board[combo[0]] == board[combo[1]] && board[combo[0]] == board[combo[2]]
|
81
|
+
return board[combo[0]]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
nil
|
86
|
+
end
|
87
|
+
|
88
|
+
def reset_game
|
89
|
+
self.board = Array.new(9, nil)
|
90
|
+
self.current_player = "X"
|
91
|
+
self.winner = nil
|
92
|
+
end
|
93
|
+
end
|
data/lib/turbo_live/component.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "phlex"
|
4
|
+
require "literal"
|
4
5
|
|
5
6
|
module TurboLive
|
6
7
|
class Component < Phlex::HTML
|
7
8
|
extend Literal::Properties
|
8
9
|
|
9
|
-
SUPPORTED_EVENTS = %i[click change].freeze
|
10
|
+
SUPPORTED_EVENTS = %i[click change input].freeze
|
10
11
|
|
11
12
|
def self.state(name, type, **options, &block)
|
12
13
|
options = {reader: :public, writer: :protected}.merge(**options).compact
|
@@ -52,27 +53,33 @@ module TurboLive
|
|
52
53
|
end
|
53
54
|
|
54
55
|
def every(milliseconds, event)
|
55
|
-
data = {milliseconds
|
56
|
-
|
56
|
+
data = {interval: milliseconds, event: to_verifiable(event)}.to_json
|
57
|
+
add_meta :interval, data
|
58
|
+
end
|
59
|
+
|
60
|
+
def norender
|
61
|
+
SKIP_RENDER
|
62
|
+
end
|
63
|
+
|
64
|
+
def norender!
|
65
|
+
raise SkipRender
|
57
66
|
end
|
58
67
|
|
59
68
|
private
|
60
69
|
|
61
|
-
def
|
62
|
-
#
|
63
|
-
# Switch to HTML templates
|
70
|
+
def add_meta(type, value)
|
71
|
+
# TODO: turbo morph does some wonky things issues here since it doesn't force a replacement everytime
|
64
72
|
div(
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
data_turbo_live_data_value: value,
|
73
|
+
data_turbo_live_meta_type: type,
|
74
|
+
data_turbo_live_meta_value: value,
|
75
|
+
data_turbo_live_target: "meta",
|
69
76
|
style: "display: none;", display: :none
|
70
77
|
) {}
|
71
78
|
end
|
72
79
|
|
73
80
|
def serialize
|
74
81
|
state = self.class.literal_properties.map do |prop|
|
75
|
-
[prop.name,
|
82
|
+
[prop.name, public_send(prop.name)]
|
76
83
|
end.to_h
|
77
84
|
|
78
85
|
{klass: self.class.to_s, state: state}
|
data/lib/turbo_live/renderer.rb
CHANGED
@@ -4,21 +4,24 @@ module TurboLive
|
|
4
4
|
class Renderer
|
5
5
|
class << self
|
6
6
|
def render(data)
|
7
|
-
data = data.symbolize_keys
|
8
7
|
# build the payload
|
9
8
|
payload = extract_payload(data)
|
10
9
|
# create the component
|
11
10
|
component = build_component(data)
|
12
11
|
# run the update function
|
13
|
-
component.update payload
|
12
|
+
result = component.update payload
|
13
|
+
return if result == TurboLive::SKIP_RENDER
|
14
|
+
|
14
15
|
# render the replace stream
|
15
16
|
<<~STREAM
|
16
|
-
<turbo-stream action="replace" target="#{data[:id]}">
|
17
|
+
<turbo-stream action="replace" method="morph" target="#{data[:id]}">
|
17
18
|
<template>
|
18
19
|
#{component.call}
|
19
20
|
</template>
|
20
21
|
</turbo-stream>
|
21
22
|
STREAM
|
23
|
+
rescue SkipRender
|
24
|
+
nil
|
22
25
|
end
|
23
26
|
|
24
27
|
private
|
data/lib/turbo_live/version.rb
CHANGED
data/lib/turbo_live.rb
CHANGED
@@ -10,6 +10,10 @@ require_relative "../app/channels/components_channel" if defined?(ActionCable)
|
|
10
10
|
module TurboLive
|
11
11
|
class Error < StandardError; end
|
12
12
|
|
13
|
+
class SkipRender < StandardError; end
|
14
|
+
|
15
|
+
SKIP_RENDER = :skip_render
|
16
|
+
|
13
17
|
class << self
|
14
18
|
attr_writer :verifier_key
|
15
19
|
|
data/package-lock.json
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
{
|
2
2
|
"name": "@radioactive-labs/turbo-live",
|
3
|
-
"version": "0.1.
|
3
|
+
"version": "0.1.3",
|
4
4
|
"lockfileVersion": 3,
|
5
5
|
"requires": true,
|
6
6
|
"packages": {
|
7
7
|
"": {
|
8
8
|
"name": "@radioactive-labs/turbo-live",
|
9
|
-
"version": "0.1.
|
9
|
+
"version": "0.1.3",
|
10
10
|
"license": "MIT",
|
11
11
|
"dependencies": {
|
12
12
|
"@hotwired/stimulus": "^3.2.2",
|
data/package.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
{
|
2
2
|
"name": "@radioactive-labs/turbo-live",
|
3
|
-
"version": "0.1.
|
4
|
-
"description": "
|
3
|
+
"version": "0.1.3",
|
4
|
+
"description": "Async, progressively enhanced, live components for Ruby applications that work over Websockets and HTTP.",
|
5
5
|
"type": "module",
|
6
6
|
"main": "src/js/core.js",
|
7
7
|
"files": [
|
@@ -5,112 +5,132 @@ export default class extends Controller {
|
|
5
5
|
id: String,
|
6
6
|
component: String,
|
7
7
|
}
|
8
|
+
static targets = ["meta"]
|
8
9
|
|
9
10
|
get component() {
|
10
11
|
return this.componentValue
|
11
12
|
}
|
12
13
|
|
14
|
+
initialize() {
|
15
|
+
this.metaTargetsMap = new WeakMap()
|
16
|
+
this.metaTargetsCount = 0
|
17
|
+
this.intervals = {}
|
18
|
+
}
|
19
|
+
|
13
20
|
connect() {
|
14
|
-
console.log("TurboLiveController connected
|
21
|
+
console.log("TurboLiveController connected", this.element.id, this.component)
|
22
|
+
}
|
15
23
|
|
16
|
-
|
24
|
+
metaTargetConnected(target) {
|
25
|
+
console.log("TurboLiveController metaTargetConnected", this.element.id, this.#metaTargetId(target))
|
26
|
+
this.#readMetadata(target)
|
27
|
+
}
|
17
28
|
|
18
|
-
|
29
|
+
metaTargetDisconnected(target) {
|
30
|
+
console.log("TurboLiveController metaTargetDisconnected", this.element.id, this.#metaTargetId(target))
|
31
|
+
this.#teardownInterval(this.#metaTargetId(target))
|
19
32
|
}
|
20
33
|
|
21
34
|
disconnect() {
|
22
|
-
console.log("TurboLiveController disconnected")
|
23
|
-
this.#
|
35
|
+
console.log("TurboLiveController disconnected", this.element.id)
|
36
|
+
this.#cleanup()
|
24
37
|
}
|
25
38
|
|
26
39
|
dispatch(event, payload) {
|
27
|
-
console.log("TurboLiveController dispatch
|
28
|
-
|
40
|
+
console.log("TurboLiveController dispatch", this.element.id, event, payload)
|
41
|
+
const data = { id: this.element.id, event, payload, component: this.component }
|
42
|
+
|
29
43
|
if (window.turboLive) {
|
30
|
-
console.log("TurboLiveController dispatching via websockets")
|
44
|
+
console.log("TurboLiveController dispatching via websockets", this.element.id)
|
31
45
|
window.turboLive.send(data)
|
32
|
-
}
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
fetch('/turbo_live', {
|
37
|
-
method: 'POST',
|
38
|
-
headers: {
|
39
|
-
'Content-Type': 'application/json',
|
40
|
-
},
|
41
|
-
body: JSON.stringify(data)
|
42
|
-
})
|
43
|
-
.then(response => {
|
44
|
-
if (!response.ok) {
|
45
|
-
throw new Error(`Network response was not OK`);
|
46
|
-
}
|
47
|
-
return response.text();
|
48
|
-
})
|
49
|
-
.then(turbo_stream => {
|
50
|
-
console.log('TurboLiveController dispatch success:', turbo_stream);
|
51
|
-
Turbo.renderStreamMessage(turbo_stream);
|
52
|
-
})
|
53
|
-
.catch((error) => {
|
54
|
-
console.error('TurboLiveController dispatch error:', error);
|
55
|
-
});
|
46
|
+
} else {
|
47
|
+
console.log("TurboLiveController dispatching via HTTP", this.element.id)
|
48
|
+
this.#dispatchHTTP(data)
|
56
49
|
}
|
57
50
|
}
|
58
51
|
|
59
52
|
onClick(event) {
|
60
|
-
|
61
|
-
console.log("TurboLiveController onClick")
|
53
|
+
console.log("TurboLiveController onClick", this.element.id)
|
62
54
|
this.#dispatchSimpleEvent("click", event)
|
63
55
|
}
|
64
56
|
|
65
57
|
onChange(event) {
|
66
|
-
|
67
|
-
console.log("TurboLiveController onChange")
|
58
|
+
console.log("TurboLiveController onChange", this.element.id)
|
68
59
|
this.#dispatchValueEvent("change", event)
|
69
60
|
}
|
70
61
|
|
71
|
-
|
72
|
-
|
73
|
-
|
62
|
+
onInput(event) {
|
63
|
+
console.log("TurboLiveController onInput", this.element.id)
|
64
|
+
this.#dispatchValueEvent("input", event)
|
65
|
+
}
|
74
66
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
break;
|
81
|
-
}
|
82
|
-
})
|
67
|
+
#metaTargetId(target) {
|
68
|
+
if (!this.metaTargetsMap.has(target)) {
|
69
|
+
this.metaTargetsMap.set(target, ++this.metaTargetsCount)
|
70
|
+
}
|
71
|
+
return this.metaTargetsMap.get(target)
|
83
72
|
}
|
84
73
|
|
85
|
-
#
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
setInterval(() => {
|
90
|
-
this.dispatch("every", [intervalConfig[interval]])
|
91
|
-
}, interval)
|
92
|
-
)
|
93
|
-
}
|
74
|
+
#readMetadata(element) {
|
75
|
+
const type = element.dataset.turboLiveMetaType
|
76
|
+
if (type === "interval") {
|
77
|
+
this.#setupInterval(element)
|
94
78
|
}
|
95
|
-
|
79
|
+
}
|
80
|
+
|
81
|
+
#setupInterval(element) {
|
82
|
+
try {
|
83
|
+
const config = JSON.parse(element.dataset.turboLiveMetaValue)
|
84
|
+
this.intervals[this.#metaTargetId(element)] = setInterval(() => {
|
85
|
+
this.dispatch("interval", [config.event])
|
86
|
+
}, config.interval)
|
87
|
+
} catch (e) {
|
96
88
|
console.error(e)
|
97
89
|
}
|
98
90
|
}
|
99
91
|
|
100
|
-
#
|
101
|
-
this.intervals
|
102
|
-
clearInterval(
|
103
|
-
|
92
|
+
#teardownInterval(id) {
|
93
|
+
if (this.intervals[id]) {
|
94
|
+
clearInterval(this.intervals[id])
|
95
|
+
delete this.intervals[id]
|
96
|
+
}
|
97
|
+
}
|
98
|
+
|
99
|
+
#cleanup() {
|
100
|
+
Object.keys(this.intervals).forEach(this.#teardownInterval.bind(this))
|
104
101
|
}
|
105
102
|
|
106
103
|
#dispatchSimpleEvent(name, { params }) {
|
107
|
-
|
108
|
-
this.dispatch(name, [
|
104
|
+
const liveEvent = params[name]
|
105
|
+
this.dispatch(name, [liveEvent])
|
109
106
|
}
|
110
107
|
|
111
108
|
#dispatchValueEvent(name, { params, target }) {
|
112
|
-
|
113
|
-
|
114
|
-
this.dispatch(name, [
|
109
|
+
const value = target.value
|
110
|
+
const liveEvent = params[name]
|
111
|
+
this.dispatch(name, [liveEvent, value])
|
112
|
+
}
|
113
|
+
|
114
|
+
#dispatchHTTP(data) {
|
115
|
+
fetch('/turbo_live', {
|
116
|
+
method: 'POST',
|
117
|
+
headers: {
|
118
|
+
'Content-Type': 'application/json',
|
119
|
+
},
|
120
|
+
body: JSON.stringify(data)
|
121
|
+
})
|
122
|
+
.then(response => {
|
123
|
+
if (!response.ok) {
|
124
|
+
throw new Error(`Network response was not OK`)
|
125
|
+
}
|
126
|
+
return response.text()
|
127
|
+
})
|
128
|
+
.then(turboStream => {
|
129
|
+
console.log('TurboLiveController dispatch success', this.element.id, turboStream)
|
130
|
+
if (turboStream) Turbo.renderStreamMessage(turboStream)
|
131
|
+
})
|
132
|
+
.catch((error) => {
|
133
|
+
console.error('TurboLiveController dispatch error', this.element.id, error)
|
134
|
+
})
|
115
135
|
}
|
116
|
-
}
|
136
|
+
}
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: turbo_live
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- TheDumbTechGuy
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-10-
|
11
|
+
date: 2024-10-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: phlex-rails
|
@@ -38,8 +38,8 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
|
-
description:
|
42
|
-
and HTTP.
|
41
|
+
description: Async, progressively enhanced, live components for Ruby applications
|
42
|
+
that work over Websockets and HTTP.
|
43
43
|
email:
|
44
44
|
- sfroelich01@gmail.com
|
45
45
|
executables: []
|
@@ -55,6 +55,10 @@ files:
|
|
55
55
|
- app/channels/components_channel.rb
|
56
56
|
- app/controllers/turbo_live/components_controller.rb
|
57
57
|
- config/routes.rb
|
58
|
+
- examples/countdown_component.rb
|
59
|
+
- examples/counter_component.rb
|
60
|
+
- examples/showcase_component.rb
|
61
|
+
- examples/tic_tac_toe_component.rb
|
58
62
|
- lib/turbo_live.rb
|
59
63
|
- lib/turbo_live/component.rb
|
60
64
|
- lib/turbo_live/engine.rb
|
@@ -95,5 +99,5 @@ requirements: []
|
|
95
99
|
rubygems_version: 3.5.16
|
96
100
|
signing_key:
|
97
101
|
specification_version: 4
|
98
|
-
summary:
|
102
|
+
summary: Async, progressively enhanced, live components for Ruby applications
|
99
103
|
test_files: []
|