react.rb 0.0.1
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 +7 -0
- data/.gitignore +30 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +48 -0
- data/LICENSE +19 -0
- data/README.md +166 -0
- data/config.ru +15 -0
- data/example/react-tutorial/Gemfile +6 -0
- data/example/react-tutorial/Gemfile.lock +45 -0
- data/example/react-tutorial/README.md +8 -0
- data/example/react-tutorial/_comments.json +6 -0
- data/example/react-tutorial/config.ru +55 -0
- data/example/react-tutorial/example.rb +104 -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/react.rb +16 -0
- data/lib/react/api.rb +95 -0
- data/lib/react/callbacks.rb +35 -0
- data/lib/react/component.rb +197 -0
- data/lib/react/element.rb +63 -0
- data/lib/react/event.rb +76 -0
- data/lib/react/ext/hash.rb +9 -0
- data/lib/react/ext/string.rb +8 -0
- data/lib/react/top_level.rb +50 -0
- data/lib/react/validator.rb +65 -0
- data/lib/react/version.rb +3 -0
- data/react.rb.gemspec +24 -0
- data/spec/callbacks_spec.rb +107 -0
- data/spec/component_spec.rb +556 -0
- data/spec/element_spec.rb +60 -0
- data/spec/event_spec.rb +22 -0
- data/spec/react_spec.rb +168 -0
- data/spec/reactjs/index.html.erb +10 -0
- data/spec/spec_helper.rb +29 -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 +189 -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
|