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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b117e175ea920117e11f0cd6c3bab5cafc0ca7c1860fb6b890c632a37efe1509
4
- data.tar.gz: ca56b1155834dbeb85a29112a2f85b0901dbf14d61909587f698f45cfc25bb34
3
+ metadata.gz: 973ebc91558a780fcdfc2e8dea4c6b1bc3d090678ee30535768e3b9d7ad1f9aa
4
+ data.tar.gz: 63e48c4f2b207557d9b111609a3dad42a10f70ea3719ea96a1477ed8f4e952fe
5
5
  SHA512:
6
- metadata.gz: fce48eb8c3ae06f14fb6bb365004cf399c4f85d4f147776a7b05d6d7ec4b2833182fe6e4fd5d99781ef555daaad2d0bcd4b7b757378c645a7486138c2d356809
7
- data.tar.gz: f0b1875a9143f5ca1ca218c05b1dbd3439ff7ebb13b8aabe9d72fcd58746fe41e8cde508f3a41f138ae3acfb25c66247594c1856397eaa89d2b9f6aaf170bea4
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 [:increment]
142
+ in :increment
143
143
  self.count += 1
144
- in [:decrement]
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
- > Note: Currently, only `:click` and `:change` events are supported.
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([:increment])
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
@@ -4,7 +4,7 @@ module TurboLive
4
4
  class ComponentsController < ActionController::API
5
5
  def update
6
6
  stream = Renderer.render params.to_unsafe_hash
7
- render plain: stream
7
+ render plain: stream if stream
8
8
  end
9
9
  end
10
10
  end
@@ -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
- else
38
- CounterComponent
42
+ when :flappy_bird
43
+ FlappyBirdComponent
39
44
  end
40
45
  end
41
46
  end
@@ -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 => to_verifiable(event)}.to_json
56
- add_data :every, data
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 add_data(type, value)
62
- # Temporary hack to embed data.
63
- # Switch to HTML templates
71
+ def add_meta(type, data)
64
72
  div(
65
- class: "turbo-live-data",
66
- data_turbo_live_id: verifiable_live_id,
67
- data_turbo_live_data_type: type,
68
- data_turbo_live_data_value: value,
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, instance_variable_get(:"@#{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}
@@ -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
- [payload_event]
34
+ payload_event
32
35
  end
33
36
  end
34
37
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TurboLive
4
- VERSION = "0.1.2"
4
+ VERSION = "0.2.0"
5
5
  end
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.1.2",
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.1.2",
9
+ "version": "0.2.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@hotwired/stimulus": "^3.2.2",
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radioactive-labs/turbo-live",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
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",
@@ -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
- console.log("TurboLiveChannel initialized")
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
- console.log("TurboLiveChannel connected")
12
+ logger.debug("TurboLiveChannel connected")
11
13
  window.turboLive = this;
12
14
  },
13
15
 
14
16
  received(turbo_stream) {
15
- console.log("TurboLiveChannel received", turbo_stream)
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
- console.log("TurboLiveChannel disconnected")
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
- console.log("TurboLiveChannel rejected")
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
- console.log("TurboLiveController connected:", this.element.id, this.component)
15
-
16
- this.intervals = []
17
-
18
- this.#readEmbeddedData()
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
- disconnect() {
22
- console.log("TurboLiveController disconnected")
23
- this.#clearIntervals()
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
- console.log("TurboLiveController dispatch:", this.element.id, event, payload)
28
- let data = { id: this.element.id, event: event, payload: payload, component: this.component }
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
- console.log("TurboLiveController dispatching via websockets")
40
+ logger.debug("TurboLiveController dispatching via", this.element.id, "websockets")
31
41
  window.turboLive.send(data)
32
- }
33
- else {
34
- console.log("TurboLiveController dispatching via HTTP")
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
- // event.preventDefault();
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
- // event.preventDefault();
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
- #readEmbeddedData() {
72
- this.element.querySelectorAll(".turbo-live-data").forEach((element) => {
73
- if (this.element.id != element.dataset.turboLiveId) return;
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
- let live_event = params[name]
108
- this.dispatch(name, [live_event])
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
- let value = target.value
113
- let live_event = params[name]
114
- this.dispatch(name, [live_event, value])
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.1.2
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-13 00:00:00.000000000 Z
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