reactive-ruby 0.7.3
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 +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,45 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ../..
|
3
|
+
specs:
|
4
|
+
opal-react (0.1.1)
|
5
|
+
opal
|
6
|
+
opal-activesupport
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: http://rubygems.org/
|
10
|
+
specs:
|
11
|
+
hike (1.2.3)
|
12
|
+
opal (0.8.0)
|
13
|
+
hike (~> 1.2)
|
14
|
+
sourcemap (~> 0.1.0)
|
15
|
+
sprockets (~> 3.1)
|
16
|
+
tilt (>= 1.4)
|
17
|
+
opal-activesupport (0.1.0)
|
18
|
+
opal (>= 0.5.0, < 1.0.0)
|
19
|
+
opal-jquery (0.4.0)
|
20
|
+
opal (>= 0.7.0, < 0.9.0)
|
21
|
+
rack (1.6.4)
|
22
|
+
rack-protection (1.5.3)
|
23
|
+
rack
|
24
|
+
react-source (0.13.3)
|
25
|
+
sinatra (1.4.6)
|
26
|
+
rack (~> 1.4)
|
27
|
+
rack-protection (~> 1.4)
|
28
|
+
tilt (>= 1.3, < 3)
|
29
|
+
sourcemap (0.1.1)
|
30
|
+
sprockets (3.2.0)
|
31
|
+
rack (~> 1.0)
|
32
|
+
tilt (2.0.1)
|
33
|
+
|
34
|
+
PLATFORMS
|
35
|
+
ruby
|
36
|
+
|
37
|
+
DEPENDENCIES
|
38
|
+
opal
|
39
|
+
opal-jquery
|
40
|
+
opal-react!
|
41
|
+
react-source
|
42
|
+
sinatra
|
43
|
+
|
44
|
+
BUNDLED WITH
|
45
|
+
1.10.2
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# config.ru
|
2
|
+
require 'bundler'
|
3
|
+
Bundler.require
|
4
|
+
|
5
|
+
require "react/source"
|
6
|
+
|
7
|
+
Opal::Processor.source_map_enabled = true
|
8
|
+
|
9
|
+
opal = Opal::Server.new {|s|
|
10
|
+
s.append_path './'
|
11
|
+
s.append_path File.dirname(::React::Source.bundled_path_for("react-with-addons.js"))
|
12
|
+
s.main = 'example'
|
13
|
+
s.debug = true
|
14
|
+
}
|
15
|
+
|
16
|
+
map opal.source_maps.prefix do
|
17
|
+
run opal.source_maps
|
18
|
+
end rescue nil
|
19
|
+
|
20
|
+
map '/assets' do
|
21
|
+
run opal.sprockets
|
22
|
+
end
|
23
|
+
|
24
|
+
get '/example/:example' do
|
25
|
+
example = params[:example]
|
26
|
+
<<-HTML
|
27
|
+
<!doctype html>
|
28
|
+
<html>
|
29
|
+
<head>
|
30
|
+
<title>Example: #{example}.rb</title>
|
31
|
+
<script src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
|
32
|
+
<script src="http://cdnjs.cloudflare.com/ajax/libs/showdown/0.3.1/showdown.min.js"></script>
|
33
|
+
<script src="/assets/react-with-addons.min.js"></script>
|
34
|
+
<script src="/assets/#{example}.js"></script>
|
35
|
+
<script>#{Opal::Processor.load_asset_code(opal.sprockets, example+".js")}</script>
|
36
|
+
</head>
|
37
|
+
<body>
|
38
|
+
<div id="content"></div>
|
39
|
+
</body>
|
40
|
+
</html>
|
41
|
+
HTML
|
42
|
+
end
|
43
|
+
|
44
|
+
run Sinatra::Application
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'opal'
|
2
|
+
require 'opal-react'
|
3
|
+
|
4
|
+
class HelloMessage
|
5
|
+
|
6
|
+
include React::Component # will create a new component named HelloMessage
|
7
|
+
|
8
|
+
MSG = {great: 'Cool!', bad: 'Cheer up!'}
|
9
|
+
|
10
|
+
optional_param :mood
|
11
|
+
required_param :name
|
12
|
+
define_state :foo, "Default greeting"
|
13
|
+
|
14
|
+
before_mount do
|
15
|
+
foo! "#{name}: #{MSG[mood]}" if mood # change the state of foo using foo!, read the state using foo
|
16
|
+
end
|
17
|
+
|
18
|
+
after_mount :log # notice the two forms of callback
|
19
|
+
|
20
|
+
def log
|
21
|
+
puts "mounted!"
|
22
|
+
end
|
23
|
+
|
24
|
+
def render # render method MUST return just one component
|
25
|
+
div do # basic dsl syntax component_name(options) { ...children... }
|
26
|
+
span { "#{foo} #{name}!" } # all html5 components are defined with lower case text
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
class App
|
33
|
+
include React::Component
|
34
|
+
|
35
|
+
def render
|
36
|
+
HelloMessage name: 'John', mood: :great # new components are accessed via the class name
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# later we will talk about nicer ways to do this: For now wait till doc is loaded
|
41
|
+
# then tell React to create an "App" and render it into the document body.
|
42
|
+
|
43
|
+
`window.onload = #{lambda {React.render(React.create_element(App), `document.body`)}}`
|
@@ -0,0 +1,49 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ../..
|
3
|
+
specs:
|
4
|
+
opal-react (0.2.1)
|
5
|
+
opal
|
6
|
+
opal-activesupport
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
hike (1.2.3)
|
12
|
+
opal (0.8.0)
|
13
|
+
hike (~> 1.2)
|
14
|
+
sourcemap (~> 0.1.0)
|
15
|
+
sprockets (~> 3.1)
|
16
|
+
tilt (>= 1.4)
|
17
|
+
opal-activesupport (0.1.0)
|
18
|
+
opal (>= 0.5.0, < 1.0.0)
|
19
|
+
opal-browser (0.1.0.beta1)
|
20
|
+
opal (>= 0.5.5)
|
21
|
+
paggio
|
22
|
+
opal-jquery (0.4.0)
|
23
|
+
opal (>= 0.7.0, < 0.9.0)
|
24
|
+
paggio (0.2.4)
|
25
|
+
rack (1.6.4)
|
26
|
+
rack-protection (1.5.3)
|
27
|
+
rack
|
28
|
+
react-source (0.13.3)
|
29
|
+
sinatra (1.4.6)
|
30
|
+
rack (~> 1.4)
|
31
|
+
rack-protection (~> 1.4)
|
32
|
+
tilt (>= 1.3, < 3)
|
33
|
+
sourcemap (0.1.1)
|
34
|
+
sprockets (3.2.0)
|
35
|
+
rack (~> 1.0)
|
36
|
+
tilt (2.0.1)
|
37
|
+
|
38
|
+
PLATFORMS
|
39
|
+
ruby
|
40
|
+
|
41
|
+
DEPENDENCIES
|
42
|
+
opal-browser
|
43
|
+
opal-jquery
|
44
|
+
opal-react!
|
45
|
+
react-source
|
46
|
+
sinatra
|
47
|
+
|
48
|
+
BUNDLED WITH
|
49
|
+
1.10.2
|
@@ -0,0 +1,8 @@
|
|
1
|
+
# React Tutorial
|
2
|
+
|
3
|
+
This is a rewrite of original comment box example using React.rb from the [React tutorial](http://facebook.github.io/react/docs/tutorial.html).
|
4
|
+
|
5
|
+
## To use
|
6
|
+
|
7
|
+
1. Make sure you use Bundler, then `bundle exec rackup`
|
8
|
+
2. And visit <http://localhost:9292/>. Try opening multiple tabs!
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# config.ru
|
2
|
+
require 'bundler'
|
3
|
+
Bundler.require
|
4
|
+
|
5
|
+
require "react/source"
|
6
|
+
|
7
|
+
Opal::Processor.source_map_enabled = true
|
8
|
+
|
9
|
+
opal = Opal::Server.new {|s|
|
10
|
+
s.append_path './'
|
11
|
+
s.append_path File.dirname(::React::Source.bundled_path_for("react-with-addons.js"))
|
12
|
+
s.main = 'example'
|
13
|
+
s.debug = true
|
14
|
+
}
|
15
|
+
|
16
|
+
map opal.source_maps.prefix do
|
17
|
+
run opal.source_maps
|
18
|
+
end rescue nil
|
19
|
+
|
20
|
+
map '/assets' do
|
21
|
+
run opal.sprockets
|
22
|
+
end
|
23
|
+
|
24
|
+
get '/comments.json' do
|
25
|
+
comments = JSON.parse(open("./_comments.json").read)
|
26
|
+
JSON.generate(comments)
|
27
|
+
end
|
28
|
+
|
29
|
+
get '/comments.js' do
|
30
|
+
content_type "application/javascript"
|
31
|
+
comments = JSON.parse(open("./_comments.json").read)
|
32
|
+
"window.initial_comments = #{JSON.generate(comments)}"
|
33
|
+
end
|
34
|
+
|
35
|
+
post "/comments.json" do
|
36
|
+
comments = JSON.parse(open("./_comments.json").read)
|
37
|
+
comments << JSON.parse(request.body.read)
|
38
|
+
File.write('./_comments.json', JSON.pretty_generate(comments, :indent => ' '))
|
39
|
+
JSON.generate(comments)
|
40
|
+
end
|
41
|
+
|
42
|
+
get '/' do
|
43
|
+
<<-HTML
|
44
|
+
<!doctype html>
|
45
|
+
<html>
|
46
|
+
<head>
|
47
|
+
<title>Hello React</title>
|
48
|
+
<link rel="stylesheet" href="base.css" />
|
49
|
+
<script src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
|
50
|
+
<script src="http://cdnjs.cloudflare.com/ajax/libs/showdown/0.3.1/showdown.min.js"></script>
|
51
|
+
<script src="/assets/react-with-addons.js"></script>
|
52
|
+
<script src="/assets/example.js"></script>
|
53
|
+
<script src="/comments.js"></script>
|
54
|
+
<script>#{Opal::Processor.load_asset_code(opal.sprockets, "example.js")}</script>
|
55
|
+
</head>
|
56
|
+
<body>
|
57
|
+
<div id="content"></div>
|
58
|
+
</body>
|
59
|
+
</html>
|
60
|
+
HTML
|
61
|
+
end
|
62
|
+
|
63
|
+
run Sinatra::Application
|
@@ -0,0 +1,290 @@
|
|
1
|
+
require 'opal'
|
2
|
+
require 'browser' # gives us wrappers on javascript methods such as setTimer and setInterval
|
3
|
+
require 'opal-jquery' # gives us a nice wrapper on jQuery which we will use mainly for HTTP calls
|
4
|
+
require "json" # json conversions
|
5
|
+
require 'opal-react' # and the whole reason we are gathered here today!
|
6
|
+
|
7
|
+
Document.ready? do # Document.ready? is a opal-jquery method. The block will run when doc is loaded
|
8
|
+
|
9
|
+
# render an instance of the CommentBox component at the '#content' element.
|
10
|
+
# url and poll_interval are the initial params for this comment box
|
11
|
+
|
12
|
+
React.render(
|
13
|
+
React.create_element(
|
14
|
+
CommentBox, url: "comments.json", poll_interval: 2),
|
15
|
+
Element['#content']
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
class CommentBox
|
20
|
+
|
21
|
+
# A react component is simply a class that has a "render" method.
|
22
|
+
|
23
|
+
# But including React::Component mixin provides a nice dsl, and many other features
|
24
|
+
|
25
|
+
include React::Component
|
26
|
+
|
27
|
+
# Components can have parameters that are passed in when the component is first "mounted"
|
28
|
+
# and then updated as the application state changes. In this case url, and poll_interval will
|
29
|
+
# never change since this is the top level component.
|
30
|
+
|
31
|
+
required_param :url
|
32
|
+
required_param :poll_interval
|
33
|
+
|
34
|
+
# Components also may have internal state variables, which are like instance variables,
|
35
|
+
# with one added feature: Changing state causes a rerender to occur.
|
36
|
+
|
37
|
+
# The "comments" state is being initialized by parsing the javascript object at window.initial_comments
|
38
|
+
# This is not a react feature, but was just set up in the HTML header (see config.ru for how this was done).
|
39
|
+
|
40
|
+
define_state comments: JSON.from_object(`window.initial_comments`)
|
41
|
+
|
42
|
+
# The following call backs are made during the component lifecycle:
|
43
|
+
|
44
|
+
# before_mount before component is first rendered
|
45
|
+
# after_mount after component is first rendered, after DOM is loaded. ONLY CALLED ON CLIENT
|
46
|
+
# before_receive_props when component is being about to be rerendered by an outside state change. CANCELLABLE
|
47
|
+
# before_update just before a rerender, and not cancellable.
|
48
|
+
# after_update after DOM has been updated.
|
49
|
+
# before_unmount before component instance will be removed. Use this to kill low level handlers etc.
|
50
|
+
|
51
|
+
# just to show off how these callbacks work we have separated setting up a repeating fetch into three pieces.
|
52
|
+
|
53
|
+
# before mounting we will initialize a polling loop, but we don't want to start it yet.
|
54
|
+
|
55
|
+
before_mount do
|
56
|
+
@fetcher = every(poll_interval) do # we use the opal browser utility to call the server every poll_interval seconds
|
57
|
+
HTTP.get(url) do |response| # notice that params poll_interval, and url are accessed as instance methods
|
58
|
+
if response.ok?
|
59
|
+
comments! JSON.parse(response.body) # comments!(value) updates the state and notifies react of the state change
|
60
|
+
else
|
61
|
+
puts "failed with status #{response.status_code}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# once we have things up and displayed lets start polling for updates
|
68
|
+
|
69
|
+
after_mount do
|
70
|
+
puts "start me up!"
|
71
|
+
@fetcher.start
|
72
|
+
end
|
73
|
+
|
74
|
+
# finally our component should be a good citizen and stop the polling when its unmounted
|
75
|
+
|
76
|
+
before_unmount do
|
77
|
+
@fetcher.stop
|
78
|
+
end
|
79
|
+
|
80
|
+
# components can have their own methods like any other class
|
81
|
+
# in this case we receive a new comment and send it the server
|
82
|
+
|
83
|
+
def send_comment_to_server(comment)
|
84
|
+
HTTP.post(url, payload: comment) do |response|
|
85
|
+
puts "failed with status #{response.status_code}" unless response.ok?
|
86
|
+
end
|
87
|
+
comment
|
88
|
+
end
|
89
|
+
|
90
|
+
# every component must implement a render method. The method must generate a single
|
91
|
+
# react virtual DOM element. React compares the output of each render and determines
|
92
|
+
# the minimum actual DOM update needed.
|
93
|
+
|
94
|
+
# A very common mistake is to try generate two or more elements (or none at all.) Either case will
|
95
|
+
# throw an error. Just remember that there is already a DOM node waiting for the output of the render
|
96
|
+
# hence the need for exactly one element per render.
|
97
|
+
|
98
|
+
def render
|
99
|
+
|
100
|
+
# the dsl syntax is simply a method call, with params hash, followed by a block
|
101
|
+
# the built in dsl methods correspond to the standard HTML5 tags such as div, h1, table, tr, td, span etc.
|
102
|
+
#return div.comment { h1 {"hello"} }
|
103
|
+
div class: "commentBox" do # just like <div class="commentBox">
|
104
|
+
|
105
|
+
h1 { "Comments" } # yep just like <h1>Comments</h1>
|
106
|
+
|
107
|
+
# Custom components use their class name, as the tag. Notice that the comments state is passed to
|
108
|
+
# to the CommentList component. This is the normal React paradigm: Data flows towards the leaf nodes.
|
109
|
+
|
110
|
+
CommentList comments: comments
|
111
|
+
|
112
|
+
# Sometimes its necessary for data to move upwards, and react provides several ways to do this.
|
113
|
+
|
114
|
+
# In this case we need to know when a new comment is submitted. So we pass a callback proc.
|
115
|
+
|
116
|
+
# The callback takes the new comment and sends it to the server and then pushes it onto the comments list.
|
117
|
+
# Again the comments! method is used to signal that the state is changing. The use of the "bang" pseudo
|
118
|
+
# operator is important as the value of comments has NOT changed (its still tha same array), but its
|
119
|
+
# internal state has.
|
120
|
+
|
121
|
+
CommentForm submit_comment: lambda { |comment| comments! << send_comment_to_server(comment)}
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
# Our second component!
|
129
|
+
|
130
|
+
class CommentList
|
131
|
+
|
132
|
+
include React::Component
|
133
|
+
|
134
|
+
# As we saw above a CommentList component takes a comments parameter
|
135
|
+
# Here we introduce optional parameter type checking. The syntax [Hash] means "Array of Hashes"
|
136
|
+
# In our case each comment is a hash with an author and text key.
|
137
|
+
|
138
|
+
# Failure to match the type puts a warning on the console not an error,
|
139
|
+
# and only in development mode not production.
|
140
|
+
|
141
|
+
required_param :comments, type: Array
|
142
|
+
|
143
|
+
# This is a good place to think more about the component lifecycle. The first time
|
144
|
+
# CommentList is mounted, comments will be the initial array of author, text hashes.
|
145
|
+
# As new comments are added the component will receive new params. However the component
|
146
|
+
# does NOT reinitialize its state. If changes in state are needed as result of incoming param changes
|
147
|
+
# the before_receive_props call back can be used.
|
148
|
+
|
149
|
+
def render
|
150
|
+
|
151
|
+
# Lets render some comments - all we need to do is iterate over the comments array using the usual
|
152
|
+
# ruby "each" method.
|
153
|
+
|
154
|
+
# This is a good place to clarify how the DSL works. Notice that we use comments.each NOT comments.collect
|
155
|
+
# When a tag method (such as div, or Comment) is called its "output" is internally pushed into a render buffer.
|
156
|
+
# This simplifies the DSL by separating the control flow from the output, but can sometimes be a bit confusing.
|
157
|
+
|
158
|
+
div.commentList.and_another_class.and_another do # you can also include the class haml style (tx to @dancinglightning!)
|
159
|
+
comments.each do |comment|
|
160
|
+
# By now we are getting used to the react paradigm: Stuff comes in, is processed, and then
|
161
|
+
# passed to next lower level. In this case we pass along each author-text pair to the Comment component.
|
162
|
+
Comment author: comment[:author], text: comment[:text], hash: comment
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
|
169
|
+
# Notice that the above CommentList component had no state. Each time its parameters change, it simply re-renders.
|
170
|
+
# CommentForm does have internal state as we will see...
|
171
|
+
|
172
|
+
class CommentForm
|
173
|
+
|
174
|
+
include React::Component
|
175
|
+
|
176
|
+
# While declaring the type of a param is optional its handy not only for debug, but also to let React create
|
177
|
+
# appropriate helpers based on the type. In this case we are passing in a Proc, and so React will treat the
|
178
|
+
# "submit_comment" param specially. Instead of submit_comment returning its value (as the previous params have done)
|
179
|
+
# it will call the associated Proc, thus allow CommentForm to communicate state changes back to the parent.
|
180
|
+
|
181
|
+
required_param :submit_comment, type: Proc
|
182
|
+
|
183
|
+
# We are going to have 2 state variable. One for each field in the comment. As the user types,
|
184
|
+
# these state variables will be updating causing a rerender of the CommentForm (but no other components.)
|
185
|
+
|
186
|
+
define_state :author, :text
|
187
|
+
|
188
|
+
def render
|
189
|
+
div do
|
190
|
+
div do
|
191
|
+
|
192
|
+
"Author: ".span # Note the shorthand for span { "Author" }. You can do this with br, span, th, td, and para (for p) tags
|
193
|
+
|
194
|
+
# Now we are going to generate an input tag. Notice how the author state variable is provided. Referencing
|
195
|
+
# author is what will cause us to re-render and update the input as the value of author changes.
|
196
|
+
# React will optimize the updates so parts that are not changing will not be effected.
|
197
|
+
|
198
|
+
input.author_name(type: :text, value: author, placeholder: "Your name", style: {width: "30%"}).
|
199
|
+
# and we attach an on_change handler to the input. As the input changes we simply update author.
|
200
|
+
on(:change) { |e| author! e.target.value }
|
201
|
+
|
202
|
+
end
|
203
|
+
|
204
|
+
div do
|
205
|
+
# lets have some fun with the text. Same deal as the author except we will use a text area...
|
206
|
+
div(style: {float: :left, width: "50%"}) do
|
207
|
+
textarea(value: text, placeholder: "Say something...", style: {width: "90%"}, rows: 30).
|
208
|
+
on(:change) { |e| text! e.target.value }
|
209
|
+
end
|
210
|
+
# and lets use Showdown to allow for markdown, and display the mark down to the left of input
|
211
|
+
# we will define Showdown later, and it will be our first reusable component, as we will use it twice.
|
212
|
+
div(style: {float: :left, width: "50%"}) do
|
213
|
+
Showdown markup: text
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Finally lets give the use a button to submit changes. Why not? We have come this far!
|
218
|
+
# Notice how the submit_comment proc param allows us to be ignorant of how the update is made.
|
219
|
+
|
220
|
+
# Notice that (author! "") updates author, but returns the current value.
|
221
|
+
# This is usually the desired behavior in React as we are typically interested in state changes,
|
222
|
+
# and before/after values, not simply doing a chained update of multiple variables.
|
223
|
+
|
224
|
+
button { "Post" }.on(:click) { submit_comment :author => (author! ""), :text => (text! "") }
|
225
|
+
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Wow only two more components left! This one is a breeze. We just take the author, and text and display
|
231
|
+
# them. We already know how to use our Showdown component to display the markdown so we can just reuse that.
|
232
|
+
|
233
|
+
class Comment
|
234
|
+
|
235
|
+
include React::Component
|
236
|
+
|
237
|
+
required_param :author
|
238
|
+
required_param :text
|
239
|
+
required_param :hash, type: Hash
|
240
|
+
|
241
|
+
def render
|
242
|
+
div.comment do
|
243
|
+
h2.comment_author { author } # NOTE: single underscores in haml style class names are converted to dashes
|
244
|
+
# so comment_author becomes comment-author, but comment__author would be comment_author
|
245
|
+
# this is handy for boot strap names like col-md-push-9 which can be written as col_md_push_9
|
246
|
+
Showdown markup: text
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
end
|
251
|
+
|
252
|
+
# Last but not least here is our ShowDown Component
|
253
|
+
|
254
|
+
class Showdown
|
255
|
+
|
256
|
+
include React::Component
|
257
|
+
|
258
|
+
required_param :markup
|
259
|
+
|
260
|
+
def render
|
261
|
+
|
262
|
+
# we will use some Opal lowlevel stuff to interface to the javascript Showdown class
|
263
|
+
# we only need to build the converter once, and then reuse it so we will use a plain old
|
264
|
+
# instance variable to keep track of it.
|
265
|
+
|
266
|
+
@converter ||= Native(`new Showdown.converter()`)
|
267
|
+
|
268
|
+
# then we will take our markup param, and convert it to html
|
269
|
+
|
270
|
+
raw_markup = @converter.makeHtml(markup) if markup
|
271
|
+
|
272
|
+
# React.js takes a very dim view of passing raw html so its purposefully made
|
273
|
+
# difficult so you won't do it by accident. After all think of how dangerous what we
|
274
|
+
# are doing right here is!
|
275
|
+
|
276
|
+
# The span tag can be replaced by any tag that could sensibly take a child html element.
|
277
|
+
# You could also use div, td, etc.
|
278
|
+
|
279
|
+
span(dangerously_set_inner_HTML: {__html: raw_markup})
|
280
|
+
|
281
|
+
end
|
282
|
+
|
283
|
+
end
|
284
|
+
|
285
|
+
|
286
|
+
|
287
|
+
|
288
|
+
|
289
|
+
|
290
|
+
|