turbo_live 0.1.1 → 0.1.2
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 +207 -16
- data/examples/countdown_component.rb +23 -0
- data/examples/counter_component.rb +26 -0
- data/examples/showcase_component.rb +41 -0
- data/examples/tic_tac_toe_component.rb +93 -0
- data/lib/turbo_live/version.rb +1 -1
- data/package-lock.json +2 -2
- data/package.json +2 -2
- metadata +8 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b117e175ea920117e11f0cd6c3bab5cafc0ca7c1860fb6b890c632a37efe1509
|
4
|
+
data.tar.gz: ca56b1155834dbeb85a29112a2f85b0901dbf14d61909587f698f45cfc25bb34
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fce48eb8c3ae06f14fb6bb365004cf399c4f85d4f147776a7b05d6d7ec4b2833182fe6e4fd5d99781ef555daaad2d0bcd4b7b757378c645a7486138c2d356809
|
7
|
+
data.tar.gz: f0b1875a9143f5ca1ca218c05b1dbd3439ff7ebb13b8aabe9d72fcd58746fe41e8cde508f3a41f138ae3acfb25c66247594c1856397eaa89d2b9f6aaf170bea4
|
data/README.md
CHANGED
@@ -1,39 +1,230 @@
|
|
1
1
|
# TurboLive
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
TurboLive is a Ruby gem that enables the creation of async, progressively enhanced, live components for Ruby applications. It works seamlessly over both WebSockets and HTTPS, providing real-time interactivity with graceful degradation.
|
4
|
+
|
5
|
+
## Table of Contents
|
6
|
+
|
7
|
+
- [Installation](#installation)
|
8
|
+
- [Setup](#setup)
|
9
|
+
- [Usage](#usage)
|
10
|
+
- [Creating a Component](#creating-a-component)
|
11
|
+
- [Model State](#model-state)
|
12
|
+
- [View](#view)
|
13
|
+
- [Update](#update)
|
14
|
+
- [Events](#events)
|
15
|
+
- [Manual Events](#manual-events)
|
16
|
+
- [Timed Events](#timed-events)
|
17
|
+
- [Examples](#examples)
|
18
|
+
- [Performance Considerations](#performance-considerations)
|
19
|
+
- [Testing](#testing)
|
20
|
+
- [Troubleshooting](#troubleshooting)
|
21
|
+
- [Contributing](#contributing)
|
22
|
+
- [Changelog](#changelog)
|
23
|
+
- [License](#license)
|
6
24
|
|
7
25
|
## Installation
|
8
26
|
|
9
|
-
|
27
|
+
Add it to your project with:
|
28
|
+
|
29
|
+
```console
|
30
|
+
bundle add 'turbo_live'
|
31
|
+
```
|
10
32
|
|
11
|
-
|
33
|
+
Or install it yourself using:
|
12
34
|
|
13
|
-
```
|
14
|
-
|
35
|
+
```console
|
36
|
+
gem install turbo_live
|
15
37
|
```
|
16
38
|
|
17
|
-
|
39
|
+
### JavaScript
|
40
|
+
|
41
|
+
TurboLive ships a JavaScript component that comes as an npm package. You can pin it with importmaps or install it as an npm package depending on your asset pipeline:
|
42
|
+
|
43
|
+
For importmaps:
|
18
44
|
|
19
|
-
```
|
20
|
-
|
45
|
+
```console
|
46
|
+
bin/importmap pin @radioactive-labs/turbo-live
|
47
|
+
```
|
48
|
+
|
49
|
+
For npm:
|
50
|
+
|
51
|
+
```console
|
52
|
+
npm install @radioactive-labs/turbo-live
|
53
|
+
```
|
54
|
+
|
55
|
+
## Setup
|
56
|
+
|
57
|
+
### Stimulus Controller
|
58
|
+
|
59
|
+
TurboLive uses a Stimulus controller to manage interactions. In your `app/javascript/controllers/index.js`:
|
60
|
+
|
61
|
+
```diff
|
62
|
+
import { application } from "controllers/application"
|
63
|
+
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
|
64
|
+
+import * as turboLive from "@radioactive-labs/turbo-live"
|
65
|
+
|
66
|
+
eagerLoadControllersFrom("controllers", application)
|
67
|
+
+turboLive.registerControllers(application)
|
68
|
+
```
|
69
|
+
|
70
|
+
### ActionCable (Optional)
|
71
|
+
|
72
|
+
TurboLive supports WebSockets using ActionCable with automatic failover to HTTPS. If you have ActionCable set up and would like to benefit from better performance, you can set up the integration.
|
73
|
+
|
74
|
+
In `app/javascript/channels/index.js`:
|
75
|
+
|
76
|
+
```diff
|
77
|
+
+import consumer from "./consumer"
|
78
|
+
+import * as turboLive from "@radioactive-labs/turbo-live"
|
79
|
+
+
|
80
|
+
+turboLive.registerChannels(consumer)
|
81
|
+
```
|
82
|
+
|
83
|
+
Then in your `app/javascript/application.js`:
|
84
|
+
|
85
|
+
```diff
|
86
|
+
import "@hotwired/turbo-rails"
|
87
|
+
import "controllers"
|
88
|
+
+import "channels"
|
21
89
|
```
|
22
90
|
|
23
91
|
## Usage
|
24
92
|
|
25
|
-
|
93
|
+
A TurboLive component is a self-contained, interactive unit of a web application that can update in real-time without full page reloads. Components follow [The Elm Architecture](https://guide.elm-lang.org/architecture/) pattern.
|
94
|
+
|
95
|
+
### Creating a Component
|
96
|
+
|
97
|
+
To create a TurboLive component, inherit from `TurboLive::Component`:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
class MyComponent < TurboLive::Component
|
101
|
+
# Component logic goes here
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
### Model State
|
106
|
+
|
107
|
+
Define state variables using the `state` method:
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
class MyComponent < TurboLive::Component
|
111
|
+
state :count, Integer do |value|
|
112
|
+
value || 0
|
113
|
+
end
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
> Note: State variables can only be primitive objects and basic collections.
|
118
|
+
|
119
|
+
### View
|
120
|
+
|
121
|
+
Define the component's HTML structure in the `view` method:
|
26
122
|
|
27
|
-
|
123
|
+
```ruby
|
124
|
+
def view
|
125
|
+
div do
|
126
|
+
button(**on(click: :increment)) { "+" }
|
127
|
+
span { count }
|
128
|
+
button(**on(click: :decrement)) { "-" }
|
129
|
+
end
|
130
|
+
end
|
131
|
+
```
|
132
|
+
|
133
|
+
Components are [phlex](https://www.phlex.fun/) views, allowing you to write HTML in Ruby.
|
134
|
+
|
135
|
+
### Update
|
28
136
|
|
29
|
-
|
137
|
+
Handle events in the `update` method:
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
def update(input)
|
141
|
+
case input
|
142
|
+
in [:increment]
|
143
|
+
self.count += 1
|
144
|
+
in [:decrement]
|
145
|
+
self.count -= 1
|
146
|
+
end
|
147
|
+
end
|
148
|
+
```
|
30
149
|
|
31
|
-
|
150
|
+
## Events
|
151
|
+
|
152
|
+
Events are transmitted to the server using the currently active transport (HTTP or WebSockets).
|
153
|
+
|
154
|
+
### Manual Events
|
155
|
+
|
156
|
+
Use the `on` method to set up manually triggered events:
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
button(**on(click: :decrement)) { "-" }
|
160
|
+
```
|
161
|
+
|
162
|
+
You can also emit compound events that carry extra data:
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
button(**on(click: [:change_value, 1])) { "+" }
|
166
|
+
```
|
167
|
+
|
168
|
+
> Note: Currently, only `:click` and `:change` events are supported.
|
169
|
+
|
170
|
+
### Timed Events
|
171
|
+
|
172
|
+
Use the `every` method to set up recurring events:
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
def view
|
176
|
+
div do
|
177
|
+
h1 { countdown }
|
178
|
+
every(1000, :tick) if countdown > 0
|
179
|
+
end
|
180
|
+
end
|
181
|
+
```
|
182
|
+
|
183
|
+
## Examples
|
184
|
+
|
185
|
+
See the [/examples](/examples) folder in for detailed component examples including Counter, Countdown, Showcase and Tic-Tac-Toe components.
|
186
|
+
|
187
|
+
## Performance Considerations
|
188
|
+
|
189
|
+
- Use fine-grained components to minimize the amount of data transferred and rendered.
|
190
|
+
- Implement debouncing for frequently triggered events.
|
191
|
+
- Consider using background jobs for heavy computations to keep the UI responsive.
|
192
|
+
|
193
|
+
## Testing
|
194
|
+
|
195
|
+
TurboLive components can be tested using standard Rails testing tools. Here's a basic example:
|
196
|
+
|
197
|
+
```ruby
|
198
|
+
require "test_helper"
|
199
|
+
|
200
|
+
class CounterComponentTest < ActiveSupport::TestCase
|
201
|
+
test "increments count" do
|
202
|
+
component = CounterComponent.new
|
203
|
+
assert_equal 0, component.count
|
204
|
+
component.update([:increment])
|
205
|
+
assert_equal 1, component.count
|
206
|
+
end
|
207
|
+
end
|
208
|
+
```
|
209
|
+
|
210
|
+
## Troubleshooting
|
211
|
+
|
212
|
+
Common issues and their solutions:
|
213
|
+
|
214
|
+
1. **Component not updating**: Ensure that your `update` method is correctly handling the event and modifying the state.
|
215
|
+
2. **WebSocket connection failing**: Check your ActionCable configuration and ensure that your server supports WebSocket connections.
|
216
|
+
3. **JavaScript errors**: Make sure you've correctly set up the TurboLive JavaScript integration in your application.
|
217
|
+
|
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
|
-
|
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,23 @@
|
|
1
|
+
class CountdownComponent < TurboLive::Component
|
2
|
+
state :countdown, Integer
|
3
|
+
|
4
|
+
def view
|
5
|
+
div do
|
6
|
+
if countdown.nil?
|
7
|
+
button(**on(click: :start)) { "Start!" }
|
8
|
+
else
|
9
|
+
h1 { countdown }
|
10
|
+
every(1000, :countdown) if countdown >= 1
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def update(input)
|
16
|
+
case input
|
17
|
+
in [:countdown]
|
18
|
+
self.countdown -= 1
|
19
|
+
in [:start]
|
20
|
+
self.countdown = 1000
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class CounterComponent < TurboLive::Component
|
2
|
+
state :count, Integer do |value|
|
3
|
+
value || 0
|
4
|
+
end
|
5
|
+
|
6
|
+
def view
|
7
|
+
div do
|
8
|
+
button(**on(click: :increment)) { "+" }
|
9
|
+
span { " Clicked: #{count} " }
|
10
|
+
button(**on(click: :decrement)) { "-" }
|
11
|
+
plain " "
|
12
|
+
button(**on(click: :reset)) { "Reset" }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def update(input)
|
17
|
+
case input
|
18
|
+
in [:decrement]
|
19
|
+
self.count -= 1
|
20
|
+
in [:increment]
|
21
|
+
self.count += 1
|
22
|
+
in [:reset]
|
23
|
+
self.count = 0
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class ShowcaseComponent < TurboLive::Component
|
2
|
+
state :component, Symbol
|
3
|
+
|
4
|
+
def view
|
5
|
+
div class: "container" do
|
6
|
+
div class: "left-column" do
|
7
|
+
h2 { "Components" }
|
8
|
+
ul do
|
9
|
+
li { button(**on(click: [:change_component, :counter])) { "Counter" } }
|
10
|
+
li { button(**on(click: [:change_component, :countdown])) { "Countdown" } }
|
11
|
+
li { button(**on(click: [:change_component, :tic_tac_toe])) { "TicTacToe" } }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
div class: "right-column" do
|
15
|
+
div class: "card" do
|
16
|
+
render selected_component.new
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def update(input)
|
23
|
+
case input
|
24
|
+
in [[:change_component, component]]
|
25
|
+
self.component = component
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def selected_component
|
32
|
+
case component
|
33
|
+
when :countdown
|
34
|
+
CountdownComponent
|
35
|
+
when :tic_tac_toe
|
36
|
+
TicTacToeComponent
|
37
|
+
else
|
38
|
+
CounterComponent
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
class TicTacToeComponent < TurboLive::Component
|
2
|
+
state :board, Array do |value|
|
3
|
+
value || Array.new(9, nil)
|
4
|
+
end
|
5
|
+
|
6
|
+
state :current_player, String do |value|
|
7
|
+
value || "X"
|
8
|
+
end
|
9
|
+
|
10
|
+
state :winner, String do |value|
|
11
|
+
value || nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def view
|
15
|
+
div(class: "tic-tac-toe-container") do
|
16
|
+
render_board
|
17
|
+
render_status
|
18
|
+
render_reset_button
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def render_board
|
23
|
+
div(class: "board") do
|
24
|
+
board.each_with_index do |cell, index|
|
25
|
+
button(
|
26
|
+
class: "cell",
|
27
|
+
**on(click: [:make_move, index]),
|
28
|
+
disabled: cell || winner,
|
29
|
+
"data-value": cell
|
30
|
+
) { cell || " ".html_safe }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def render_status
|
36
|
+
div(class: "status") do
|
37
|
+
if winner
|
38
|
+
"Winner: #{winner}"
|
39
|
+
elsif board.compact.length == 9
|
40
|
+
"It's a draw!"
|
41
|
+
else
|
42
|
+
"Current player: #{current_player}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def render_reset_button
|
48
|
+
button(**on(click: :reset_game)) { "Reset Game" }
|
49
|
+
end
|
50
|
+
|
51
|
+
def update(input)
|
52
|
+
case input
|
53
|
+
in [[:make_move, index]]
|
54
|
+
make_move(index)
|
55
|
+
in [:reset_game]
|
56
|
+
reset_game
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def make_move(index)
|
63
|
+
return if board[index] || winner
|
64
|
+
|
65
|
+
new_board = board.dup
|
66
|
+
new_board[index] = current_player
|
67
|
+
self.board = new_board
|
68
|
+
self.winner = check_winner
|
69
|
+
self.current_player = (current_player == "X") ? "O" : "X" unless winner
|
70
|
+
end
|
71
|
+
|
72
|
+
def check_winner
|
73
|
+
winning_combinations = [
|
74
|
+
[0, 1, 2], [3, 4, 5], [6, 7, 8], # Rows
|
75
|
+
[0, 3, 6], [1, 4, 7], [2, 5, 8], # Columns
|
76
|
+
[0, 4, 8], [2, 4, 6] # Diagonals
|
77
|
+
]
|
78
|
+
|
79
|
+
winning_combinations.each do |combo|
|
80
|
+
if board[combo[0]] && board[combo[0]] == board[combo[1]] && board[combo[0]] == board[combo[2]]
|
81
|
+
return board[combo[0]]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
nil
|
86
|
+
end
|
87
|
+
|
88
|
+
def reset_game
|
89
|
+
self.board = Array.new(9, nil)
|
90
|
+
self.current_player = "X"
|
91
|
+
self.winner = nil
|
92
|
+
end
|
93
|
+
end
|
data/lib/turbo_live/version.rb
CHANGED
data/package-lock.json
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
{
|
2
2
|
"name": "@radioactive-labs/turbo-live",
|
3
|
-
"version": "0.1.
|
3
|
+
"version": "0.1.2",
|
4
4
|
"lockfileVersion": 3,
|
5
5
|
"requires": true,
|
6
6
|
"packages": {
|
7
7
|
"": {
|
8
8
|
"name": "@radioactive-labs/turbo-live",
|
9
|
-
"version": "0.1.
|
9
|
+
"version": "0.1.2",
|
10
10
|
"license": "MIT",
|
11
11
|
"dependencies": {
|
12
12
|
"@hotwired/stimulus": "^3.2.2",
|
data/package.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
{
|
2
2
|
"name": "@radioactive-labs/turbo-live",
|
3
|
-
"version": "0.1.
|
4
|
-
"description": "
|
3
|
+
"version": "0.1.2",
|
4
|
+
"description": "Async, progressively enhanced, live components for Ruby applications that work over Websockets and HTTP.",
|
5
5
|
"type": "module",
|
6
6
|
"main": "src/js/core.js",
|
7
7
|
"files": [
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: turbo_live
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- TheDumbTechGuy
|
@@ -38,8 +38,8 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
|
-
description:
|
42
|
-
and HTTP.
|
41
|
+
description: Async, progressively enhanced, live components for Ruby applications
|
42
|
+
that work over Websockets and HTTP.
|
43
43
|
email:
|
44
44
|
- sfroelich01@gmail.com
|
45
45
|
executables: []
|
@@ -55,6 +55,10 @@ files:
|
|
55
55
|
- app/channels/components_channel.rb
|
56
56
|
- app/controllers/turbo_live/components_controller.rb
|
57
57
|
- config/routes.rb
|
58
|
+
- examples/countdown_component.rb
|
59
|
+
- examples/counter_component.rb
|
60
|
+
- examples/showcase_component.rb
|
61
|
+
- examples/tic_tac_toe_component.rb
|
58
62
|
- lib/turbo_live.rb
|
59
63
|
- lib/turbo_live/component.rb
|
60
64
|
- lib/turbo_live/engine.rb
|
@@ -95,5 +99,5 @@ requirements: []
|
|
95
99
|
rubygems_version: 3.5.16
|
96
100
|
signing_key:
|
97
101
|
specification_version: 4
|
98
|
-
summary:
|
102
|
+
summary: Async, progressively enhanced, live components for Ruby applications
|
99
103
|
test_files: []
|