reactive-ruby 0.7.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +30 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +53 -0
- data/LICENSE +19 -0
- data/README.md +303 -0
- data/config.ru +15 -0
- data/example/examples/Gemfile +7 -0
- data/example/examples/Gemfile.lock +45 -0
- data/example/examples/config.ru +44 -0
- data/example/examples/hello.js.rb +43 -0
- data/example/react-tutorial/Gemfile +7 -0
- data/example/react-tutorial/Gemfile.lock +49 -0
- data/example/react-tutorial/README.md +8 -0
- data/example/react-tutorial/_comments.json +14 -0
- data/example/react-tutorial/config.ru +63 -0
- data/example/react-tutorial/example.js.rb +290 -0
- data/example/react-tutorial/public/base.css +62 -0
- data/example/todos/Gemfile +11 -0
- data/example/todos/Gemfile.lock +84 -0
- data/example/todos/README.md +37 -0
- data/example/todos/Rakefile +8 -0
- data/example/todos/app/application.rb +22 -0
- data/example/todos/app/components/app.react.rb +61 -0
- data/example/todos/app/components/footer.react.rb +31 -0
- data/example/todos/app/components/todo_item.react.rb +46 -0
- data/example/todos/app/components/todo_list.react.rb +25 -0
- data/example/todos/app/models/todo.rb +19 -0
- data/example/todos/config.ru +14 -0
- data/example/todos/index.html.haml +16 -0
- data/example/todos/spec/todo_spec.rb +28 -0
- data/example/todos/vendor/base.css +410 -0
- data/example/todos/vendor/bg.png +0 -0
- data/example/todos/vendor/jquery.js +4 -0
- data/lib/rails-helpers/react_component.rb +32 -0
- data/lib/reactive-ruby.rb +23 -0
- data/lib/reactive-ruby/api.rb +177 -0
- data/lib/reactive-ruby/callbacks.rb +35 -0
- data/lib/reactive-ruby/component.rb +411 -0
- data/lib/reactive-ruby/element.rb +87 -0
- data/lib/reactive-ruby/event.rb +76 -0
- data/lib/reactive-ruby/ext/hash.rb +9 -0
- data/lib/reactive-ruby/ext/string.rb +8 -0
- data/lib/reactive-ruby/isomorphic_helpers.rb +223 -0
- data/lib/reactive-ruby/observable.rb +33 -0
- data/lib/reactive-ruby/rendering_context.rb +91 -0
- data/lib/reactive-ruby/serializers.rb +15 -0
- data/lib/reactive-ruby/state.rb +90 -0
- data/lib/reactive-ruby/top_level.rb +53 -0
- data/lib/reactive-ruby/validator.rb +83 -0
- data/lib/reactive-ruby/version.rb +3 -0
- data/logo1.png +0 -0
- data/logo2.png +0 -0
- data/logo3.png +0 -0
- data/reactive-ruby.gemspec +25 -0
- data/spec/callbacks_spec.rb +107 -0
- data/spec/component_spec.rb +597 -0
- data/spec/element_spec.rb +60 -0
- data/spec/event_spec.rb +22 -0
- data/spec/react_spec.rb +209 -0
- data/spec/reactjs/index.html.erb +11 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/tutorial/tutorial_spec.rb +37 -0
- data/spec/validator_spec.rb +79 -0
- data/vendor/active_support/core_ext/array/extract_options.rb +29 -0
- data/vendor/active_support/core_ext/class/attribute.rb +127 -0
- data/vendor/active_support/core_ext/kernel/singleton_class.rb +13 -0
- data/vendor/active_support/core_ext/module/remove_method.rb +11 -0
- metadata +205 -0
@@ -0,0 +1,62 @@
|
|
1
|
+
body {
|
2
|
+
background: #fff;
|
3
|
+
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;;
|
4
|
+
font-size: 15px;
|
5
|
+
line-height: 1.7;
|
6
|
+
margin: 0;
|
7
|
+
padding: 30px;
|
8
|
+
}
|
9
|
+
|
10
|
+
a {
|
11
|
+
color: #4183c4;
|
12
|
+
text-decoration: none;
|
13
|
+
}
|
14
|
+
|
15
|
+
a:hover {
|
16
|
+
text-decoration: underline;
|
17
|
+
}
|
18
|
+
|
19
|
+
code {
|
20
|
+
background-color: #f8f8f8;
|
21
|
+
border: 1px solid #ddd;
|
22
|
+
border-radius: 3px;
|
23
|
+
font-family: "Bitstream Vera Sans Mono", Consolas, Courier, monospace;
|
24
|
+
font-size: 12px;
|
25
|
+
margin: 0 2px;
|
26
|
+
padding: 0px 5px;
|
27
|
+
}
|
28
|
+
|
29
|
+
h1, h2, h3, h4 {
|
30
|
+
font-weight: bold;
|
31
|
+
margin: 0 0 15px;
|
32
|
+
padding: 0;
|
33
|
+
}
|
34
|
+
|
35
|
+
h1 {
|
36
|
+
border-bottom: 1px solid #ddd;
|
37
|
+
font-size: 2.5em;
|
38
|
+
font-weight: bold;
|
39
|
+
margin: 0 0 15px;
|
40
|
+
padding: 0;
|
41
|
+
}
|
42
|
+
|
43
|
+
h2 {
|
44
|
+
border-bottom: 1px solid #eee;
|
45
|
+
font-size: 2em;
|
46
|
+
}
|
47
|
+
|
48
|
+
h3 {
|
49
|
+
font-size: 1.5em;
|
50
|
+
}
|
51
|
+
|
52
|
+
h4 {
|
53
|
+
font-size: 1.2em;
|
54
|
+
}
|
55
|
+
|
56
|
+
p, ul {
|
57
|
+
margin: 15px 0;
|
58
|
+
}
|
59
|
+
|
60
|
+
ul {
|
61
|
+
padding-left: 30px;
|
62
|
+
}
|
@@ -0,0 +1,11 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gem 'rake'
|
4
|
+
gem 'opal', :github => 'opal/opal', :ref => '85220f32136c74ac93f1cb721462324a3423cf44'
|
5
|
+
gem 'opal-jquery', :github => 'opal/opal-jquery'
|
6
|
+
gem 'vienna', :github => 'opal/vienna', :ref => '593335cbd7fb99ce471fa720e9b9c849d99b8dda'
|
7
|
+
gem 'opal-haml', :github => 'opal/opal-haml'
|
8
|
+
gem 'opal-rspec', '0.3.0.beta2'
|
9
|
+
gem 'react.rb', :path => "../.."
|
10
|
+
gem 'thin'
|
11
|
+
gem 'react-source'
|
@@ -0,0 +1,84 @@
|
|
1
|
+
GIT
|
2
|
+
remote: git://github.com/opal/opal-haml.git
|
3
|
+
revision: 0bdd3eb53ec03d380e14440a94f779ed7c3741e1
|
4
|
+
specs:
|
5
|
+
opal-haml (0.2.0)
|
6
|
+
haml
|
7
|
+
opal (>= 0.5.0, < 1.0.0)
|
8
|
+
|
9
|
+
GIT
|
10
|
+
remote: git://github.com/opal/opal-jquery.git
|
11
|
+
revision: 1814202085f168176231b877b2b7a967b75b0726
|
12
|
+
specs:
|
13
|
+
opal-jquery (0.1.2)
|
14
|
+
opal (>= 0.5.0, < 1.0.0)
|
15
|
+
|
16
|
+
GIT
|
17
|
+
remote: git://github.com/opal/opal.git
|
18
|
+
revision: 85220f32136c74ac93f1cb721462324a3423cf44
|
19
|
+
ref: 85220f32136c74ac93f1cb721462324a3423cf44
|
20
|
+
specs:
|
21
|
+
opal (0.6.0)
|
22
|
+
source_map
|
23
|
+
sprockets
|
24
|
+
|
25
|
+
GIT
|
26
|
+
remote: git://github.com/opal/vienna.git
|
27
|
+
revision: 593335cbd7fb99ce471fa720e9b9c849d99b8dda
|
28
|
+
ref: 593335cbd7fb99ce471fa720e9b9c849d99b8dda
|
29
|
+
specs:
|
30
|
+
vienna (0.0.2)
|
31
|
+
opal (>= 0.5.0, < 1.0.0)
|
32
|
+
opal-activesupport
|
33
|
+
opal-jquery
|
34
|
+
|
35
|
+
PATH
|
36
|
+
remote: ../..
|
37
|
+
specs:
|
38
|
+
react.rb (0.0.1)
|
39
|
+
opal (~> 0.6.0)
|
40
|
+
opal-activesupport
|
41
|
+
|
42
|
+
GEM
|
43
|
+
remote: https://rubygems.org/
|
44
|
+
specs:
|
45
|
+
daemons (1.1.9)
|
46
|
+
eventmachine (1.0.3)
|
47
|
+
haml (4.0.5)
|
48
|
+
tilt
|
49
|
+
hike (1.2.3)
|
50
|
+
json (1.8.2)
|
51
|
+
multi_json (1.10.1)
|
52
|
+
opal-activesupport (0.1.0)
|
53
|
+
opal (>= 0.5.0, < 1.0.0)
|
54
|
+
opal-rspec (0.3.0.beta2)
|
55
|
+
opal (>= 0.6.0, < 1.0.0)
|
56
|
+
rack (1.5.2)
|
57
|
+
rake (10.1.1)
|
58
|
+
react-source (0.12.2)
|
59
|
+
source_map (3.0.1)
|
60
|
+
json
|
61
|
+
sprockets (2.12.3)
|
62
|
+
hike (~> 1.2)
|
63
|
+
multi_json (~> 1.0)
|
64
|
+
rack (~> 1.0)
|
65
|
+
tilt (~> 1.1, != 1.3.0)
|
66
|
+
thin (1.6.2)
|
67
|
+
daemons (>= 1.0.9)
|
68
|
+
eventmachine (>= 1.0.0)
|
69
|
+
rack (>= 1.0.0)
|
70
|
+
tilt (1.4.1)
|
71
|
+
|
72
|
+
PLATFORMS
|
73
|
+
ruby
|
74
|
+
|
75
|
+
DEPENDENCIES
|
76
|
+
opal!
|
77
|
+
opal-haml!
|
78
|
+
opal-jquery!
|
79
|
+
opal-rspec (= 0.3.0.beta2)
|
80
|
+
rake
|
81
|
+
react-source
|
82
|
+
react.rb!
|
83
|
+
thin
|
84
|
+
vienna!
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# react.rb-todos
|
2
|
+
|
3
|
+
Modify from original version of [Opal-Todos](https://github.com/opal/opal-todos)
|
4
|
+
|
5
|
+
## Running
|
6
|
+
|
7
|
+
Get dependencies:
|
8
|
+
|
9
|
+
```
|
10
|
+
$ bundle install
|
11
|
+
```
|
12
|
+
|
13
|
+
Run the sprockets based server for auto-compiling:
|
14
|
+
|
15
|
+
```
|
16
|
+
$ bundle exec rackup
|
17
|
+
```
|
18
|
+
|
19
|
+
Open `http://localhost:9292` in the browser.
|
20
|
+
|
21
|
+
## Code Overview
|
22
|
+
|
23
|
+
Opal comes with sprockets support built in, so using rack we can have an
|
24
|
+
easy to boot build system to handle all opal dependencies. If you look
|
25
|
+
in `index.html.erb`, you will see a call to `javascript_include_tag`
|
26
|
+
which acts just like the rails tag helper. This will include our
|
27
|
+
`application.rb` file, and all of its dependencies. Each file will be included
|
28
|
+
in a seperate `<script>..</script>` tag to make navigating the code in a
|
29
|
+
web browser easier.
|
30
|
+
|
31
|
+
### Router
|
32
|
+
|
33
|
+
`TodoAppView` use router provided by [Vienna](https://github.com/opal/vienna)
|
34
|
+
|
35
|
+
### Model
|
36
|
+
|
37
|
+
Use model layer provided by [Vienna](https://github.com/opal/vienna) which provide basic support of local storage and update notification.
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'opal'
|
2
|
+
require 'jquery'
|
3
|
+
require 'opal-jquery'
|
4
|
+
require 'opal-haml'
|
5
|
+
require 'vienna'
|
6
|
+
require "react"
|
7
|
+
|
8
|
+
require 'models/todo'
|
9
|
+
|
10
|
+
require "components/app.react"
|
11
|
+
|
12
|
+
Document.ready? do
|
13
|
+
element = React.create_element(TodoAppView, filter: "all")
|
14
|
+
component = React.render(element, Element.find('#todoapp').get(0))
|
15
|
+
|
16
|
+
Vienna::Router.new.tap do |router|
|
17
|
+
router.route('/:filter') do |params|
|
18
|
+
component.set_props(filter: params[:filter].empty? ? "all" : params[:filter])
|
19
|
+
end
|
20
|
+
end.update
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require "components/footer.react"
|
2
|
+
require "components/todo_item.react"
|
3
|
+
require "components/todo_list.react"
|
4
|
+
|
5
|
+
class TodoAppView
|
6
|
+
include React::Component
|
7
|
+
|
8
|
+
KEY_ENTER = 13
|
9
|
+
|
10
|
+
params do
|
11
|
+
requires :filter, values: ["all", "active", "completed"]
|
12
|
+
end
|
13
|
+
|
14
|
+
define_state(:todos) { [] }
|
15
|
+
|
16
|
+
before_mount do
|
17
|
+
Todo.on(:create) { Todo.adapter.sync_models(Todo); reload_current_filter }
|
18
|
+
Todo.on(:update) { Todo.adapter.sync_models(Todo); reload_current_filter }
|
19
|
+
Todo.on(:destroy) { Todo.adapter.sync_models(Todo); reload_current_filter }
|
20
|
+
end
|
21
|
+
|
22
|
+
before_receive_props do |next_props|
|
23
|
+
apply_filter next_props[:filter]
|
24
|
+
end
|
25
|
+
|
26
|
+
def reload_current_filter
|
27
|
+
apply_filter(params[:filter])
|
28
|
+
end
|
29
|
+
|
30
|
+
def apply_filter(filter)
|
31
|
+
Todo.adapter.find_all(Todo) do |models|
|
32
|
+
case filter
|
33
|
+
when "all"
|
34
|
+
self.todos = models
|
35
|
+
when "active"
|
36
|
+
self.todos = models.reject(&:completed)
|
37
|
+
when "completed"
|
38
|
+
self.todos = models.select(&:completed)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def handle_keydown(event)
|
44
|
+
if event.key_code == KEY_ENTER
|
45
|
+
value = event.target.value.strip
|
46
|
+
Todo.create title: value, completed: false
|
47
|
+
event.target.value = ""
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def render
|
52
|
+
div do
|
53
|
+
header(id: "header") do
|
54
|
+
h1 { "Todos" }
|
55
|
+
input(id: "new-todo", placeholder: "What needs to be done?").on(:key_down) { |e| handle_keydown(e) }
|
56
|
+
end
|
57
|
+
present TodoList, todos: self.todos
|
58
|
+
present Footer, selected_filter: params[:filter]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class Footer
|
2
|
+
include React::Component
|
3
|
+
|
4
|
+
def clear_completed
|
5
|
+
Todo.completed.each { |t| t.destroy }
|
6
|
+
end
|
7
|
+
|
8
|
+
def render
|
9
|
+
footer(id: "footer") do
|
10
|
+
span(id: "todo-count") do
|
11
|
+
strong { Todo.active.size }
|
12
|
+
span { Todo.active.size == 1 ? ' item left' : ' items left' }
|
13
|
+
end
|
14
|
+
|
15
|
+
ul(id: "filters") do
|
16
|
+
filters = [{href: "#/", filter: "all"},
|
17
|
+
{href: "#/active", filter: "active"},
|
18
|
+
{href: "#/completed", filter: "completed"}]
|
19
|
+
filters.map do |item|
|
20
|
+
li { a(href: item[:href], class_name: {selected: params[:selected_filter] == item[:filter]}) { item[:filter].capitalize } }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
completed = Todo.completed.size
|
25
|
+
|
26
|
+
if completed > 0
|
27
|
+
button(id: "clear-completed") { "Clear completed (#{completed})" }.on(:click) { clear_completed }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class TodoItem
|
2
|
+
include React::Component
|
3
|
+
KEY_ENTER = 13
|
4
|
+
|
5
|
+
define_state(:editing) { false }
|
6
|
+
define_state(:edit_text)
|
7
|
+
|
8
|
+
before_mount :set_up
|
9
|
+
|
10
|
+
def set_up
|
11
|
+
self.edit_text = params[:todo].title
|
12
|
+
end
|
13
|
+
|
14
|
+
def finish_editing
|
15
|
+
self.editing = false
|
16
|
+
new_value = self.edit_text.strip
|
17
|
+
if new_value.empty?
|
18
|
+
params[:todo].destroy
|
19
|
+
else
|
20
|
+
params[:todo].update(title: new_value)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def render
|
25
|
+
li(class_name: {editing: self.editing}) do
|
26
|
+
div(class_name: 'view') do
|
27
|
+
input(class_name: "toggle", type: "checkbox", checked: params[:todo].completed).on(:click) do
|
28
|
+
todo = params[:todo]
|
29
|
+
todo.update(:completed => !todo.completed)
|
30
|
+
end
|
31
|
+
label { self.edit_text }.on(:double_click) do
|
32
|
+
# set on state will trigger re-render, so we manipulate the DOM after render done
|
33
|
+
self.set_state(editing: true) do
|
34
|
+
self.refs[:input].dom_node.focus
|
35
|
+
end
|
36
|
+
self.edit_text = params[:todo].title
|
37
|
+
end
|
38
|
+
button(class_name: "destroy").on(:click) { params[:todo].destroy }
|
39
|
+
end
|
40
|
+
input(class_name: "edit", value: self.edit_text, ref: :input)
|
41
|
+
.on(:blur) { finish_editing }
|
42
|
+
.on(:change) {|e| self.edit_text = e.target.value }
|
43
|
+
.on(:key_down) { |e| finish_editing if (e.key_code == KEY_ENTER) }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class TodoList
|
2
|
+
include React::Component
|
3
|
+
|
4
|
+
def toggle_all
|
5
|
+
distinct_status = Todo.all.map {|t| t.completed }.uniq
|
6
|
+
|
7
|
+
if distinct_status.count == 1
|
8
|
+
Todo.all.each {|t| t.update(:completed => !distinct_status[0]) }
|
9
|
+
else # toggle all as completed
|
10
|
+
Todo.all.each {|t| t.update(:completed => true) }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def render
|
15
|
+
section(id: "main") do
|
16
|
+
input(id: "toggle-all", type: "checkbox").on(:click) { toggle_all }
|
17
|
+
label(htmlFor: "toggle-all") { "Mark all as complete" }
|
18
|
+
ul(id: "todo-list") do
|
19
|
+
params[:todos].map do |todo|
|
20
|
+
present TodoItem, todo: todo , key: todo.id
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'vienna/adapters/local'
|
2
|
+
|
3
|
+
class Todo < Vienna::Model
|
4
|
+
adapter Vienna::LocalAdapter
|
5
|
+
|
6
|
+
attributes :title, :completed
|
7
|
+
|
8
|
+
alias completed? completed
|
9
|
+
|
10
|
+
# All active (not completed) todos
|
11
|
+
def self.active
|
12
|
+
all.reject(&:completed)
|
13
|
+
end
|
14
|
+
|
15
|
+
# All completed todos
|
16
|
+
def self.completed
|
17
|
+
all.select(&:completed)
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler.require
|
3
|
+
|
4
|
+
require "react/source"
|
5
|
+
|
6
|
+
run Opal::Server.new { |s|
|
7
|
+
s.append_path 'app'
|
8
|
+
s.append_path 'vendor'
|
9
|
+
s.append_path File.dirname(::React::Source.bundled_path_for("react-with-addons.js"))
|
10
|
+
|
11
|
+
s.debug = true
|
12
|
+
s.main = 'application'
|
13
|
+
s.index_path = 'index.html.haml'
|
14
|
+
}
|
@@ -0,0 +1,16 @@
|
|
1
|
+
!!!
|
2
|
+
%html(lang="en")
|
3
|
+
%head
|
4
|
+
%meta(charset="utf-8")
|
5
|
+
%meta(http-equiv="X-UA-Compatible" content="IE=edge,chrome=1")
|
6
|
+
%link(rel="stylesheet" href="/vendor/base.css")
|
7
|
+
= javascript_include_tag 'react-with-addons.min.js'
|
8
|
+
= javascript_include_tag 'application'
|
9
|
+
|
10
|
+
%body
|
11
|
+
%section#todoapp
|
12
|
+
#info
|
13
|
+
%p Double-click to edit a todo
|
14
|
+
%p
|
15
|
+
Part of
|
16
|
+
%a(href="http://todomvc.com") TodoMVC
|