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,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