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
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'clearwater/component'
|
2
|
+
require 'browser/history'
|
3
|
+
|
4
|
+
# TODO: Remove this once opal-browser supports coordinates natively
|
5
|
+
# for touch events.
|
6
|
+
module Browser
|
7
|
+
class Event
|
8
|
+
class Touch
|
9
|
+
def x
|
10
|
+
`#@native.pageX`
|
11
|
+
end
|
12
|
+
|
13
|
+
def y
|
14
|
+
`#@native.pageY`
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Link
|
21
|
+
include Clearwater::Component
|
22
|
+
|
23
|
+
attr_reader :attributes, :content
|
24
|
+
|
25
|
+
def initialize(attributes={}, content=nil)
|
26
|
+
# Augment the onclick passed in by the user, don't replace it.
|
27
|
+
@onclick = attributes.dup.delete(:onclick)
|
28
|
+
@attributes = attributes.merge(
|
29
|
+
onclick: method(:handle_click),
|
30
|
+
ontouchstart: method(:handle_touch),
|
31
|
+
key: attributes[:href]
|
32
|
+
)
|
33
|
+
|
34
|
+
check_active attributes[:href]
|
35
|
+
|
36
|
+
@content = content
|
37
|
+
end
|
38
|
+
|
39
|
+
def handle_click event
|
40
|
+
if @onclick
|
41
|
+
@onclick.call event
|
42
|
+
end
|
43
|
+
|
44
|
+
if touch?
|
45
|
+
event.prevent
|
46
|
+
return
|
47
|
+
end
|
48
|
+
|
49
|
+
if event.prevented?
|
50
|
+
warn "You are preventing the default behavior of a `Link` component. " +
|
51
|
+
"In this case, you could just use an `a` element."
|
52
|
+
else
|
53
|
+
navigate event
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def handle_touch event
|
58
|
+
# All links will treat this as touch because this is a touch device
|
59
|
+
@@touch = true
|
60
|
+
moved = false
|
61
|
+
x_start = event.x
|
62
|
+
y_start = event.y
|
63
|
+
|
64
|
+
touch_move_handler = proc do |event|
|
65
|
+
x_now = event.x
|
66
|
+
y_now = event.y
|
67
|
+
|
68
|
+
# Count this gesture as a non-click if user moves over 30px
|
69
|
+
if ((x_now - x_start) ** 2 + (y_now - y_start) ** 2) ** 0.5 > 30
|
70
|
+
moved = true
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
touch_end_handler = proc do
|
75
|
+
unless moved
|
76
|
+
@onclick.call event if @onclick
|
77
|
+
navigate event
|
78
|
+
end
|
79
|
+
|
80
|
+
$document.off 'touchmove', &touch_move_handler
|
81
|
+
$document.off 'touchend', &touch_end_handler
|
82
|
+
end
|
83
|
+
|
84
|
+
$document.on 'touchmove', &touch_move_handler
|
85
|
+
$document.on 'touchend', &touch_end_handler
|
86
|
+
end
|
87
|
+
|
88
|
+
def navigate event
|
89
|
+
unless event.meta? || event.shift? || event.ctrl? || event.alt?
|
90
|
+
event.prevent
|
91
|
+
if href != $window.location.path
|
92
|
+
$window.history.push href
|
93
|
+
call
|
94
|
+
$window.scroll.to x: 0, y: 0
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def href
|
100
|
+
attributes[:href]
|
101
|
+
end
|
102
|
+
|
103
|
+
def render
|
104
|
+
a(attributes, content)
|
105
|
+
end
|
106
|
+
|
107
|
+
def touch?
|
108
|
+
!!@@touch
|
109
|
+
end
|
110
|
+
|
111
|
+
def check_active href
|
112
|
+
if $window.location.path == href
|
113
|
+
class_name = (
|
114
|
+
@attributes.delete(:class_name) ||
|
115
|
+
@attributes.delete(:class) ||
|
116
|
+
@attributes.delete(:className)
|
117
|
+
).to_s
|
118
|
+
|
119
|
+
@attributes[:className] = "#{class_name} active"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "clearwater/binding"
|
2
|
+
|
3
|
+
module Clearwater
|
4
|
+
class Model
|
5
|
+
def initialize attributes={}
|
6
|
+
@_bindings = Hash.new { |h,k| h[k] = [] }
|
7
|
+
self.class.attributes.each do |attr|
|
8
|
+
public_send "#{attr}=", attributes[attr]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_binding attribute, &block
|
13
|
+
binding = Binding.new(self, attribute, &block)
|
14
|
+
@_bindings[attribute].delete_if(&:dead?)
|
15
|
+
@_bindings[attribute] << binding
|
16
|
+
binding
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.attributes *args
|
20
|
+
@attributes ||= []
|
21
|
+
args.each do |attr|
|
22
|
+
attr_reader attr
|
23
|
+
|
24
|
+
define_method "#{attr}=" do |value|
|
25
|
+
instance_variable_set "@#{attr}", value
|
26
|
+
@_bindings[attr].each(&:call)
|
27
|
+
@_bindings[attr].delete_if(&:dead?)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
@attributes.concat args
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require "clearwater/router/route_collection"
|
2
|
+
|
3
|
+
module Clearwater
|
4
|
+
class Router
|
5
|
+
attr_reader :window, :location, :history
|
6
|
+
attr_accessor :application
|
7
|
+
|
8
|
+
def initialize options={}, &block
|
9
|
+
@window = options.fetch(:window) { Native(`window`) }
|
10
|
+
@location = options.fetch(:location) { window[:location] }
|
11
|
+
@history = options.fetch(:history) { window[:history] }
|
12
|
+
@routes = RouteCollection.new(self)
|
13
|
+
@application = options[:application]
|
14
|
+
|
15
|
+
add_routes(&block) if block_given?
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_routes &block
|
19
|
+
@routes.instance_exec(&block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def routes_for_path path
|
23
|
+
parts = path.split("/").reject(&:empty?)
|
24
|
+
@routes[parts]
|
25
|
+
end
|
26
|
+
|
27
|
+
def canonical_path_for_path path
|
28
|
+
routes_for_path(path).map { |r| "/#{r.key}" }.join
|
29
|
+
end
|
30
|
+
|
31
|
+
def targets_for_path path
|
32
|
+
routes_for_path(path).map(&:target)
|
33
|
+
end
|
34
|
+
|
35
|
+
def params_for_path path
|
36
|
+
path_parts = path.split("/").reject(&:empty?)
|
37
|
+
canonical_parts = canonical_path_for_path(path).split("/").reject(&:empty?)
|
38
|
+
params = {}
|
39
|
+
canonical_parts.each_with_object(params)
|
40
|
+
.each_with_index { |(part, params), index|
|
41
|
+
if part.start_with? ":"
|
42
|
+
param = part[1..-1].to_sym
|
43
|
+
params[param] = path_parts[index]
|
44
|
+
end
|
45
|
+
}
|
46
|
+
params
|
47
|
+
end
|
48
|
+
|
49
|
+
def canonical_path
|
50
|
+
end
|
51
|
+
|
52
|
+
def current_path
|
53
|
+
location[:pathname]
|
54
|
+
end
|
55
|
+
|
56
|
+
def navigate_to path
|
57
|
+
push_state path
|
58
|
+
set_outlets
|
59
|
+
render_application
|
60
|
+
end
|
61
|
+
|
62
|
+
def navigate_to_remote path
|
63
|
+
location[:href] = path
|
64
|
+
end
|
65
|
+
|
66
|
+
def current_url
|
67
|
+
location[:href]
|
68
|
+
end
|
69
|
+
|
70
|
+
def back
|
71
|
+
history.back
|
72
|
+
end
|
73
|
+
|
74
|
+
def set_outlets targets=targets_for_path(current_path)
|
75
|
+
if targets.any?
|
76
|
+
(targets.count).times do |index|
|
77
|
+
targets[index].outlet = targets[index + 1]
|
78
|
+
end
|
79
|
+
|
80
|
+
application && application.component.outlet = targets.first
|
81
|
+
else
|
82
|
+
application && application.component.outlet = nil
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def push_state path
|
89
|
+
history.pushState({}, nil, path)
|
90
|
+
end
|
91
|
+
|
92
|
+
def render_application
|
93
|
+
if application && application.component
|
94
|
+
application.component.call
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "clearwater/router/route_collection"
|
2
|
+
|
3
|
+
module Clearwater
|
4
|
+
class Router
|
5
|
+
class Route
|
6
|
+
attr_reader :target, :key, :parent
|
7
|
+
|
8
|
+
def initialize options={}
|
9
|
+
@key = options.fetch(:key)
|
10
|
+
@target = options.fetch(:target)
|
11
|
+
@parent = options.fetch(:parent)
|
12
|
+
end
|
13
|
+
|
14
|
+
def route *args, &block
|
15
|
+
nested_routes.route *args, &block
|
16
|
+
end
|
17
|
+
|
18
|
+
def canonical_path
|
19
|
+
@canonical_path ||= "#{parent.canonical_path}/#{key}".gsub("//", "/")
|
20
|
+
end
|
21
|
+
|
22
|
+
def match key, other_parts=[]
|
23
|
+
if key && (key == self.key || param_key?)
|
24
|
+
if Array(other_parts).any?
|
25
|
+
[self, nested_routes[other_parts]]
|
26
|
+
else
|
27
|
+
self
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def param_key?
|
35
|
+
@param_key ||= key.start_with? ":"
|
36
|
+
end
|
37
|
+
|
38
|
+
def nested_routes
|
39
|
+
@nested_routes ||= RouteCollection.new(self)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require "clearwater/router/route"
|
2
|
+
|
3
|
+
module Clearwater
|
4
|
+
class Router
|
5
|
+
class RouteCollection
|
6
|
+
attr_reader :router
|
7
|
+
|
8
|
+
def initialize parent
|
9
|
+
@routes = []
|
10
|
+
@parent = parent
|
11
|
+
@router = loop do
|
12
|
+
break parent if parent.is_a? Router
|
13
|
+
parent = parent.parent
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def route route_options, &block
|
18
|
+
route_key = route_options.keys.first.to_s
|
19
|
+
target = route_options.delete(route_key)
|
20
|
+
target.router = router
|
21
|
+
options = {
|
22
|
+
key: route_key,
|
23
|
+
target: target,
|
24
|
+
parent: parent,
|
25
|
+
}.merge(route_options)
|
26
|
+
route = Route.new(options)
|
27
|
+
route.instance_exec(&block) if block_given?
|
28
|
+
@routes << route
|
29
|
+
end
|
30
|
+
|
31
|
+
def namespace path
|
32
|
+
@namespace = path
|
33
|
+
end
|
34
|
+
|
35
|
+
def [] route_names
|
36
|
+
if route_names.any? && route_names.first == @namespace
|
37
|
+
route_names = route_names[1..-1]
|
38
|
+
end
|
39
|
+
routes = @routes.map { |r|
|
40
|
+
r.match route_names.first, route_names[1..-1]
|
41
|
+
}
|
42
|
+
routes.compact!
|
43
|
+
routes.flatten!
|
44
|
+
routes
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
attr_reader :parent
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module Clearwater
|
4
|
+
class Store
|
5
|
+
attr_reader :mapping, :identity_map
|
6
|
+
|
7
|
+
def initialize options={}
|
8
|
+
@url = options.fetch(:url) { "/api/:model/:id" }
|
9
|
+
@mapping = Mapping.new(options.fetch(:mapping) { {} })
|
10
|
+
@protocol = options.fetch(:protocol) { HTTP }
|
11
|
+
@identity_map = IdentityMap.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def all klass
|
15
|
+
deserialized = api(:get, url_for(klass)).body
|
16
|
+
models = JSON.parse(deserialized).map do |attributes|
|
17
|
+
deserialize(klass, attributes)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def find klass, id
|
22
|
+
identity_map.fetch(klass, id) do
|
23
|
+
serialized = api(:get, url_for(klass, id)).body
|
24
|
+
identity_map[klass][id] = deserialize(klass, JSON.parse(serialized))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def save model
|
29
|
+
method = persisted?(model) ? :patch : :post
|
30
|
+
response = api method, url_for_model(model)
|
31
|
+
if method == :post && response.ok
|
32
|
+
attributes = JSON.parse(response.body)
|
33
|
+
model.instance_variable_set :@id, attributes[:id]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def delete model
|
38
|
+
api :delete, url_for_model(model)
|
39
|
+
end
|
40
|
+
|
41
|
+
def persisted? model
|
42
|
+
!!model.id
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def api method, url
|
48
|
+
@protocol.public_send method, url
|
49
|
+
end
|
50
|
+
|
51
|
+
def url_for klass, id=nil
|
52
|
+
@url.gsub(":model", mapping[klass])
|
53
|
+
.gsub(":id", id.to_s)
|
54
|
+
end
|
55
|
+
|
56
|
+
def url_for_model model
|
57
|
+
url_for(model.class, model.id)
|
58
|
+
end
|
59
|
+
|
60
|
+
def url_for_class klass, id
|
61
|
+
url_for(klass, nil)
|
62
|
+
end
|
63
|
+
|
64
|
+
def deserialize klass, attributes
|
65
|
+
model = klass.allocate
|
66
|
+
attributes.each do |attr, value|
|
67
|
+
model.instance_variable_set "@#{attr}", value
|
68
|
+
end
|
69
|
+
|
70
|
+
model
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class Mapping
|
75
|
+
def initialize mappings={}
|
76
|
+
@custom_mappings = mappings
|
77
|
+
end
|
78
|
+
|
79
|
+
def [] klass
|
80
|
+
@custom_mappings.fetch(klass) { |*args|
|
81
|
+
klass.name.downcase.gsub("::", "/") + "s"
|
82
|
+
}
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class IdentityMap
|
87
|
+
def initialize
|
88
|
+
@map = Hash.new { |h, k| h[k] = {} }
|
89
|
+
end
|
90
|
+
|
91
|
+
def [] key
|
92
|
+
@map[key]
|
93
|
+
end
|
94
|
+
|
95
|
+
def fetch(klass, id, &block)
|
96
|
+
self[klass].fetch(id, &block)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|