turbo_live 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|