opal-vienna 0.7.0

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