turbo_live 0.1.2 → 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/README.md +19 -4
- data/app/channels/components_channel.rb +2 -2
- data/app/controllers/turbo_live/components_controller.rb +1 -1
- data/examples/flappy_bird_component.rb +143 -0
- data/examples/showcase_component.rb +8 -3
- data/lib/turbo_live/component.rb +21 -12
- data/lib/turbo_live/renderer.rb +7 -4
- data/lib/turbo_live/version.rb +1 -1
- data/lib/turbo_live.rb +5 -0
- data/package-lock.json +2 -2
- data/package.json +1 -1
- data/src/js/channels/turbo_live_channel.js +7 -5
- data/src/js/controllers/register_controllers.js +2 -0
- data/src/js/controllers/turbo_live_controller.js +60 -78
- data/src/js/controllers/turbo_live_meta_data_controller.js +69 -0
- data/src/js/core.js +2 -1
- data/src/js/logger.js +57 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 973ebc91558a780fcdfc2e8dea4c6b1bc3d090678ee30535768e3b9d7ad1f9aa
|
4
|
+
data.tar.gz: 63e48c4f2b207557d9b111609a3dad42a10f70ea3719ea96a1477ed8f4e952fe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f1118112757bfde64bbdd6417827ad56ea031ba950f2006a8151436f55101ca14543b991317d3825ecd143844417b2fafe84c2d6cccf7d408c9c14d213e18c1f
|
7
|
+
data.tar.gz: 7c1269811dd43b3a80188f9cafefc571c198c0eb9bd5adf9683353b7a67ef4ef915832a29bdf3fc1db710eeeb10a8ebca4b1829d4b05dacb7e2d05468891c01d
|
data/README.md
CHANGED
@@ -139,9 +139,9 @@ Handle events in the `update` method:
|
|
139
139
|
```ruby
|
140
140
|
def update(input)
|
141
141
|
case input
|
142
|
-
in
|
142
|
+
in :increment
|
143
143
|
self.count += 1
|
144
|
-
in
|
144
|
+
in :decrement
|
145
145
|
self.count -= 1
|
146
146
|
end
|
147
147
|
end
|
@@ -165,7 +165,21 @@ You can also emit compound events that carry extra data:
|
|
165
165
|
button(**on(click: [:change_value, 1])) { "+" }
|
166
166
|
```
|
167
167
|
|
168
|
-
|
168
|
+
Certain events carry extra data as well, such as `input` and `change` events.
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
input(value:, input_value, **on(input: :input_changed))
|
172
|
+
```
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
def update(input)
|
176
|
+
case input
|
177
|
+
in [:input_changed, value]
|
178
|
+
self.input_value = value
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
> Note: Currently, only `:click`, `:input` and `:change` events are supported.
|
169
183
|
|
170
184
|
### Timed Events
|
171
185
|
|
@@ -201,7 +215,7 @@ class CounterComponentTest < ActiveSupport::TestCase
|
|
201
215
|
test "increments count" do
|
202
216
|
component = CounterComponent.new
|
203
217
|
assert_equal 0, component.count
|
204
|
-
component.update(
|
218
|
+
component.update(:increment)
|
205
219
|
assert_equal 1, component.count
|
206
220
|
end
|
207
221
|
end
|
@@ -214,6 +228,7 @@ Common issues and their solutions:
|
|
214
228
|
1. **Component not updating**: Ensure that your `update` method is correctly handling the event and modifying the state.
|
215
229
|
2. **WebSocket connection failing**: Check your ActionCable configuration and ensure that your server supports WebSocket connections.
|
216
230
|
3. **JavaScript errors**: Make sure you've correctly set up the TurboLive JavaScript integration in your application.
|
231
|
+
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.
|
217
232
|
|
218
233
|
For more issues, please check our [FAQ](https://github.com/radioactive-labs/turbo_live/wiki/FAQ) or open an issue on GitHub.
|
219
234
|
|
@@ -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,143 @@
|
|
1
|
+
class FlappyBirdComponent < TurboLive::Component
|
2
|
+
GRAVITY = 0.5
|
3
|
+
JUMP_STRENGTH = -10.0
|
4
|
+
GAME_HEIGHT = 400
|
5
|
+
GAME_WIDTH = 300
|
6
|
+
BIRD_SIZE = 20
|
7
|
+
OBSTACLE_WIDTH = 50
|
8
|
+
GAP_HEIGHT = 100
|
9
|
+
OBSTACLE_SPEED = 2
|
10
|
+
|
11
|
+
state :bird_y, Float do |value|
|
12
|
+
value || 150.0
|
13
|
+
end
|
14
|
+
|
15
|
+
state :bird_velocity, Float do |value|
|
16
|
+
value || 0.0
|
17
|
+
end
|
18
|
+
|
19
|
+
state :obstacles, Array do |value|
|
20
|
+
value || []
|
21
|
+
end
|
22
|
+
|
23
|
+
state :score, Integer do |value|
|
24
|
+
value || 0
|
25
|
+
end
|
26
|
+
|
27
|
+
state :game_over, _Boolean
|
28
|
+
|
29
|
+
state :nonce, Integer do |value|
|
30
|
+
value || 0
|
31
|
+
end
|
32
|
+
|
33
|
+
NONCES = {}
|
34
|
+
|
35
|
+
def initialize(...)
|
36
|
+
super
|
37
|
+
NONCES[live_id] ||= 0
|
38
|
+
end
|
39
|
+
|
40
|
+
def view
|
41
|
+
div(class: "flappy-bird-game") do
|
42
|
+
render_game
|
43
|
+
render_controls
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def render_game
|
48
|
+
svg(viewBox: "0 0 #{GAME_WIDTH} #{GAME_HEIGHT}", width: GAME_WIDTH, height: GAME_HEIGHT) do |svg|
|
49
|
+
# Render bird
|
50
|
+
svg.circle(cx: 50, cy: bird_y, r: BIRD_SIZE / 2, fill: "yellow")
|
51
|
+
|
52
|
+
# Render obstacles
|
53
|
+
obstacles.each do |obstacle|
|
54
|
+
svg.rect(x: obstacle[:x], y: 0, width: OBSTACLE_WIDTH, height: obstacle[:top], fill: "green")
|
55
|
+
svg.rect(x: obstacle[:x], y: obstacle[:bottom], width: OBSTACLE_WIDTH, height: GAME_HEIGHT - obstacle[:bottom], fill: "green")
|
56
|
+
end
|
57
|
+
|
58
|
+
# Render score
|
59
|
+
svg.text(x: 10, y: 30, fill: "white", "font-size": "20px") { score.to_s }
|
60
|
+
|
61
|
+
# Render game over message
|
62
|
+
if game_over
|
63
|
+
svg.text(x: GAME_WIDTH / 2, y: GAME_HEIGHT / 2, fill: "red", "font-size": "30px", "text-anchor": "middle") { "Game Over" }
|
64
|
+
else
|
65
|
+
every(1000 / 30.0, :tick)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def render_controls
|
71
|
+
div(class: "controls") do
|
72
|
+
button(**on(click: :jump)) { "Jump / Restart" }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def update(input)
|
77
|
+
case input
|
78
|
+
in [:jump]
|
79
|
+
if game_over
|
80
|
+
reset_game
|
81
|
+
else
|
82
|
+
self.nonce = NONCES[live_id] = NONCES[live_id] + 1
|
83
|
+
self.bird_velocity = JUMP_STRENGTH
|
84
|
+
end
|
85
|
+
in [:tick]
|
86
|
+
norender! if nonce < NONCES[live_id]
|
87
|
+
update_game_state
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def update_game_state
|
94
|
+
return if game_over
|
95
|
+
|
96
|
+
# Update bird position
|
97
|
+
self.bird_velocity += GRAVITY
|
98
|
+
self.bird_y += bird_velocity
|
99
|
+
|
100
|
+
# Add new obstacles
|
101
|
+
if obstacles.empty? || obstacles.last[:x] < GAME_WIDTH - 200
|
102
|
+
add_obstacle
|
103
|
+
end
|
104
|
+
|
105
|
+
# Update obstacle positions
|
106
|
+
self.obstacles = obstacles.map do |obstacle|
|
107
|
+
obstacle[:x] -= OBSTACLE_SPEED
|
108
|
+
obstacle
|
109
|
+
end.reject { |obstacle| obstacle[:x] < -OBSTACLE_WIDTH }
|
110
|
+
|
111
|
+
# Check collisions
|
112
|
+
check_collisions
|
113
|
+
|
114
|
+
# Update score
|
115
|
+
self.score += 1 if obstacles.any? { |obstacle| obstacle[:x] == 48 } # Bird's x position is 50, width is 20
|
116
|
+
end
|
117
|
+
|
118
|
+
def add_obstacle
|
119
|
+
gap_start = rand(50..(GAME_HEIGHT - GAP_HEIGHT - 50))
|
120
|
+
obstacles << {x: GAME_WIDTH, top: gap_start, bottom: gap_start + GAP_HEIGHT}
|
121
|
+
end
|
122
|
+
|
123
|
+
def check_collisions
|
124
|
+
if bird_y < 0 || bird_y > GAME_HEIGHT
|
125
|
+
self.game_over = true
|
126
|
+
end
|
127
|
+
|
128
|
+
obstacles.each do |obstacle|
|
129
|
+
if (obstacle[:x] < 70 && obstacle[:x] > 30) &&
|
130
|
+
(bird_y < obstacle[:top] + BIRD_SIZE / 2 || bird_y > obstacle[:bottom] - BIRD_SIZE / 2)
|
131
|
+
self.game_over = true
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def reset_game
|
137
|
+
self.bird_y = 150.0
|
138
|
+
self.bird_velocity = 0.0
|
139
|
+
self.obstacles = []
|
140
|
+
self.score = 0
|
141
|
+
self.game_over = false
|
142
|
+
end
|
143
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
class ShowcaseComponent < TurboLive::Component
|
2
|
-
state :component, Symbol
|
2
|
+
state :component, Symbol do |value|
|
3
|
+
value || :counter
|
4
|
+
end
|
3
5
|
|
4
6
|
def view
|
5
7
|
div class: "container" do
|
@@ -9,6 +11,7 @@ class ShowcaseComponent < TurboLive::Component
|
|
9
11
|
li { button(**on(click: [:change_component, :counter])) { "Counter" } }
|
10
12
|
li { button(**on(click: [:change_component, :countdown])) { "Countdown" } }
|
11
13
|
li { button(**on(click: [:change_component, :tic_tac_toe])) { "TicTacToe" } }
|
14
|
+
li { button(**on(click: [:change_component, :flappy_bird])) { "Flappy Bird" } }
|
12
15
|
end
|
13
16
|
end
|
14
17
|
div class: "right-column" do
|
@@ -30,12 +33,14 @@ class ShowcaseComponent < TurboLive::Component
|
|
30
33
|
|
31
34
|
def selected_component
|
32
35
|
case component
|
36
|
+
when :counter
|
37
|
+
CounterComponent
|
33
38
|
when :countdown
|
34
39
|
CountdownComponent
|
35
40
|
when :tic_tac_toe
|
36
41
|
TicTacToeComponent
|
37
|
-
|
38
|
-
|
42
|
+
when :flappy_bird
|
43
|
+
FlappyBirdComponent
|
39
44
|
end
|
40
45
|
end
|
41
46
|
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
|
@@ -22,7 +23,8 @@ module TurboLive
|
|
22
23
|
id: verifiable_live_id,
|
23
24
|
style: "display: contents;",
|
24
25
|
data_controller: "turbo-live",
|
25
|
-
data_turbo_live_component_value: to_verifiable(serialize)
|
26
|
+
data_turbo_live_component_value: to_verifiable(serialize),
|
27
|
+
data_turbo_live_protocol_version_value: TurboLive::PROTOCOL_VERSION
|
26
28
|
) do
|
27
29
|
view
|
28
30
|
end
|
@@ -52,27 +54,34 @@ module TurboLive
|
|
52
54
|
end
|
53
55
|
|
54
56
|
def every(milliseconds, event)
|
55
|
-
data = {milliseconds
|
56
|
-
|
57
|
+
data = {interval: milliseconds, event: to_verifiable(event)}.to_json
|
58
|
+
add_meta :interval, data
|
59
|
+
end
|
60
|
+
|
61
|
+
def norender
|
62
|
+
SKIP_RENDER
|
63
|
+
end
|
64
|
+
|
65
|
+
def norender!
|
66
|
+
raise SkipRender
|
57
67
|
end
|
58
68
|
|
59
69
|
private
|
60
70
|
|
61
|
-
def
|
62
|
-
# Temporary hack to embed data.
|
63
|
-
# Switch to HTML templates
|
71
|
+
def add_meta(type, data)
|
64
72
|
div(
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
73
|
+
data_controller: "turbo-live-meta-data",
|
74
|
+
data_turbo_live_target: "meta",
|
75
|
+
data_turbo_live_meta_data_type_value: type,
|
76
|
+
data_turbo_live_meta_data_data_value: data,
|
77
|
+
data_action: "turbo:before-morph-element->turbo-live-meta-data#beforeMorph turbo:morph-element->turbo-live-meta-data#afterMorph",
|
69
78
|
style: "display: none;", display: :none
|
70
79
|
) {}
|
71
80
|
end
|
72
81
|
|
73
82
|
def serialize
|
74
83
|
state = self.class.literal_properties.map do |prop|
|
75
|
-
[prop.name,
|
84
|
+
[prop.name, public_send(prop.name)]
|
76
85
|
end.to_h
|
77
86
|
|
78
87
|
{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
|
@@ -28,7 +31,7 @@ module TurboLive
|
|
28
31
|
if data[:payload].size == 2
|
29
32
|
[payload_event, data[:payload][1]]
|
30
33
|
else
|
31
|
-
|
34
|
+
payload_event
|
32
35
|
end
|
33
36
|
end
|
34
37
|
|
data/lib/turbo_live/version.rb
CHANGED
data/lib/turbo_live.rb
CHANGED
@@ -10,6 +10,11 @@ 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
|
+
PROTOCOL_VERSION = "0.2.0"
|
16
|
+
SKIP_RENDER = :skip_render
|
17
|
+
|
13
18
|
class << self
|
14
19
|
attr_writer :verifier_key
|
15
20
|
|
data/package-lock.json
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
{
|
2
2
|
"name": "@radioactive-labs/turbo-live",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.2.0",
|
4
4
|
"lockfileVersion": 3,
|
5
5
|
"requires": true,
|
6
6
|
"packages": {
|
7
7
|
"": {
|
8
8
|
"name": "@radioactive-labs/turbo-live",
|
9
|
-
"version": "0.
|
9
|
+
"version": "0.2.0",
|
10
10
|
"license": "MIT",
|
11
11
|
"dependencies": {
|
12
12
|
"@hotwired/stimulus": "^3.2.2",
|
data/package.json
CHANGED
@@ -1,30 +1,32 @@
|
|
1
|
+
import { logger } from "../logger.js"
|
2
|
+
|
1
3
|
export default function (consumer) {
|
2
4
|
consumer.subscriptions.create({ channel: "TurboLive::ComponentsChannel" }, {
|
3
5
|
// Called once when the subscription is created.
|
4
6
|
initialized() {
|
5
|
-
|
7
|
+
logger.debug("TurboLiveChannel initialized")
|
6
8
|
},
|
7
9
|
|
8
10
|
// Called when the subscription is ready for use on the server.
|
9
11
|
connected() {
|
10
|
-
|
12
|
+
logger.debug("TurboLiveChannel connected")
|
11
13
|
window.turboLive = this;
|
12
14
|
},
|
13
15
|
|
14
16
|
received(turbo_stream) {
|
15
|
-
|
17
|
+
logger.info("TurboLiveChannel received", turbo_stream)
|
16
18
|
Turbo.renderStreamMessage(turbo_stream);
|
17
19
|
},
|
18
20
|
|
19
21
|
// Called when the WebSocket connection is closed.
|
20
22
|
disconnected() {
|
21
|
-
|
23
|
+
logger.debug("TurboLiveChannel disconnected")
|
22
24
|
window.turboLive = null;
|
23
25
|
},
|
24
26
|
|
25
27
|
// Called when the subscription is rejected by the server.
|
26
28
|
rejected() {
|
27
|
-
|
29
|
+
logger.debug("TurboLiveChannel rejected")
|
28
30
|
window.turboLive = null;
|
29
31
|
},
|
30
32
|
})
|
@@ -1,7 +1,9 @@
|
|
1
1
|
// Import controllers here
|
2
2
|
import TurboLiveController from "./turbo_live_controller.js"
|
3
|
+
import TurboLiveMetaDataController from "./turbo_live_meta_data_controller.js"
|
3
4
|
|
4
5
|
export default function (application) {
|
5
6
|
// Register controllers here
|
6
7
|
application.register("turbo-live", TurboLiveController)
|
8
|
+
application.register("turbo-live-meta-data", TurboLiveMetaDataController)
|
7
9
|
}
|
@@ -1,116 +1,98 @@
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
2
|
+
import { logger } from "../logger.js"
|
3
|
+
|
4
|
+
const SupportedProtocolVersion = "0.2.0"
|
2
5
|
|
3
6
|
export default class extends Controller {
|
4
7
|
static values = {
|
5
8
|
id: String,
|
6
9
|
component: String,
|
10
|
+
protocolVersion: String,
|
7
11
|
}
|
12
|
+
static targets = ["meta"]
|
8
13
|
|
9
14
|
get component() {
|
10
15
|
return this.componentValue
|
11
16
|
}
|
12
17
|
|
13
18
|
connect() {
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
+
logger.log("TurboLiveController connect", this.element.id, this.protocolVersionValue)
|
20
|
+
if (SupportedProtocolVersion != this.protocolVersionValue) {
|
21
|
+
throw Error(`
|
22
|
+
Protocol version ${this.protocolVersionValue} is not supported
|
23
|
+
(supported version: ${SupportedProtocolVersion})
|
24
|
+
`)
|
25
|
+
}
|
19
26
|
}
|
20
27
|
|
21
|
-
|
22
|
-
|
23
|
-
this
|
28
|
+
metaTargetConnected(target) {
|
29
|
+
logger.debug("TurboLiveController metaTargetConnected", this.element.id, target)
|
30
|
+
this.application
|
31
|
+
.getControllerForElementAndIdentifier(target, "turbo-live-meta-data")
|
32
|
+
.setComponent(this)
|
24
33
|
}
|
25
34
|
|
26
35
|
dispatch(event, payload) {
|
27
|
-
|
28
|
-
|
36
|
+
const data = { id: this.element.id, event, payload, component: this.component }
|
37
|
+
logger.info("TurboLiveController dispatching", this.element.id, data)
|
38
|
+
|
29
39
|
if (window.turboLive) {
|
30
|
-
|
40
|
+
logger.debug("TurboLiveController dispatching via", this.element.id, "websockets")
|
31
41
|
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
|
-
});
|
42
|
+
} else {
|
43
|
+
logger.debug("TurboLiveController dispatching via", this.element.id, "http")
|
44
|
+
this.#dispatchHTTP(data)
|
56
45
|
}
|
57
46
|
}
|
58
47
|
|
59
48
|
onClick(event) {
|
60
|
-
|
61
|
-
console.log("TurboLiveController onClick")
|
49
|
+
logger.debug("TurboLiveController onClick", this.element.id, event)
|
62
50
|
this.#dispatchSimpleEvent("click", event)
|
63
51
|
}
|
64
52
|
|
65
53
|
onChange(event) {
|
66
|
-
|
67
|
-
console.log("TurboLiveController onChange")
|
54
|
+
logger.debug("TurboLiveController onChange", this.element.id, event)
|
68
55
|
this.#dispatchValueEvent("change", event)
|
69
56
|
}
|
70
57
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
let type = element.dataset.turboLiveDataType
|
76
|
-
let value = JSON.parse(element.dataset.turboLiveDataValue)
|
77
|
-
switch (type) {
|
78
|
-
case "every":
|
79
|
-
this.#setupInterval(value)
|
80
|
-
break;
|
81
|
-
}
|
82
|
-
})
|
83
|
-
}
|
84
|
-
|
85
|
-
#setupInterval(intervalConfig) {
|
86
|
-
try {
|
87
|
-
for (let interval in intervalConfig) {
|
88
|
-
this.intervals.push(
|
89
|
-
setInterval(() => {
|
90
|
-
this.dispatch("every", [intervalConfig[interval]])
|
91
|
-
}, interval)
|
92
|
-
)
|
93
|
-
}
|
94
|
-
}
|
95
|
-
catch (e) {
|
96
|
-
console.error(e)
|
97
|
-
}
|
98
|
-
}
|
99
|
-
|
100
|
-
#clearIntervals() {
|
101
|
-
this.intervals.forEach((interval) => {
|
102
|
-
clearInterval(interval)
|
103
|
-
})
|
58
|
+
onInput(event) {
|
59
|
+
logger.debug("TurboLiveController onInput", this.element.id, event)
|
60
|
+
this.#dispatchValueEvent("input", event)
|
104
61
|
}
|
105
62
|
|
106
63
|
#dispatchSimpleEvent(name, { params }) {
|
107
|
-
|
108
|
-
|
64
|
+
logger.debug("TurboLiveController dispatchSimpleEvent", this.element.id, name, params)
|
65
|
+
const liveEvent = params[name]
|
66
|
+
this.dispatch(name, [liveEvent])
|
109
67
|
}
|
110
68
|
|
111
69
|
#dispatchValueEvent(name, { params, target }) {
|
112
|
-
|
113
|
-
|
114
|
-
|
70
|
+
logger.debug("TurboLiveController dispatchValueEvent", this.element.id, name, params)
|
71
|
+
const value = target.value
|
72
|
+
const liveEvent = params[name]
|
73
|
+
this.dispatch(name, [liveEvent, value])
|
74
|
+
}
|
75
|
+
|
76
|
+
#dispatchHTTP(data) {
|
77
|
+
fetch('/turbo_live', {
|
78
|
+
method: 'POST',
|
79
|
+
headers: {
|
80
|
+
'Content-Type': 'application/json',
|
81
|
+
},
|
82
|
+
body: JSON.stringify(data)
|
83
|
+
})
|
84
|
+
.then(response => {
|
85
|
+
if (!response.ok) {
|
86
|
+
throw new Error(`Network response was not OK`)
|
87
|
+
}
|
88
|
+
return response.text()
|
89
|
+
})
|
90
|
+
.then(turboStream => {
|
91
|
+
logger.info('TurboLiveController dispatch success', this.element.id, turboStream)
|
92
|
+
if (turboStream) Turbo.renderStreamMessage(turboStream)
|
93
|
+
})
|
94
|
+
.catch((error) => {
|
95
|
+
logger.error('TurboLiveController dispatch error', this.element.id, error)
|
96
|
+
})
|
115
97
|
}
|
116
|
-
}
|
98
|
+
}
|
@@ -0,0 +1,69 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
import { logger } from "../logger.js"
|
3
|
+
|
4
|
+
export default class extends Controller {
|
5
|
+
static values = {
|
6
|
+
type: String,
|
7
|
+
data: String,
|
8
|
+
}
|
9
|
+
|
10
|
+
connect() {
|
11
|
+
logger.debug("TurboLiveMetaDataController connected", this.typeValue, this.dataValue)
|
12
|
+
this.#setup(this.typeValue, this.dataValue)
|
13
|
+
}
|
14
|
+
|
15
|
+
disconnect() {
|
16
|
+
logger.debug("TurboLiveMetaDataController disconnected", this.typeValue, this.dataValue)
|
17
|
+
this.#teardown()
|
18
|
+
this.component = null
|
19
|
+
}
|
20
|
+
|
21
|
+
beforeMorph({ detail: { currentElement, newElement } }) {
|
22
|
+
logger.debug("TurboLiveMetaDataController beforeMorph", this.typeValue, this.dataValue)
|
23
|
+
if (currentElement.dataset.turboLiveMetaType != newElement.dataset.turboLiveMetaType ||
|
24
|
+
currentElement.dataset.turboLiveMetaValue != newElement.dataset.turboLiveMetaValue) {
|
25
|
+
logger.info("TurboLiveMetaDataController changed",
|
26
|
+
this.typeValue, this.dataValue,
|
27
|
+
newElement.dataset.turboLiveMetaDataTypeValue, newElement.dataset.turboLiveMetaDataDataValue)
|
28
|
+
|
29
|
+
this.changedDuringMorph = true
|
30
|
+
// teardown here since we still have our current state
|
31
|
+
this.#teardown()
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
afterMorph() {
|
36
|
+
logger.debug("TurboLiveMetaDataController afterMorph", this.typeValue, this.dataValue)
|
37
|
+
if (this.changedDuringMorph) {
|
38
|
+
this.#setup(this.typeValue, this.dataValue)
|
39
|
+
this.changedDuringMorph = false
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
setComponent(component) {
|
44
|
+
logger.debug("TurboLiveMetaDataController setComponent", this.typeValue, this.dataValue, component)
|
45
|
+
this.component = component
|
46
|
+
}
|
47
|
+
|
48
|
+
#setup(type, data) {
|
49
|
+
logger.debug("TurboLiveMetaDataController #setup", this.typeValue, this.dataValue, type, data)
|
50
|
+
if (type === "interval") {
|
51
|
+
this.#setupInterval(JSON.parse(data))
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
#teardown() {
|
56
|
+
logger.debug("TurboLiveMetaDataController #teardown", this.typeValue, this.dataValue)
|
57
|
+
if (this.interval) {
|
58
|
+
clearInterval(this.interval)
|
59
|
+
this.interval = null
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
#setupInterval(config) {
|
64
|
+
logger.debug("TurboLiveMetaDataController #setupInterval", this.typeValue, this.dataValue, config)
|
65
|
+
this.interval = setInterval(() => {
|
66
|
+
this.component.dispatch("interval", [config.event])
|
67
|
+
}, config.interval)
|
68
|
+
}
|
69
|
+
}
|
data/src/js/core.js
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import registerControllers from "./controllers/register_controllers.js"
|
2
2
|
import registerChannels from "./channels/register_channels.js"
|
3
|
+
import { logger, Logger } from "./logger.js"
|
3
4
|
|
4
5
|
|
5
|
-
export { registerControllers, registerChannels }
|
6
|
+
export { registerControllers, registerChannels, logger, Logger }
|
data/src/js/logger.js
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
export class Logger {
|
2
|
+
static LOG_LEVELS = {
|
3
|
+
OFF: -1,
|
4
|
+
ERROR: 0,
|
5
|
+
WARN: 1,
|
6
|
+
INFO: 2,
|
7
|
+
DEBUG: 3
|
8
|
+
};
|
9
|
+
|
10
|
+
constructor(level = Logger.LOG_LEVELS.OFF) {
|
11
|
+
this.setLevel(level)
|
12
|
+
}
|
13
|
+
|
14
|
+
setLevel(level) {
|
15
|
+
if (typeof level === 'string' && level in Logger.LOG_LEVELS) {
|
16
|
+
this.level = Logger.LOG_LEVELS[level];
|
17
|
+
} else if (typeof level === 'number' && (level === -1 || Object.values(Logger.LOG_LEVELS).includes(level))) {
|
18
|
+
this.level = level;
|
19
|
+
} else {
|
20
|
+
throw new Error('Invalid log level');
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
error(...args) {
|
25
|
+
if (this.level >= Logger.LOG_LEVELS.ERROR) console.error('[ERROR]', ...args);
|
26
|
+
}
|
27
|
+
|
28
|
+
warn(...args) {
|
29
|
+
if (this.level >= Logger.LOG_LEVELS.WARN) console.warn('[WARN]', ...args);
|
30
|
+
}
|
31
|
+
|
32
|
+
info(...args) {
|
33
|
+
if (this.level >= Logger.LOG_LEVELS.INFO) console.info('[INFO]', ...args);
|
34
|
+
}
|
35
|
+
|
36
|
+
debug(...args) {
|
37
|
+
if (this.level >= Logger.LOG_LEVELS.DEBUG) console.log('[DEBUG]', ...args);
|
38
|
+
}
|
39
|
+
|
40
|
+
log(...args) {
|
41
|
+
if (this.level > Logger.LOG_LEVELS.OFF) this.info(...args);
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
export const logger = new Logger();
|
46
|
+
|
47
|
+
// Usage examples:
|
48
|
+
// import { Logger, logger } from './logger.js';
|
49
|
+
//
|
50
|
+
// logger.setLevel('DEBUG');
|
51
|
+
// logger.debug('This is a debug message');
|
52
|
+
//
|
53
|
+
// logger.setLevel(-1);
|
54
|
+
// logger.error('This error will not be logged');
|
55
|
+
//
|
56
|
+
// const customLogger = new Logger(Logger.LOG_LEVELS.OFF);
|
57
|
+
// customLogger.error('This error will not be logged either');
|
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.
|
4
|
+
version: 0.2.0
|
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-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: phlex-rails
|
@@ -57,6 +57,7 @@ files:
|
|
57
57
|
- config/routes.rb
|
58
58
|
- examples/countdown_component.rb
|
59
59
|
- examples/counter_component.rb
|
60
|
+
- examples/flappy_bird_component.rb
|
60
61
|
- examples/showcase_component.rb
|
61
62
|
- examples/tic_tac_toe_component.rb
|
62
63
|
- lib/turbo_live.rb
|
@@ -72,7 +73,9 @@ files:
|
|
72
73
|
- src/js/channels/turbo_live_channel.js
|
73
74
|
- src/js/controllers/register_controllers.js
|
74
75
|
- src/js/controllers/turbo_live_controller.js
|
76
|
+
- src/js/controllers/turbo_live_meta_data_controller.js
|
75
77
|
- src/js/core.js
|
78
|
+
- src/js/logger.js
|
76
79
|
homepage: https://github.com/radioactive-labs/turbo_live
|
77
80
|
licenses:
|
78
81
|
- MIT
|