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