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 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