opal-vienna 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.travis.yml +8 -0
- data/Gemfile +7 -0
- data/README.md +294 -0
- data/Rakefile +7 -0
- data/config.ru +8 -0
- data/lib/opal-vienna.rb +1 -0
- data/lib/opal/vienna.rb +7 -0
- data/lib/opal/vienna/version.rb +5 -0
- data/opal-vienna.gemspec +26 -0
- data/opal/vienna.rb +5 -0
- data/opal/vienna/adapters/base.rb +45 -0
- data/opal/vienna/adapters/local.rb +50 -0
- data/opal/vienna/adapters/rest.rb +97 -0
- data/opal/vienna/eventable.rb +35 -0
- data/opal/vienna/history_router.rb +44 -0
- data/opal/vienna/model.rb +222 -0
- data/opal/vienna/observable.rb +90 -0
- data/opal/vienna/observable_array.rb +73 -0
- data/opal/vienna/output_buffer.rb +13 -0
- data/opal/vienna/record_array.rb +31 -0
- data/opal/vienna/router.rb +85 -0
- data/opal/vienna/template_view.rb +41 -0
- data/opal/vienna/view.rb +93 -0
- data/spec/eventable_spec.rb +94 -0
- data/spec/history_router_spec.rb +47 -0
- data/spec/model/accessing_attributes_spec.rb +29 -0
- data/spec/model/as_json_spec.rb +28 -0
- data/spec/model/attribute_spec.rb +22 -0
- data/spec/model/initialize_spec.rb +42 -0
- data/spec/model/load_spec.rb +17 -0
- data/spec/model/persistence_spec.rb +84 -0
- data/spec/model_spec.rb +84 -0
- data/spec/observable_array_spec.rb +130 -0
- data/spec/observable_spec.rb +116 -0
- data/spec/output_buffer_spec.rb +37 -0
- data/spec/record_array_spec.rb +50 -0
- data/spec/route_spec.rb +89 -0
- data/spec/router_spec.rb +103 -0
- data/spec/spec_helper.rb +53 -0
- data/spec/template_view_spec.rb +47 -0
- data/spec/vendor/jquery.js +2 -0
- data/spec/view_spec.rb +78 -0
- metadata +181 -0
@@ -0,0 +1,97 @@
|
|
1
|
+
module Vienna
|
2
|
+
class Model
|
3
|
+
def self.url(url = nil)
|
4
|
+
url ? @url = url : @url
|
5
|
+
end
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
module Vienna
|
10
|
+
# Adapter for a REST backend
|
11
|
+
class RESTAdapter < Adapter
|
12
|
+
def create_record(record, &block)
|
13
|
+
url = record_url(record)
|
14
|
+
options = { dataType: "json", payload: record.as_json }
|
15
|
+
HTTP.post(url, options) do |response|
|
16
|
+
if response.ok?
|
17
|
+
record.load Hash.new(response.body)
|
18
|
+
record.class.trigger :ajax_success, response
|
19
|
+
record.did_create
|
20
|
+
record.class.trigger :change, record.class.all
|
21
|
+
else
|
22
|
+
record.trigger_events :ajax_error, response
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
block.call(record) if block
|
27
|
+
end
|
28
|
+
|
29
|
+
def update_record(record, &block)
|
30
|
+
url = record_url(record)
|
31
|
+
options = { dataType: "json", payload: record.as_json }
|
32
|
+
HTTP.put(url, options) do |response|
|
33
|
+
if response.ok?
|
34
|
+
record.class.load_json response.body
|
35
|
+
record.class.trigger :ajax_success, response
|
36
|
+
record.did_update
|
37
|
+
record.class.trigger :change, record.class.all
|
38
|
+
else
|
39
|
+
record.trigger_events :ajax_error, response
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
block.call(record) if block
|
44
|
+
end
|
45
|
+
|
46
|
+
def delete_record(record, &block)
|
47
|
+
options = { dataType: "json" }
|
48
|
+
url = record_url(record)
|
49
|
+
|
50
|
+
HTTP.delete(url, options) do |response|
|
51
|
+
if response.ok?
|
52
|
+
record.did_destroy
|
53
|
+
record.class.trigger :ajax_success, response
|
54
|
+
record.class.trigger :change, record.class.all
|
55
|
+
else
|
56
|
+
record.class.trigger :ajax_error, response
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
block.call(record) if block
|
61
|
+
end
|
62
|
+
|
63
|
+
def find(record, id)
|
64
|
+
# TODO remote fetch
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def fetch(model, options = {}, &block)
|
69
|
+
id = options.fetch(:id, nil)
|
70
|
+
params = options.fetch(:params, nil)
|
71
|
+
url = id ? "#{record_url(model)}/#{id}" : record_url(model)
|
72
|
+
options = { dataType: "json", data: params }.merge(options)
|
73
|
+
HTTP.get(url, options) do |response|
|
74
|
+
if response.ok?
|
75
|
+
response.body.map { |json| model.load_json json }
|
76
|
+
model.trigger :ajax_success, response
|
77
|
+
model.trigger :refresh, model.all
|
78
|
+
else
|
79
|
+
model.trigger :ajax_error, response
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
block.call(model.all) if block
|
84
|
+
end
|
85
|
+
|
86
|
+
def record_url(record)
|
87
|
+
return record.to_url if record.respond_to? :to_url
|
88
|
+
return record.url if record.respond_to? :url
|
89
|
+
|
90
|
+
if klass_url = record.class.url
|
91
|
+
return "#{klass_url}/#{record.id}"
|
92
|
+
end
|
93
|
+
|
94
|
+
raise "Model does not define REST url: #{record}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Vienna
|
2
|
+
# A simple event registering/triggering module to mix into classes.
|
3
|
+
# Events are stored in the `@events` ivar.
|
4
|
+
module Eventable
|
5
|
+
# Register a handler for the given event name.
|
6
|
+
#
|
7
|
+
# obj.on(:foo) { puts "foo was called" }
|
8
|
+
#
|
9
|
+
# @param [String, Symbol] name event name
|
10
|
+
# @return handler
|
11
|
+
def on(name, &handler)
|
12
|
+
@eventable ||= Hash.new { |hash, key| hash[key] = [] }
|
13
|
+
@eventable[name] << handler
|
14
|
+
handler
|
15
|
+
end
|
16
|
+
|
17
|
+
def off(name, handler)
|
18
|
+
if @eventable and evts = @eventable[name]
|
19
|
+
evts.delete handler
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Trigger the given event name and passes all args to each handler
|
24
|
+
# for this event.
|
25
|
+
#
|
26
|
+
# obj.trigger(:foo)
|
27
|
+
# obj.trigger(:foo, 1, 2, 3)
|
28
|
+
#
|
29
|
+
# @param [String, Symbol] name event name to trigger
|
30
|
+
def trigger(name, *args)
|
31
|
+
@eventable ||= Hash.new { |hash, key| hash[key] = [] }
|
32
|
+
@eventable[name].each { |handler| handler.call(*args) }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'opal-jquery'
|
2
|
+
|
3
|
+
module Vienna
|
4
|
+
class HistoryRouter
|
5
|
+
attr_reader :path, :routes
|
6
|
+
|
7
|
+
def initialize(&block)
|
8
|
+
@routes = []
|
9
|
+
@location = $global.location
|
10
|
+
|
11
|
+
Window.on(:popstate) { update }
|
12
|
+
|
13
|
+
instance_eval(&block) if block
|
14
|
+
end
|
15
|
+
|
16
|
+
def route(path, &handler)
|
17
|
+
route = Router::Route.new(path, &handler)
|
18
|
+
@routes << route
|
19
|
+
route
|
20
|
+
end
|
21
|
+
|
22
|
+
def update
|
23
|
+
path = if @location.pathname.empty?
|
24
|
+
'/'
|
25
|
+
else
|
26
|
+
@location.pathname
|
27
|
+
end
|
28
|
+
|
29
|
+
unless @path == path
|
30
|
+
@path = path
|
31
|
+
match @path
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def match(path)
|
36
|
+
@routes.find { |r| r.match path }
|
37
|
+
end
|
38
|
+
|
39
|
+
def navigate(path)
|
40
|
+
`history.pushState(null, null, path)`
|
41
|
+
update
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
require 'vienna/eventable'
|
2
|
+
require 'vienna/record_array'
|
3
|
+
|
4
|
+
module Vienna
|
5
|
+
class Model
|
6
|
+
include Eventable
|
7
|
+
extend Eventable
|
8
|
+
|
9
|
+
attr_accessor :id
|
10
|
+
attr_writer :loaded
|
11
|
+
attr_writer :new_record
|
12
|
+
|
13
|
+
class << self
|
14
|
+
attr_reader :identity_map
|
15
|
+
attr_reader :all
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.reset!
|
19
|
+
@identity_map = {}
|
20
|
+
@all = RecordArray.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.inherited(base)
|
24
|
+
base.reset!
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.attributes(*attributes)
|
28
|
+
attributes.each { |name| attribute name }
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.attribute(name)
|
32
|
+
columns << name
|
33
|
+
attr_accessor name
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.columns
|
37
|
+
@columns ||= []
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.primary_key(primary_key = nil)
|
41
|
+
primary_key ? @primary_key = primary_key : @primary_key ||= :id
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.adapter(klass = nil)
|
45
|
+
return @adapter = klass.new if klass
|
46
|
+
@adapter || raise("No adapter for #{self}")
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.find(id, &block)
|
50
|
+
if record = identity_map[id]
|
51
|
+
return record
|
52
|
+
end
|
53
|
+
|
54
|
+
record = self.new
|
55
|
+
self.adapter.find(record, id, &block)
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.load(attributes)
|
59
|
+
unless id = attributes[primary_key]
|
60
|
+
raise ArgumentError, "no id (#{primary_key}) given"
|
61
|
+
end
|
62
|
+
|
63
|
+
map = identity_map
|
64
|
+
unless model = map[id]
|
65
|
+
model = self.new
|
66
|
+
model.id = id
|
67
|
+
map[id] = model
|
68
|
+
self.all << model
|
69
|
+
end
|
70
|
+
|
71
|
+
model.load(attributes)
|
72
|
+
|
73
|
+
model
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.load_json(json)
|
77
|
+
load Hash.new json
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.create(attrs = {})
|
81
|
+
model = self.new(attrs)
|
82
|
+
model.save
|
83
|
+
model
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.fetch(options = {}, &block)
|
87
|
+
reset!
|
88
|
+
adapter.fetch(self, options, &block)
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.from_form(form)
|
92
|
+
attrs = {}
|
93
|
+
`#{form}.serializeArray()`.each do |field|
|
94
|
+
key, val = `field.name`, `field.value`
|
95
|
+
attrs[key] = val
|
96
|
+
end
|
97
|
+
model = new attrs
|
98
|
+
if attrs.has_key?(self.primary_key) and ! attrs[self.primary_key].empty?
|
99
|
+
model.instance_variable_set('@new_record', false)
|
100
|
+
end
|
101
|
+
model
|
102
|
+
end
|
103
|
+
|
104
|
+
def as_json
|
105
|
+
json = {}
|
106
|
+
json[:id] = self.id if self.id
|
107
|
+
|
108
|
+
self.class.columns.each { |name| json[name] = __send__(name) }
|
109
|
+
json
|
110
|
+
end
|
111
|
+
|
112
|
+
def to_json
|
113
|
+
as_json.to_json
|
114
|
+
end
|
115
|
+
|
116
|
+
def trigger_events(name)
|
117
|
+
self.class.trigger(name, self)
|
118
|
+
self.trigger(name)
|
119
|
+
end
|
120
|
+
|
121
|
+
def inspect
|
122
|
+
"#<#{self.class.name}: #{self.class.columns.map { |name|
|
123
|
+
"#{name}=#{__send__(name).inspect}"
|
124
|
+
}.join(" ")}>"
|
125
|
+
end
|
126
|
+
|
127
|
+
def load(attributes = nil)
|
128
|
+
self.loaded = true
|
129
|
+
self.new_record = false
|
130
|
+
|
131
|
+
self.attributes = attributes if attributes
|
132
|
+
|
133
|
+
trigger_events :load
|
134
|
+
end
|
135
|
+
|
136
|
+
def new_record?
|
137
|
+
@new_record
|
138
|
+
end
|
139
|
+
|
140
|
+
def destroyed?
|
141
|
+
@destroyed
|
142
|
+
end
|
143
|
+
|
144
|
+
def loaded?
|
145
|
+
@loaded
|
146
|
+
end
|
147
|
+
|
148
|
+
def save(&block)
|
149
|
+
@new_record ? create(&block) : update(&block)
|
150
|
+
end
|
151
|
+
|
152
|
+
def create(&block)
|
153
|
+
self.class.adapter.create_record(self, &block)
|
154
|
+
end
|
155
|
+
|
156
|
+
def update(attributes = nil, &block)
|
157
|
+
self.attributes = attributes if attributes
|
158
|
+
self.class.adapter.update_record(self, &block)
|
159
|
+
end
|
160
|
+
|
161
|
+
def destroy(&block)
|
162
|
+
self.class.adapter.delete_record(self, &block)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Should be considered a private method. This is called by an adapter when
|
166
|
+
# this record gets deleted/destroyed. This method is then responsible from
|
167
|
+
# remoing this record instance from the class' identity_map, and triggering
|
168
|
+
# a `:destroy` event. If you override this method, you *must* call super,
|
169
|
+
# otherwise undefined bad things will happen.
|
170
|
+
def did_destroy
|
171
|
+
@destroyed = true
|
172
|
+
self.class.identity_map.delete self.id
|
173
|
+
self.class.all.delete self
|
174
|
+
|
175
|
+
trigger_events(:destroy)
|
176
|
+
end
|
177
|
+
|
178
|
+
# A private method. This is called by the adapter once this record has been
|
179
|
+
# created in the backend.
|
180
|
+
def did_create
|
181
|
+
@new_record = false
|
182
|
+
self.class.identity_map[self.id] = self
|
183
|
+
self.class.all.push self
|
184
|
+
|
185
|
+
trigger_events(:create)
|
186
|
+
end
|
187
|
+
|
188
|
+
# A private method. This is called by the adapter when this record has been
|
189
|
+
# updated in the adapter. It should not be called directly. It may be
|
190
|
+
# overriden, aslong as `super()` is called.
|
191
|
+
def did_update
|
192
|
+
trigger_events(:update)
|
193
|
+
end
|
194
|
+
|
195
|
+
def [](attribute)
|
196
|
+
instance_variable_get "@#{attribute}"
|
197
|
+
end
|
198
|
+
|
199
|
+
def []=(attribute, value)
|
200
|
+
instance_variable_set "@#{attribute}", value
|
201
|
+
end
|
202
|
+
|
203
|
+
def attributes=(attributes)
|
204
|
+
attributes.each do |name, value|
|
205
|
+
setter = "#{name}="
|
206
|
+
if respond_to? setter
|
207
|
+
__send__ setter, value
|
208
|
+
else
|
209
|
+
instance_variable_set "@#{name}", value
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def initialize(attributes = nil)
|
215
|
+
@attributes = {}
|
216
|
+
@new_record = true
|
217
|
+
@loaded = false
|
218
|
+
|
219
|
+
self.attributes = attributes if attributes
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Vienna
|
2
|
+
module Observable
|
3
|
+
|
4
|
+
def add_observer(attribute, &handler)
|
5
|
+
unless observers = @attr_observers
|
6
|
+
observers = @attr_observers = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
if attribute.include? '.'
|
10
|
+
return PathObserver.create(self, attribute, handler)
|
11
|
+
end
|
12
|
+
|
13
|
+
unless handlers = observers[attribute]
|
14
|
+
handlers = observers[attribute] = []
|
15
|
+
replace_writer_for(attribute)
|
16
|
+
end
|
17
|
+
|
18
|
+
handlers << handler
|
19
|
+
end
|
20
|
+
|
21
|
+
def remove_observer(attribute, handler)
|
22
|
+
return unless @attr_observers
|
23
|
+
|
24
|
+
if handlers = @attr_observers[attribute]
|
25
|
+
handlers.delete handler
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Triggers observers for the given attribute. You may call this directly if
|
30
|
+
# needed, but it is generally called automatically for you inside a
|
31
|
+
# replaced setter method.
|
32
|
+
def attribute_did_change(attribute)
|
33
|
+
return unless @attr_observers
|
34
|
+
|
35
|
+
if handlers = @attr_observers[attribute]
|
36
|
+
new_val = __send__(attribute) if respond_to?(attribute)
|
37
|
+
handlers.each { |h| h.call new_val }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# private?
|
42
|
+
def replace_writer_for(attribute)
|
43
|
+
if respond_to? "#{attribute}="
|
44
|
+
define_singleton_method("#{attribute}=") do |val|
|
45
|
+
result = super val
|
46
|
+
attribute_did_change(attribute)
|
47
|
+
result
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class PathObserver
|
54
|
+
def self.create(object, path, handler)
|
55
|
+
parts = path.split '.'
|
56
|
+
base = PathObserver.new parts[0]
|
57
|
+
last = base
|
58
|
+
|
59
|
+
parts.drop(1).each do |attr|
|
60
|
+
last = last.next = PathObserver.new(attr)
|
61
|
+
end
|
62
|
+
|
63
|
+
base.object = object
|
64
|
+
last.handler = handler
|
65
|
+
end
|
66
|
+
|
67
|
+
attr_accessor :next, :handler
|
68
|
+
|
69
|
+
def initialize(attr)
|
70
|
+
@attr = attr
|
71
|
+
end
|
72
|
+
|
73
|
+
def object=(obj)
|
74
|
+
return if obj == @object
|
75
|
+
|
76
|
+
if @object = obj
|
77
|
+
obj.add_observer(@attr) { value_changed }
|
78
|
+
end
|
79
|
+
|
80
|
+
value_changed
|
81
|
+
end
|
82
|
+
|
83
|
+
def value_changed
|
84
|
+
value = @object.__send__ @attr
|
85
|
+
|
86
|
+
@next.object = value if @next
|
87
|
+
@handler.call value if @handler
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|