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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b09d0611f88239ef252d838f98660fe7369dfb2e
4
+ data.tar.gz: 61de2285db362bd22937096c888a702888ebcf66
5
+ SHA512:
6
+ metadata.gz: f6a03da4dbd2644efa67ec614b32aa24ada741c9f811c827319684539452b5dee68e3c17acbcb62e903578171710e30b51e7d2b729cb3a594a0153f6fb2c1624
7
+ data.tar.gz: 4ca6629997042994b3487cecddcf918e77b67ebfdf8b8423731283760c22b7c2ca389fa844b26fd92a970e2d8a6e84c20b8f3c12637acd4b2a3455659aa406f0
@@ -0,0 +1,3 @@
1
+ .DS_Store
2
+ /build
3
+ Gemfile.lock
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 1.9.3
5
+ - 2.0.0
6
+
7
+ notifications:
8
+ irc: "irc.freenode.org#opal"
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'opal'
5
+ gem 'opal-jquery', :github => 'opal/opal-jquery'
6
+ gem 'opal-activesupport', :github => 'opal/opal-activesupport'
7
+ gem 'opal-rspec', '0.4.0.beta4'
@@ -0,0 +1,294 @@
1
+ # Vienna: Client side MVC framework for Opal
2
+
3
+ [![Build Status](https://travis-ci.org/opal/vienna.png?branch=master)](https://travis-ci.org/opal/vienna)
4
+
5
+ Until a better README is out (shame on us) you can take a look at
6
+ the [Opal implementation](https://github.com/opal/opal-todos)
7
+ of [TodoMVC](http://todomvc.com).
8
+
9
+ ## Installation
10
+
11
+ Add vienna to your ```Gemfile``` with a reference to the Github source.
12
+
13
+ Note: The vienna hosted on rubygems.org is a different project.
14
+
15
+ ```ruby
16
+ gem 'opal-vienna'
17
+ ```
18
+
19
+ If you're compiling opal in a static application, make sure to require bundler first.
20
+
21
+ ```ruby
22
+ require 'bundler'
23
+ Bundler.require
24
+ ```
25
+
26
+ ## Model
27
+
28
+ Client side models.
29
+
30
+ ```ruby
31
+ class Book < Vienna::Model
32
+ attributes :title, :author
33
+ end
34
+
35
+ book = Book.new(title: 'My awesome book', author: 'Bob')
36
+ book.title = 'Bob: A story of awesome'
37
+ ```
38
+
39
+ ### Attributes
40
+
41
+ Attributes can be defined on subclasses using `attributes`. This simply defines
42
+ a getter/setter method using `attr_accessor`. You can override either method as
43
+ expected:
44
+
45
+ ```ruby
46
+ class Book < Vienna::Model
47
+ attributes :title, :release_date
48
+
49
+ # If date is a string, then we need to parse it
50
+ def release_date=(date)
51
+ date = Date.parse(date) if String === date
52
+ @release_date = date
53
+ end
54
+ end
55
+
56
+ book = Book.new(:release_date => '2013-1-10')
57
+ book.release_date
58
+ # => #<Date: 2013-1-10>
59
+ ```
60
+
61
+ ## Views
62
+
63
+ `Vienna::View` is a simple wrapper class around a dom element representing a
64
+ view of some model (or models). A view's `element` is dynamically created when
65
+ first accessed. `View.element` can be used to specify a dom selector to find
66
+ the view in the dom.
67
+
68
+ Assuming the given html:
69
+
70
+ ```html
71
+ <body>
72
+ <div id="foo">
73
+ <span>Hi</span>
74
+ </div>
75
+ </body>
76
+ ```
77
+
78
+ We can create our view like so:
79
+
80
+ ```ruby
81
+ class MyView < Vienna::View
82
+ element '#foo'
83
+ end
84
+
85
+ MyView.new.element
86
+ # => #<Element: [<div id="foo">]>
87
+ ```
88
+
89
+ A real, existing, element can also be passed into the class method:
90
+
91
+ ```ruby
92
+ class MyView < Vienna::View
93
+ # Instances of this view will have the document as an element
94
+ element Document
95
+ end
96
+ ```
97
+
98
+ Views can have parents. If a child view is created, then the dom selector is
99
+ only searched inside the parents element.
100
+
101
+ ### Customizing elements
102
+
103
+ A `View` will render as a div tag, by default, with no classes (unless an
104
+ element selector is defined). Both these can be overriden inside your view
105
+ subclass.
106
+
107
+ ```ruby
108
+ class NavigationView < Vienna::View
109
+ def tag_name
110
+ :ul
111
+ end
112
+
113
+ def class_name
114
+ "navbar navbar-blue"
115
+ end
116
+ end
117
+ ```
118
+
119
+ ### Rendering views
120
+
121
+ Views have a placeholder `render` method, that doesnt do anything by default.
122
+ This is the place to put rendering logic.
123
+
124
+ ```ruby
125
+ class MyView < Vienna::View
126
+ def render
127
+ element.html = 'Welcome to my rubyicious page'
128
+ end
129
+ end
130
+
131
+ view = MyView.new
132
+ view.render
133
+
134
+ view.element
135
+ # => '<div>Welcome to my rubyicious page</div>'
136
+ ```
137
+
138
+ ### Listening for events
139
+
140
+ When an element is created, defined events can be added to it. When a view is
141
+ destroyed, these event handlers are then removed.
142
+
143
+ ```ruby
144
+ class ButtonView < Vienna::View
145
+ on :click do |evt|
146
+ puts "clicked on button"
147
+ end
148
+
149
+ def tag_name
150
+ :button
151
+ end
152
+ end
153
+ ```
154
+
155
+ For complex views, you can provide an optional css selector to scope the events:
156
+
157
+ ```ruby
158
+ class NavigationView < Vienna::View
159
+ on :click, 'ul.navbar li' do |evt|
160
+ puts "clicked: #{evt.target}"
161
+ end
162
+
163
+ on :mouseover, 'ul.navbar li.selected', :handle_mouseover
164
+
165
+ def handle_mouseover(evt)
166
+ # ...
167
+ end
168
+ end
169
+ ```
170
+
171
+ As you can see, you can specify a method to handle events instead of a block.
172
+
173
+ ### Customizing element creation
174
+
175
+ You can also override `create_element` if you wish to have any custom element
176
+ creation behaviour.
177
+
178
+ For example, a subview that is created from a parent element
179
+
180
+ ```ruby
181
+ class NavigationView < Vienna::View
182
+ def initialize(parent, selector)
183
+ @parent, @selector = parent, selector
184
+ end
185
+
186
+ def create_element
187
+ @parent.find(@selector)
188
+ end
189
+ end
190
+ ```
191
+
192
+ Assuming we have the html:
193
+
194
+ ```html
195
+ <div id="header">
196
+ <img id="logo" src="logo.png" />
197
+ <ul class="navigation">
198
+ <li>Homepage</li>
199
+ </ul>
200
+ </div>
201
+ ```
202
+
203
+ We can use the navigation view like this:
204
+
205
+ ```ruby
206
+ @header = Element.find '#header'
207
+ nav_view = NavigationView.new @header, '.navigation'
208
+
209
+ nav_view.element
210
+ # => [<ul class="navigation">]
211
+ ```
212
+
213
+ ## Router
214
+
215
+ `Vienna::Router` is a simple router that watches for hashchange events.
216
+
217
+ ```ruby
218
+ router = Vienna::Router.new
219
+
220
+ router.route("/users") do
221
+ puts "need to show all users"
222
+ end
223
+
224
+ router.route("/users/:id") do |params|
225
+ puts "need to show user: #{ params[:id] }"
226
+ end
227
+
228
+
229
+ # visit "example.com/#/users"
230
+ # visit "example.com/#/users/3"
231
+ # visit "example.com/#/users/5"
232
+
233
+ # => "need to show all users"
234
+ # => need to show user: 3
235
+ # => need to show user: 5
236
+ ```
237
+
238
+ ## Observable
239
+
240
+ Adds KVO style attribute observing.
241
+
242
+ ```ruby
243
+ class MyObject
244
+ include Vienna::Observable
245
+
246
+ attr_accessor :name
247
+ attr_reader :age
248
+
249
+ def age=(age)
250
+ @age = age + 10
251
+ end
252
+ end
253
+
254
+ obj = MyObject.new
255
+ obj.add_observer(:name) { |new_val| puts "name changed to #{new_val}" }
256
+ obj.add_observer(:age) { |new_age| puts "age changed to #{new_age}" }
257
+
258
+ obj.name = "bob"
259
+ obj.age = 42
260
+
261
+ # => "name changed to bob"
262
+ # => "age changed to 52"
263
+ ```
264
+
265
+ ## Observable Arrays
266
+
267
+ ```ruby
268
+ class MyArray
269
+ include Vienna::ObservableArray
270
+ end
271
+
272
+ array = MyArray.new
273
+
274
+ array.add_observer(:content) { |content| puts "content is now #{content}" }
275
+ array.add_observer(:size) { |size| puts "size is now #{size}" }
276
+
277
+ array << :foo
278
+ array << :bar
279
+
280
+ # => content is now [:foo]
281
+ # => size is now 1
282
+ # => content is now [:bar]
283
+ # => size is now 2
284
+ ```
285
+
286
+ #### Todo
287
+
288
+ * Support older browsers which do not support onhashchange.
289
+ * Support not-hash style routes with HTML5 routing.
290
+
291
+ ## License
292
+
293
+ MIT
294
+
@@ -0,0 +1,7 @@
1
+ require 'bundler'
2
+ Bundler.require
3
+ Bundler::GemHelper.install_tasks
4
+
5
+ require 'opal/activesupport'
6
+ require 'opal/rspec/rake_task'
7
+ Opal::RSpec::RakeTask.new(:default)
@@ -0,0 +1,8 @@
1
+ require 'bundler'
2
+ Bundler.require
3
+
4
+ run Opal::Server.new { |s|
5
+ s.main = 'opal/rspec/sprockets_runner'
6
+ s.append_path 'spec'
7
+ s.debug = false
8
+ }
@@ -0,0 +1 @@
1
+ require 'opal/vienna'
@@ -0,0 +1,7 @@
1
+ require 'opal'
2
+ require 'opal-jquery'
3
+ require 'opal-activesupport'
4
+ require 'opal/vienna/version'
5
+
6
+ # Just register our opal code path with opal build tools
7
+ Opal.append_path File.expand_path('../../../opal', __FILE__)
@@ -0,0 +1,5 @@
1
+ module Opal
2
+ module Vienna
3
+ VERSION = '0.7.0'
4
+ end
5
+ end
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $LOAD_PATH << File.expand_path('../lib', __FILE__)
3
+ require 'opal/vienna/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'opal-vienna'
7
+ s.version = Opal::Vienna::VERSION
8
+ s.author = 'Adam Beynon'
9
+ s.email = 'adam@adambeynon.com'
10
+ s.homepage = 'http://opalrb.org'
11
+ s.summary = 'Client side MVC framework for Opal'
12
+ s.description = 'Client side MVC framework for Opal'
13
+ s.license = 'MIT'
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.require_paths = ['lib']
19
+
20
+ s.add_dependency 'opal', ['>= 0.5.0', '< 1.0.0']
21
+ s.add_dependency 'opal-jquery'
22
+ s.add_dependency 'opal-activesupport'
23
+
24
+ s.add_development_dependency 'opal-rspec', '>= 0.2.1'
25
+ s.add_development_dependency 'rake'
26
+ end
@@ -0,0 +1,5 @@
1
+ require 'vienna/eventable'
2
+ require 'vienna/model'
3
+ require 'vienna/adapters/base'
4
+ require 'vienna/view'
5
+ require 'vienna/router'
@@ -0,0 +1,45 @@
1
+ module Vienna
2
+ class << self
3
+ attr_accessor :adapter
4
+ end
5
+
6
+ # An adapter is responsible for fetching and saving records from
7
+ # some endpoint. This base class acts as an interface which subclasses
8
+ # should develop. Adapters can be set on a per-model basis, or as an
9
+ # adapter for all models. Example adapter subclasses are RESTAdapter
10
+ # and LocalAdapter. A fixtures adapter is also provided for testing
11
+ # purposes.
12
+ class Adapter
13
+ def find(record, id, &block)
14
+ implement "find"
15
+ end
16
+
17
+ def load(record, id, &block)
18
+ implement "load"
19
+ end
20
+
21
+ def create_record(record)
22
+ implement "create_record"
23
+ end
24
+
25
+ def update_record(record)
26
+ implement "update_record"
27
+ end
28
+
29
+ def save_record(record)
30
+ implement "save_record"
31
+ end
32
+
33
+ def delete_record(record)
34
+ implement "delete_record"
35
+ end
36
+
37
+ def fetch
38
+ implement "fetch"
39
+ end
40
+
41
+ def implement(method)
42
+ raise NoMethodError, "Adapter subclass should implement `#{method}'"
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,50 @@
1
+ require 'vienna/adapters/base'
2
+
3
+ module Vienna
4
+ # Adapter using LocalStorage as a backend
5
+ class LocalAdapter < Adapter
6
+ def initialize
7
+ @storage = $global.localStorage
8
+ end
9
+
10
+ def create_record(record, &block)
11
+ record.id = self.unique_id
12
+
13
+ record.did_create
14
+ sync_models(record.class)
15
+
16
+ block.call(record) if block
17
+ end
18
+
19
+ def update_record(record, &block)
20
+ record.did_update
21
+ sync_models(record.class)
22
+
23
+ block.call(record) if block
24
+ end
25
+
26
+ def delete_record(record, &block)
27
+ record.did_destroy
28
+ sync_models record.class
29
+ block.call(record) if block
30
+ end
31
+
32
+ def find_all(klass, &block)
33
+ if data = @storage.getItem(klass.name)
34
+ models = JSON.parse(data).map { |m| klass.load(m) }
35
+ block.call(models) if block
36
+ end
37
+ end
38
+
39
+ # sync all records in given class to localstorage, now!
40
+ def sync_models(klass)
41
+ name = klass.name
42
+ @storage.setItem name, klass.all.to_json
43
+ end
44
+
45
+ # generate a new unique id.. just use timestamp for now
46
+ def unique_id
47
+ (Time.now.to_f * 1000).to_s
48
+ end
49
+ end
50
+ end