turbo_live 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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