opal-vienna 0.7.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/.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
|