clearwater 0.1.0
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/lib/clearwater.rb +7 -0
- data/lib/clearwater/version.rb +3 -0
- data/opal/clearwater.rb +4 -0
- data/opal/clearwater/api_client.rb +90 -0
- data/opal/clearwater/application.rb +128 -0
- data/opal/clearwater/application_registry.rb +23 -0
- data/opal/clearwater/cached_render.rb +15 -0
- data/opal/clearwater/cgi.rb +14 -0
- data/opal/clearwater/component.rb +205 -0
- data/opal/clearwater/link.rb +122 -0
- data/opal/clearwater/model.rb +33 -0
- data/opal/clearwater/router.rb +98 -0
- data/opal/clearwater/router/route.rb +43 -0
- data/opal/clearwater/router/route_collection.rb +52 -0
- data/opal/clearwater/store.rb +99 -0
- data/opal/clearwater/virtual_dom.rb +108 -0
- data/opal/clearwater/virtual_dom/js/virtual_dom.js +1663 -0
- metadata +174 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e0dbe512477a239908316a57e35d45ee6071a644
|
4
|
+
data.tar.gz: 1a1e20bb5da838c0e7abd7cb8f43394fde474763
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 974c1f2fba54d8cf8b0467fcaaae1ebfa4e92ad1ce419be6096dbe0869044ed402b24bf8fbb557b9ab44fa37ea0ed388f443bbddb97f1a6c0dcc60b8a4e5321a
|
7
|
+
data.tar.gz: ff9e1f46a84c517f5984b0a7854add9ec298ef00dbbc206cede4b29866898b0b80d4c268e42d6af987bca27a3d565d4bf79330bd0e9fdf37c2df6f6587fa4edf
|
data/lib/clearwater.rb
ADDED
data/opal/clearwater.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
require "clearwater/cgi"
|
2
|
+
require "browser/http"
|
3
|
+
|
4
|
+
module Clearwater
|
5
|
+
class APIClient
|
6
|
+
ResponseNotFinished = Class.new(RuntimeError)
|
7
|
+
|
8
|
+
attr_reader :base_url
|
9
|
+
|
10
|
+
def initialize attributes={}
|
11
|
+
@base_url = attributes.fetch(:base_url) { nil }
|
12
|
+
end
|
13
|
+
|
14
|
+
def fetch resource, id, params
|
15
|
+
response = Browser::HTTP.get(path_for_resource(resource, id, params))
|
16
|
+
Response.new(response)
|
17
|
+
end
|
18
|
+
|
19
|
+
def store resource, id, data={}
|
20
|
+
path = case id
|
21
|
+
when String, Numeric
|
22
|
+
path_for_resource(resource, id)
|
23
|
+
when Hash
|
24
|
+
data.merge! id
|
25
|
+
path_for_resource(resource)
|
26
|
+
end
|
27
|
+
|
28
|
+
response = Browser::HTTP.post(path, data)
|
29
|
+
Response.new(response)
|
30
|
+
end
|
31
|
+
|
32
|
+
def update resource, id, data
|
33
|
+
response = Browser::HTTP.patch path_for_resource(resource, id), data: data
|
34
|
+
Response.new(response)
|
35
|
+
end
|
36
|
+
|
37
|
+
def delete resource, id
|
38
|
+
response = Browser::HTTP.delete path_for_resource(resource, id)
|
39
|
+
Response.new(response)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def path_for_resource resource, id, params={}
|
45
|
+
path = "#{base_url}/#{resource}"
|
46
|
+
case id
|
47
|
+
when String, Numeric
|
48
|
+
path += "/#{id}"
|
49
|
+
when Hash
|
50
|
+
params.merge! id
|
51
|
+
end
|
52
|
+
|
53
|
+
if params.any?
|
54
|
+
path += "?#{query_string(params)}"
|
55
|
+
end
|
56
|
+
|
57
|
+
path
|
58
|
+
end
|
59
|
+
|
60
|
+
def query_string params
|
61
|
+
query_params = params.map { |key, value|
|
62
|
+
"#{CGI.escape(key)}=#{CGI.escape(value)}"
|
63
|
+
}.join("&")
|
64
|
+
end
|
65
|
+
|
66
|
+
class Response
|
67
|
+
attr_reader :response
|
68
|
+
|
69
|
+
def initialize response
|
70
|
+
@response = response
|
71
|
+
end
|
72
|
+
|
73
|
+
def then
|
74
|
+
response.then do |r|
|
75
|
+
yield self.class.new(r)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def fail
|
80
|
+
response.fail do |r|
|
81
|
+
yield self.class.new(r)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def json
|
86
|
+
response.json
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'clearwater/router'
|
2
|
+
require 'clearwater/application_registry'
|
3
|
+
require 'browser/delay'
|
4
|
+
require 'browser/event/pop_state'
|
5
|
+
|
6
|
+
module Clearwater
|
7
|
+
class Application
|
8
|
+
RENDER_FPS = 60
|
9
|
+
AppRegistry = ApplicationRegistry.new
|
10
|
+
|
11
|
+
attr_reader :store, :router, :component, :api_client, :on_render
|
12
|
+
|
13
|
+
def self.render
|
14
|
+
AppRegistry.render_all
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize options={}
|
18
|
+
@store = options.fetch(:store) { nil }
|
19
|
+
@router = options.fetch(:router) { Router.new }
|
20
|
+
@component = options.fetch(:component) { nil }
|
21
|
+
@api_client = options.fetch(:api_client) { nil }
|
22
|
+
@element = options.fetch(:element) { nil }
|
23
|
+
@on_render = []
|
24
|
+
|
25
|
+
router.application = self
|
26
|
+
component.router = router
|
27
|
+
|
28
|
+
$document.on 'visibilitychange' do
|
29
|
+
if @render_on_visibility_change
|
30
|
+
@render_on_visibility_change = false
|
31
|
+
render
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def call
|
37
|
+
AppRegistry << self
|
38
|
+
render_current_url
|
39
|
+
watch_url
|
40
|
+
end
|
41
|
+
|
42
|
+
def watch_url
|
43
|
+
unless @watching_url
|
44
|
+
@watching_url = true
|
45
|
+
$window.on('popstate') { render_current_url }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def render_current_url &block
|
50
|
+
router.set_outlets
|
51
|
+
render &block
|
52
|
+
end
|
53
|
+
|
54
|
+
def render &block
|
55
|
+
on_render << block if block
|
56
|
+
|
57
|
+
# If the app isn't being shown, wait to render until it is.
|
58
|
+
if `document.hidden`
|
59
|
+
@render_on_visibility_change = true
|
60
|
+
return
|
61
|
+
end
|
62
|
+
|
63
|
+
delay_render
|
64
|
+
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def element
|
69
|
+
@element ||= begin
|
70
|
+
if `document.body != null` || `document.body != undefined`
|
71
|
+
$document.body
|
72
|
+
else
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def benchmark message
|
79
|
+
if debug?
|
80
|
+
start = Time.now
|
81
|
+
result = yield
|
82
|
+
finish = Time.now
|
83
|
+
puts "#{message} in #{(finish - start) * 1000}ms"
|
84
|
+
result
|
85
|
+
else
|
86
|
+
yield
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def debug?
|
91
|
+
false
|
92
|
+
end
|
93
|
+
|
94
|
+
def delay_render
|
95
|
+
# Throttle rendering
|
96
|
+
unless @next_render
|
97
|
+
@next_render = last_render + time_between_renders
|
98
|
+
now = Time.now
|
99
|
+
after [@next_render - now, time_between_renders].max do
|
100
|
+
perform_render
|
101
|
+
@next_render = nil
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def perform_render
|
107
|
+
if element.nil?
|
108
|
+
raise TypeError, "Cannot render to a non-existent element. Make sure the document ready event has been triggered before invoking the application."
|
109
|
+
end
|
110
|
+
|
111
|
+
@_virtual_dom ||= VirtualDOM::Document.new(element)
|
112
|
+
|
113
|
+
rendered = benchmark('Generated virtual DOM') { component.render }
|
114
|
+
benchmark('Rendered to actual DOM') { @_virtual_dom.render rendered }
|
115
|
+
@last_render = Time.now
|
116
|
+
on_render.each(&:call)
|
117
|
+
on_render.clear
|
118
|
+
end
|
119
|
+
|
120
|
+
def last_render
|
121
|
+
@last_render ||= Time.now - 10
|
122
|
+
end
|
123
|
+
|
124
|
+
def time_between_renders
|
125
|
+
1 / RENDER_FPS
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Clearwater
|
4
|
+
class ApplicationRegistry
|
5
|
+
def initialize
|
6
|
+
@apps = Set.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def << app
|
10
|
+
@apps << app
|
11
|
+
end
|
12
|
+
|
13
|
+
def render_all &block
|
14
|
+
@apps.each do |app|
|
15
|
+
app.render_current_url &block
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def delete app
|
20
|
+
@apps.delete app
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Clearwater
|
2
|
+
class CGI
|
3
|
+
def self.escape string
|
4
|
+
# string.gsub(/([^ a-zA-Z0-9_.-]+)/) do |ch|
|
5
|
+
string.chars.map do |ch|
|
6
|
+
if ch =~ /([^ a-zA-Z0-9_.-]+)/
|
7
|
+
"%#{ch.ord.to_s(16).upcase}"
|
8
|
+
else
|
9
|
+
ch
|
10
|
+
end
|
11
|
+
end.join.tr(" ", "+")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
require 'clearwater/virtual_dom'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module Clearwater
|
5
|
+
module Component
|
6
|
+
attr_accessor :router, :outlet
|
7
|
+
|
8
|
+
def self.included(klass)
|
9
|
+
def klass.attributes(*attrs)
|
10
|
+
attrs.each do |attr|
|
11
|
+
ivar = "@#{attr}"
|
12
|
+
define_method(attr) { instance_variable_get(ivar) }
|
13
|
+
define_method("#{attr}=") do |value|
|
14
|
+
instance_variable_set ivar, value
|
15
|
+
call
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def render
|
22
|
+
end
|
23
|
+
|
24
|
+
HTML_TAGS = %w(
|
25
|
+
a
|
26
|
+
abbr
|
27
|
+
address
|
28
|
+
area
|
29
|
+
article
|
30
|
+
aside
|
31
|
+
audio
|
32
|
+
b
|
33
|
+
base
|
34
|
+
bdi
|
35
|
+
bdo
|
36
|
+
blockquote
|
37
|
+
body
|
38
|
+
br
|
39
|
+
button
|
40
|
+
canvas
|
41
|
+
caption
|
42
|
+
cite
|
43
|
+
code
|
44
|
+
col
|
45
|
+
colgroup
|
46
|
+
command
|
47
|
+
data
|
48
|
+
datalist
|
49
|
+
dd
|
50
|
+
del
|
51
|
+
details
|
52
|
+
dfn
|
53
|
+
dialog
|
54
|
+
div
|
55
|
+
dl
|
56
|
+
dt
|
57
|
+
em
|
58
|
+
embed
|
59
|
+
fieldset
|
60
|
+
figcaption
|
61
|
+
figure
|
62
|
+
footer
|
63
|
+
form
|
64
|
+
h1
|
65
|
+
h2
|
66
|
+
h3
|
67
|
+
h4
|
68
|
+
h5
|
69
|
+
h6
|
70
|
+
head
|
71
|
+
header
|
72
|
+
hgroup
|
73
|
+
hr
|
74
|
+
html
|
75
|
+
i
|
76
|
+
iframe
|
77
|
+
img
|
78
|
+
input
|
79
|
+
ins
|
80
|
+
kbd
|
81
|
+
keygen
|
82
|
+
label
|
83
|
+
legend
|
84
|
+
li
|
85
|
+
link
|
86
|
+
map
|
87
|
+
mark
|
88
|
+
menu
|
89
|
+
meta
|
90
|
+
meter
|
91
|
+
nav
|
92
|
+
noscript
|
93
|
+
object
|
94
|
+
ol
|
95
|
+
optgroup
|
96
|
+
option
|
97
|
+
output
|
98
|
+
p
|
99
|
+
param
|
100
|
+
pre
|
101
|
+
progress
|
102
|
+
q
|
103
|
+
rp
|
104
|
+
rt
|
105
|
+
ruby
|
106
|
+
s
|
107
|
+
samp
|
108
|
+
script
|
109
|
+
section
|
110
|
+
select
|
111
|
+
small
|
112
|
+
source
|
113
|
+
span
|
114
|
+
strong
|
115
|
+
style
|
116
|
+
sub
|
117
|
+
summary
|
118
|
+
sup
|
119
|
+
table
|
120
|
+
tbody
|
121
|
+
td
|
122
|
+
textarea
|
123
|
+
tfoot
|
124
|
+
th
|
125
|
+
thead
|
126
|
+
time
|
127
|
+
title
|
128
|
+
tr
|
129
|
+
track
|
130
|
+
u
|
131
|
+
ul
|
132
|
+
var
|
133
|
+
video
|
134
|
+
wbr
|
135
|
+
)
|
136
|
+
|
137
|
+
HTML_TAGS.each do |tag_name|
|
138
|
+
define_method(tag_name) do |attributes, content|
|
139
|
+
tag(tag_name, attributes, content)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def tag tag_name, attributes=nil, content=nil
|
144
|
+
VirtualDOM.node(
|
145
|
+
tag_name,
|
146
|
+
sanitize_attributes(attributes),
|
147
|
+
sanitize_content(content)
|
148
|
+
)
|
149
|
+
end
|
150
|
+
|
151
|
+
def params
|
152
|
+
router.params_for_path(router.current_path)
|
153
|
+
end
|
154
|
+
|
155
|
+
def param(key)
|
156
|
+
params[key]
|
157
|
+
end
|
158
|
+
|
159
|
+
def sanitize_attributes attributes
|
160
|
+
return nil if attributes.nil?
|
161
|
+
|
162
|
+
# Allow specifying `class` instead of `class_name`.
|
163
|
+
# Note: `class_name` is still allowed
|
164
|
+
if attributes.key? :class
|
165
|
+
if attributes.key? :class_name
|
166
|
+
warn "You have both `class` and `class_name` attributes for this " +
|
167
|
+
"element. `class` takes precedence: #{attributes}"
|
168
|
+
end
|
169
|
+
|
170
|
+
attributes[:class_name] = attributes.delete(:class)
|
171
|
+
end
|
172
|
+
|
173
|
+
attributes.each do |key, handler|
|
174
|
+
if key.start_with? 'on'
|
175
|
+
attributes[key] = proc do |event|
|
176
|
+
handler.call(Browser::Event.new(event))
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
attributes
|
182
|
+
end
|
183
|
+
|
184
|
+
def sanitize_content content
|
185
|
+
case content
|
186
|
+
when Numeric, String
|
187
|
+
content.to_s
|
188
|
+
when Array
|
189
|
+
content.map { |c| sanitize_content(c) }.compact
|
190
|
+
else
|
191
|
+
if content.respond_to? :cached_render
|
192
|
+
content.cached_render
|
193
|
+
elsif content.respond_to? :render
|
194
|
+
sanitize_content content.render
|
195
|
+
else
|
196
|
+
content
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def call &block
|
202
|
+
Clearwater::Application::AppRegistry.render_all(&block)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|