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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.travis.yml +8 -0
  4. data/Gemfile +7 -0
  5. data/README.md +294 -0
  6. data/Rakefile +7 -0
  7. data/config.ru +8 -0
  8. data/lib/opal-vienna.rb +1 -0
  9. data/lib/opal/vienna.rb +7 -0
  10. data/lib/opal/vienna/version.rb +5 -0
  11. data/opal-vienna.gemspec +26 -0
  12. data/opal/vienna.rb +5 -0
  13. data/opal/vienna/adapters/base.rb +45 -0
  14. data/opal/vienna/adapters/local.rb +50 -0
  15. data/opal/vienna/adapters/rest.rb +97 -0
  16. data/opal/vienna/eventable.rb +35 -0
  17. data/opal/vienna/history_router.rb +44 -0
  18. data/opal/vienna/model.rb +222 -0
  19. data/opal/vienna/observable.rb +90 -0
  20. data/opal/vienna/observable_array.rb +73 -0
  21. data/opal/vienna/output_buffer.rb +13 -0
  22. data/opal/vienna/record_array.rb +31 -0
  23. data/opal/vienna/router.rb +85 -0
  24. data/opal/vienna/template_view.rb +41 -0
  25. data/opal/vienna/view.rb +93 -0
  26. data/spec/eventable_spec.rb +94 -0
  27. data/spec/history_router_spec.rb +47 -0
  28. data/spec/model/accessing_attributes_spec.rb +29 -0
  29. data/spec/model/as_json_spec.rb +28 -0
  30. data/spec/model/attribute_spec.rb +22 -0
  31. data/spec/model/initialize_spec.rb +42 -0
  32. data/spec/model/load_spec.rb +17 -0
  33. data/spec/model/persistence_spec.rb +84 -0
  34. data/spec/model_spec.rb +84 -0
  35. data/spec/observable_array_spec.rb +130 -0
  36. data/spec/observable_spec.rb +116 -0
  37. data/spec/output_buffer_spec.rb +37 -0
  38. data/spec/record_array_spec.rb +50 -0
  39. data/spec/route_spec.rb +89 -0
  40. data/spec/router_spec.rb +103 -0
  41. data/spec/spec_helper.rb +53 -0
  42. data/spec/template_view_spec.rb +47 -0
  43. data/spec/vendor/jquery.js +2 -0
  44. data/spec/view_spec.rb +78 -0
  45. 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