clearwater 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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