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
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,294 @@
|
|
1
|
+
# Vienna: Client side MVC framework for Opal
|
2
|
+
|
3
|
+
[](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
|
+
|
data/Rakefile
ADDED
data/config.ru
ADDED
data/lib/opal-vienna.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'opal/vienna'
|
data/lib/opal/vienna.rb
ADDED
data/opal-vienna.gemspec
ADDED
@@ -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
|
data/opal/vienna.rb
ADDED
@@ -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
|