clearwater 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,7 @@
1
+ require "opal"
2
+
3
+ module Clearwater
4
+ require_relative "clearwater/version"
5
+ end
6
+
7
+ Opal.append_path(File.expand_path(File.join("..", "..", "opal"), __FILE__).untaint)
@@ -0,0 +1,3 @@
1
+ module Clearwater
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,4 @@
1
+ require 'browser'
2
+ require 'clearwater/component'
3
+ require 'clearwater/link'
4
+ require 'clearwater/application'
@@ -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,15 @@
1
+ module Clearwater
2
+ module CachedRender
3
+ def cached_render
4
+ if !@cached_render || should_render?
5
+ @cached_render = sanitize_content(render)
6
+ else
7
+ @cached_render
8
+ end
9
+ end
10
+
11
+ def should_render?
12
+ false
13
+ end
14
+ end
15
+ 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