turbo_live 0.1.0 → 0.1.2

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: 374c0f738381bbb9e6b5537e995fedb58f3d6134e2a127d35ee1f9cf5dacc213
4
- data.tar.gz: 1219f6ff5ebabfeecbd412a66f54d48744964ab67998137f3d5f1ea746c04e88
3
+ metadata.gz: b117e175ea920117e11f0cd6c3bab5cafc0ca7c1860fb6b890c632a37efe1509
4
+ data.tar.gz: ca56b1155834dbeb85a29112a2f85b0901dbf14d61909587f698f45cfc25bb34
5
5
  SHA512:
6
- metadata.gz: '092dcf7715ef30e5d309c7e6332218098ac190985a4e58b4d26384c26803815bc2029066b4e93afcdf4039f24bdfc51fd8891ccde9add989296e5956d9786408'
7
- data.tar.gz: b2bfaa08e8673940757d8673c66b7d2dba11cf5b857c8ea1e66b5e4f2de955e4bcead0ebb6de9db84a9fcb443c9d2e6ed7391fd5cb4a8dbd54945f60fee3aba4
6
+ metadata.gz: fce48eb8c3ae06f14fb6bb365004cf399c4f85d4f147776a7b05d6d7ec4b2833182fe6e4fd5d99781ef555daaad2d0bcd4b7b757378c645a7486138c2d356809
7
+ data.tar.gz: f0b1875a9143f5ca1ca218c05b1dbd3439ff7ebb13b8aabe9d72fcd58746fe41e8cde508f3a41f138ae3acfb25c66247594c1856397eaa89d2b9f6aaf170bea4
data/README.md CHANGED
@@ -1,39 +1,230 @@
1
1
  # TurboLive
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
4
-
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/turbo_live`. To experiment with that code, run `bin/console` for an interactive prompt.
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
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
27
+ Add it to your project with:
28
+
29
+ ```console
30
+ bundle add 'turbo_live'
31
+ ```
10
32
 
11
- Install the gem and add to the application's Gemfile by executing:
33
+ Or install it yourself using:
12
34
 
13
- ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
35
+ ```console
36
+ gem install turbo_live
15
37
  ```
16
38
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
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
- ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
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
- TODO: Write usage instructions here
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
- ## Development
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
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
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
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
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
+
218
+ For more issues, please check our [FAQ](https://github.com/radioactive-labs/turbo_live/wiki/FAQ) or open an issue on GitHub.
32
219
 
33
220
  ## Contributing
34
221
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/turbo_live.
222
+ We welcome contributions to TurboLive! Please see our [Contributing Guidelines](CONTRIBUTING.md) for more information on how to get started.
223
+
224
+ ## Changelog
225
+
226
+ See the [CHANGELOG.md](CHANGELOG.md) file for details on each release.
36
227
 
37
228
  ## License
38
229
 
39
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
230
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboLive
4
+ class ComponentsChannel < ActionCable::Channel::Base
5
+ def subscribed
6
+ stream_from stream_name
7
+ end
8
+
9
+ def receive(params)
10
+ stream = Renderer.render params
11
+ ActionCable.server.broadcast(stream_name, stream)
12
+ end
13
+
14
+ protected
15
+
16
+ def stream_name
17
+ @stream_name ||= "turbo_live-#{SecureRandom.hex}"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboLive
4
+ class ComponentsController < ActionController::API
5
+ def update
6
+ stream = Renderer.render params.to_unsafe_hash
7
+ render plain: stream
8
+ end
9
+ end
10
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,3 @@
1
+ TurboLive::Engine.routes.draw do
2
+ post "" => "components#update"
3
+ end
@@ -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 || "&nbsp;".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
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "phlex"
4
+
5
+ module TurboLive
6
+ class Component < Phlex::HTML
7
+ extend Literal::Properties
8
+
9
+ SUPPORTED_EVENTS = %i[click change].freeze
10
+
11
+ def self.state(name, type, **options, &block)
12
+ options = {reader: :public, writer: :protected}.merge(**options).compact
13
+ prop(name, _Nilable(type), **options, &block)
14
+ end
15
+
16
+ state :live_id, String, writer: nil do |value|
17
+ value || SecureRandom.hex
18
+ end
19
+
20
+ def view_template
21
+ div(
22
+ id: verifiable_live_id,
23
+ style: "display: contents;",
24
+ data_controller: "turbo-live",
25
+ data_turbo_live_component_value: to_verifiable(serialize)
26
+ ) do
27
+ view
28
+ end
29
+ end
30
+
31
+ def update(input)
32
+ end
33
+
34
+ def verifiable_live_id
35
+ to_verifiable(live_id)
36
+ end
37
+
38
+ protected
39
+
40
+ def on(**mappings)
41
+ actions = []
42
+ params = []
43
+ mappings.each do |event, param|
44
+ raise NotImplementedError, "TurboLive does not support '#{event}' events" unless SUPPORTED_EVENTS.include?(event)
45
+
46
+ actions << "#{event}->turbo-live#on#{event.capitalize}"
47
+ params << [:"data_turbo_live_#{event}_param", to_verifiable(param)]
48
+ end
49
+
50
+ data_action = actions.join(" ")
51
+ params.to_h.merge(data_action: data_action)
52
+ end
53
+
54
+ def every(milliseconds, event)
55
+ data = {milliseconds => to_verifiable(event)}.to_json
56
+ add_data :every, data
57
+ end
58
+
59
+ private
60
+
61
+ def add_data(type, value)
62
+ # Temporary hack to embed data.
63
+ # Switch to HTML templates
64
+ 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,
69
+ style: "display: none;", display: :none
70
+ ) {}
71
+ end
72
+
73
+ def serialize
74
+ state = self.class.literal_properties.map do |prop|
75
+ [prop.name, instance_variable_get(:"@#{prop.name}")]
76
+ end.to_h
77
+
78
+ {klass: self.class.to_s, state: state}
79
+ end
80
+
81
+ def to_verifiable(value)
82
+ TurboLive.verifier.generate value
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboLive
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace TurboLive
6
+
7
+ initializer "turbo_live.verifier_key" do
8
+ config.after_initialize do
9
+ TurboLive.verifier_key = Rails.application.key_generator.generate_key("turbo_live/verifier_key")
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboLive
4
+ class Renderer
5
+ class << self
6
+ def render(data)
7
+ data = data.symbolize_keys
8
+ # build the payload
9
+ payload = extract_payload(data)
10
+ # create the component
11
+ component = build_component(data)
12
+ # run the update function
13
+ component.update payload
14
+ # render the replace stream
15
+ <<~STREAM
16
+ <turbo-stream action="replace" target="#{data[:id]}">
17
+ <template>
18
+ #{component.call}
19
+ </template>
20
+ </turbo-stream>
21
+ STREAM
22
+ end
23
+
24
+ private
25
+
26
+ def extract_payload(data)
27
+ payload_event = from_verifiable(data[:payload][0])
28
+ if data[:payload].size == 2
29
+ [payload_event, data[:payload][1]]
30
+ else
31
+ [payload_event]
32
+ end
33
+ end
34
+
35
+ def build_component(data)
36
+ component_data = from_verifiable(data[:component])
37
+ component_klass = component_data[:klass].safe_constantize
38
+ # Ensure we have a correct class
39
+ unless component_klass.is_a?(Class) && component_klass < Component
40
+ raise ArgumentError, "[IMPORTANT!!!] Unexpected class: #{component_klass}"
41
+ end
42
+
43
+ component = component_klass.new(**component_data[:state])
44
+ # rudimentary checksum to ensure id matches
45
+ raise ArgumentError, "component ID mismatch" unless component.verifiable_live_id == data[:id]
46
+
47
+ component
48
+ end
49
+
50
+ def from_verifiable(verifiable)
51
+ TurboLive.verifier.verified verifiable
52
+ end
53
+ end
54
+ end
55
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TurboLive
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/turbo_live.rb CHANGED
@@ -1,8 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "turbo_live/version"
4
+ require_relative "turbo_live/component"
5
+ require_relative "turbo_live/renderer"
6
+
7
+ require_relative "turbo_live/engine" if defined?(Rails)
8
+ require_relative "../app/channels/components_channel" if defined?(ActionCable)
4
9
 
5
10
  module TurboLive
6
11
  class Error < StandardError; end
7
- # Your code goes here...
12
+
13
+ class << self
14
+ attr_writer :verifier_key
15
+
16
+ def verifier
17
+ @verifier ||= ActiveSupport::MessageVerifier.new(verifier_key, digest: "SHA256", serializer: YAML)
18
+ end
19
+
20
+ def verifier_key
21
+ @verifier_key or raise ArgumentError, "Turbo requires a verifier_key"
22
+ end
23
+ end
8
24
  end
data/package-lock.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@radioactive-labs/turbo-live",
3
+ "version": "0.1.2",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "@radioactive-labs/turbo-live",
9
+ "version": "0.1.2",
10
+ "license": "MIT",
11
+ "dependencies": {
12
+ "@hotwired/stimulus": "^3.2.2",
13
+ "@hotwired/turbo": "^8.0.4"
14
+ },
15
+ "devDependencies": {}
16
+ },
17
+ "node_modules/@hotwired/stimulus": {
18
+ "version": "3.2.2",
19
+ "resolved": "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.2.2.tgz",
20
+ "integrity": "sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A==",
21
+ "license": "MIT"
22
+ },
23
+ "node_modules/@hotwired/turbo": {
24
+ "version": "8.0.10",
25
+ "resolved": "https://registry.npmjs.org/@hotwired/turbo/-/turbo-8.0.10.tgz",
26
+ "integrity": "sha512-xen1YhNQirAHlA8vr/444XsTNITC1Il2l/Vx4w8hAWPpI5nQO78mVHNsmFuayETodzPwh25ob2TgfCEV/Loiog==",
27
+ "license": "MIT",
28
+ "engines": {
29
+ "node": ">= 14"
30
+ }
31
+ }
32
+ }
33
+ }
data/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@radioactive-labs/turbo-live",
3
+ "version": "0.1.2",
4
+ "description": "Async, progressively enhanced, live components for Ruby applications that work over Websockets and HTTP.",
5
+ "type": "module",
6
+ "main": "src/js/core.js",
7
+ "files": [
8
+ "src/"
9
+ ],
10
+ "author": "Stefan Froelich (@thedumbtechguy)",
11
+ "license": "MIT",
12
+ "bugs": {
13
+ "url": "https://github.com/radioactive-labs/turbo_live/issues"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/radioactive-labs/turbo_live.git"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "homepage": "https://github.com/radioactive-labs/turbo_live#readme",
23
+ "dependencies": {
24
+ "@hotwired/stimulus": "^3.2.2",
25
+ "@hotwired/turbo": "^8.0.4"
26
+ },
27
+ "devDependencies": {},
28
+ "scripts": {}
29
+ }
data/src/.npmignore ADDED
@@ -0,0 +1 @@
1
+ build/
@@ -0,0 +1,7 @@
1
+ // Import controllers here
2
+ import registerTurboLiveChannel from "./turbo_live_channel.js"
3
+
4
+ export default function (consumer) {
5
+ // Register channels here
6
+ registerTurboLiveChannel(consumer)
7
+ }
@@ -0,0 +1,31 @@
1
+ export default function (consumer) {
2
+ consumer.subscriptions.create({ channel: "TurboLive::ComponentsChannel" }, {
3
+ // Called once when the subscription is created.
4
+ initialized() {
5
+ console.log("TurboLiveChannel initialized")
6
+ },
7
+
8
+ // Called when the subscription is ready for use on the server.
9
+ connected() {
10
+ console.log("TurboLiveChannel connected")
11
+ window.turboLive = this;
12
+ },
13
+
14
+ received(turbo_stream) {
15
+ console.log("TurboLiveChannel received", turbo_stream)
16
+ Turbo.renderStreamMessage(turbo_stream);
17
+ },
18
+
19
+ // Called when the WebSocket connection is closed.
20
+ disconnected() {
21
+ console.log("TurboLiveChannel disconnected")
22
+ window.turboLive = null;
23
+ },
24
+
25
+ // Called when the subscription is rejected by the server.
26
+ rejected() {
27
+ console.log("TurboLiveChannel rejected")
28
+ window.turboLive = null;
29
+ },
30
+ })
31
+ }
@@ -0,0 +1,7 @@
1
+ // Import controllers here
2
+ import TurboLiveController from "./turbo_live_controller.js"
3
+
4
+ export default function (application) {
5
+ // Register controllers here
6
+ application.register("turbo-live", TurboLiveController)
7
+ }
@@ -0,0 +1,116 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static values = {
5
+ id: String,
6
+ component: String,
7
+ }
8
+
9
+ get component() {
10
+ return this.componentValue
11
+ }
12
+
13
+ connect() {
14
+ console.log("TurboLiveController connected:", this.element.id, this.component)
15
+
16
+ this.intervals = []
17
+
18
+ this.#readEmbeddedData()
19
+ }
20
+
21
+ disconnect() {
22
+ console.log("TurboLiveController disconnected")
23
+ this.#clearIntervals()
24
+ }
25
+
26
+ 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 }
29
+ if (window.turboLive) {
30
+ console.log("TurboLiveController dispatching via websockets")
31
+ 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
+ });
56
+ }
57
+ }
58
+
59
+ onClick(event) {
60
+ // event.preventDefault();
61
+ console.log("TurboLiveController onClick")
62
+ this.#dispatchSimpleEvent("click", event)
63
+ }
64
+
65
+ onChange(event) {
66
+ // event.preventDefault();
67
+ console.log("TurboLiveController onChange")
68
+ this.#dispatchValueEvent("change", event)
69
+ }
70
+
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
+ })
104
+ }
105
+
106
+ #dispatchSimpleEvent(name, { params }) {
107
+ let live_event = params[name]
108
+ this.dispatch(name, [live_event])
109
+ }
110
+
111
+ #dispatchValueEvent(name, { params, target }) {
112
+ let value = target.value
113
+ let live_event = params[name]
114
+ this.dispatch(name, [live_event, value])
115
+ }
116
+ }
data/src/js/core.js ADDED
@@ -0,0 +1,5 @@
1
+ import registerControllers from "./controllers/register_controllers.js"
2
+ import registerChannels from "./channels/register_channels.js"
3
+
4
+
5
+ export { registerControllers, registerChannels }
metadata CHANGED
@@ -1,17 +1,45 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: turbo_live
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
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 00:00:00.000000000 Z
12
- dependencies: []
13
- description: Stateless live components for Ruby applications that work over Websockets
14
- and HTTP.
11
+ date: 2024-10-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: phlex-rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: literal
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Async, progressively enhanced, live components for Ruby applications
42
+ that work over Websockets and HTTP.
15
43
  email:
16
44
  - sfroelich01@gmail.com
17
45
  executables: []
@@ -24,9 +52,27 @@ files:
24
52
  - LICENSE.txt
25
53
  - README.md
26
54
  - Rakefile
55
+ - app/channels/components_channel.rb
56
+ - app/controllers/turbo_live/components_controller.rb
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
27
62
  - lib/turbo_live.rb
63
+ - lib/turbo_live/component.rb
64
+ - lib/turbo_live/engine.rb
65
+ - lib/turbo_live/renderer.rb
28
66
  - lib/turbo_live/version.rb
67
+ - package-lock.json
68
+ - package.json
29
69
  - sig/turbo_live.rbs
70
+ - src/.npmignore
71
+ - src/js/channels/register_channels.js
72
+ - src/js/channels/turbo_live_channel.js
73
+ - src/js/controllers/register_controllers.js
74
+ - src/js/controllers/turbo_live_controller.js
75
+ - src/js/core.js
30
76
  homepage: https://github.com/radioactive-labs/turbo_live
31
77
  licenses:
32
78
  - MIT
@@ -53,5 +99,5 @@ requirements: []
53
99
  rubygems_version: 3.5.16
54
100
  signing_key:
55
101
  specification_version: 4
56
- summary: Stateless live components for Ruby applications
102
+ summary: Async, progressively enhanced, live components for Ruby applications
57
103
  test_files: []