opal-vienna 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'vienna/observable'
|
2
|
+
|
3
|
+
module Vienna
|
4
|
+
module ObservableArray
|
5
|
+
include Vienna::Observable
|
6
|
+
|
7
|
+
attr_reader :content
|
8
|
+
|
9
|
+
def initialize(content = [])
|
10
|
+
@content = content
|
11
|
+
end
|
12
|
+
|
13
|
+
def inspect
|
14
|
+
"#<ObservableArray: #{content.inspect}>"
|
15
|
+
end
|
16
|
+
|
17
|
+
alias to_s inspect
|
18
|
+
|
19
|
+
def array_content_did_change(idx, removed, added)
|
20
|
+
if observers = @array_observers
|
21
|
+
observers.each { |obj|
|
22
|
+
obj.array_did_change(self, idx, removed, added)
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
attribute_did_change :size
|
27
|
+
attribute_did_change :content
|
28
|
+
attribute_did_change :empty?
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_array_observer(obj)
|
32
|
+
(@array_observers ||= []) << obj
|
33
|
+
end
|
34
|
+
|
35
|
+
def <<(obj)
|
36
|
+
length = @content.length
|
37
|
+
@content << obj
|
38
|
+
|
39
|
+
array_content_did_change length, 0, 1
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def delete(obj)
|
44
|
+
if idx = @content.index(obj)
|
45
|
+
@content.delete_at idx
|
46
|
+
array_content_did_change idx, 1, 0
|
47
|
+
end
|
48
|
+
|
49
|
+
obj
|
50
|
+
end
|
51
|
+
|
52
|
+
def insert(idx, obj)
|
53
|
+
if idx > @content.length
|
54
|
+
raise ArgumentError, 'out of range'
|
55
|
+
end
|
56
|
+
|
57
|
+
@content.insert idx, obj
|
58
|
+
array_content_did_change idx, 0, 1
|
59
|
+
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
def clear
|
64
|
+
length = @content.length
|
65
|
+
@content.clear
|
66
|
+
|
67
|
+
array_content_did_change 0, length, 0
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
alias push <<
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'vienna/observable_array'
|
2
|
+
|
3
|
+
module Vienna
|
4
|
+
class RecordArray
|
5
|
+
include ObservableArray
|
6
|
+
|
7
|
+
attr_writer :content
|
8
|
+
|
9
|
+
def ==(arr)
|
10
|
+
if arr.respond_to? :content
|
11
|
+
@content == arr.content
|
12
|
+
else
|
13
|
+
@content == arr
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def method_missing(sym, *args, &block)
|
18
|
+
@content.__send__(sym, *args, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def each(&block)
|
22
|
+
@content.each(&block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def size
|
26
|
+
@content.size
|
27
|
+
end
|
28
|
+
|
29
|
+
alias length size
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'native'
|
2
|
+
|
3
|
+
module Vienna
|
4
|
+
class Router
|
5
|
+
attr_reader :path, :routes
|
6
|
+
|
7
|
+
def initialize(&block)
|
8
|
+
@routes = []
|
9
|
+
@location = $global.location
|
10
|
+
|
11
|
+
$global.addEventListener 'hashchange', -> { update }, false
|
12
|
+
|
13
|
+
instance_eval(&block) if block
|
14
|
+
end
|
15
|
+
|
16
|
+
def route(path, &handler)
|
17
|
+
route = Route.new(path, &handler)
|
18
|
+
@routes << route
|
19
|
+
route
|
20
|
+
end
|
21
|
+
|
22
|
+
def update
|
23
|
+
@path = if @location.hash.empty?
|
24
|
+
"/"
|
25
|
+
else
|
26
|
+
@location.hash.sub(/^#*/, '')
|
27
|
+
end
|
28
|
+
|
29
|
+
match @path
|
30
|
+
end
|
31
|
+
|
32
|
+
def match(path)
|
33
|
+
@routes.find { |r| r.match path }
|
34
|
+
end
|
35
|
+
|
36
|
+
# Navigate to the given hash location. This adds the '#'
|
37
|
+
# fragment to the start of the path
|
38
|
+
def navigate(path)
|
39
|
+
@location.hash = "##{path}"
|
40
|
+
end
|
41
|
+
|
42
|
+
class Route
|
43
|
+
# Regexp for matching named params in path
|
44
|
+
NAMED = /:(\w+)/
|
45
|
+
|
46
|
+
# Regexp for matching named splats in path
|
47
|
+
SPLAT = /\\\*(\w+)/
|
48
|
+
|
49
|
+
OPTIONAL = /\\\((.*?)\\\)/
|
50
|
+
|
51
|
+
attr_reader :regexp, :named
|
52
|
+
|
53
|
+
def initialize(pattern, &handler)
|
54
|
+
@named, @handler = [], handler
|
55
|
+
|
56
|
+
pattern = Regexp.escape pattern
|
57
|
+
pattern = pattern.gsub OPTIONAL, "(?:$1)?"
|
58
|
+
|
59
|
+
pattern.gsub(NAMED) { |m| @named << m[1..-1] }
|
60
|
+
pattern.gsub(SPLAT) { |m| @named << m[2..-1] }
|
61
|
+
|
62
|
+
pattern = pattern.gsub NAMED, "([^\\/]*)"
|
63
|
+
pattern = pattern.gsub SPLAT, "(.*?)"
|
64
|
+
|
65
|
+
@regexp = Regexp.new "^#{pattern}$"
|
66
|
+
end
|
67
|
+
|
68
|
+
# Return a hash of all named parts to values if route matches path, or
|
69
|
+
# nil otherwise
|
70
|
+
def match(path)
|
71
|
+
if match = @regexp.match(path)
|
72
|
+
params = {}
|
73
|
+
@named.each_with_index { |name, i| params[name] = match[i + 1] }
|
74
|
+
|
75
|
+
@handler.call params if @handler
|
76
|
+
|
77
|
+
return true
|
78
|
+
end
|
79
|
+
|
80
|
+
false
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'template'
|
2
|
+
require 'vienna/view'
|
3
|
+
require 'vienna/output_buffer'
|
4
|
+
require 'active_support/core_ext/string'
|
5
|
+
|
6
|
+
module Vienna
|
7
|
+
class TemplateView < View
|
8
|
+
def self.template(name = nil)
|
9
|
+
if name
|
10
|
+
@template = name
|
11
|
+
elsif @template
|
12
|
+
@template
|
13
|
+
elsif name = self.name
|
14
|
+
@template = name.sub(/View$/, '').demodulize.underscore
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def render
|
19
|
+
before_render
|
20
|
+
|
21
|
+
if template = Template[self.class.template]
|
22
|
+
element.html = _render_template(template)
|
23
|
+
end
|
24
|
+
|
25
|
+
after_render
|
26
|
+
end
|
27
|
+
|
28
|
+
def partial(name)
|
29
|
+
Template[name].render(self)
|
30
|
+
end
|
31
|
+
|
32
|
+
def _render_template(template)
|
33
|
+
@output_buffer = OutputBuffer.new
|
34
|
+
instance_exec @output_buffer, &template.body
|
35
|
+
end
|
36
|
+
|
37
|
+
def before_render; end
|
38
|
+
|
39
|
+
def after_render; end
|
40
|
+
end
|
41
|
+
end
|
data/opal/vienna/view.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
module Vienna
|
2
|
+
class View
|
3
|
+
def self.element(selector = nil)
|
4
|
+
selector ? @element = selector : @element
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.tag_name(tag = nil)
|
8
|
+
define_method(:tag_name) { tag } if tag
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.class_name(css_class = nil)
|
12
|
+
define_method(:class_name) { css_class } if css_class
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.events
|
16
|
+
@events ||= []
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.on(name, selector = nil, method = nil, &handler)
|
20
|
+
handler = proc { |evt| __send__(method, evt) } if method
|
21
|
+
events << [name, selector, handler]
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_accessor :parent
|
25
|
+
|
26
|
+
def element
|
27
|
+
return @element if @element
|
28
|
+
|
29
|
+
@element = create_element
|
30
|
+
@element.add_class class_name
|
31
|
+
setup_events
|
32
|
+
|
33
|
+
@element
|
34
|
+
end
|
35
|
+
|
36
|
+
def create_element
|
37
|
+
scope = (self.parent ? parent.element : Element)
|
38
|
+
|
39
|
+
if el = self.class.element
|
40
|
+
scope.find el
|
41
|
+
else
|
42
|
+
e = scope.new tag_name
|
43
|
+
e.add_class class_name
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def render
|
48
|
+
end
|
49
|
+
|
50
|
+
def class_name
|
51
|
+
""
|
52
|
+
end
|
53
|
+
|
54
|
+
def tag_name
|
55
|
+
"div"
|
56
|
+
end
|
57
|
+
|
58
|
+
def find(selector)
|
59
|
+
element.find(selector)
|
60
|
+
end
|
61
|
+
|
62
|
+
def setup_events
|
63
|
+
return @dom_events if @dom_events
|
64
|
+
|
65
|
+
el = element
|
66
|
+
@dom_events = self.class.events.map do |event|
|
67
|
+
name, selector, handler = event
|
68
|
+
wrapper = proc { |e| instance_exec(e, &handler) }
|
69
|
+
|
70
|
+
el.on(name, selector, &wrapper)
|
71
|
+
[name, selector, wrapper]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def teardown_events
|
76
|
+
el = element
|
77
|
+
@dom_events.each do |event|
|
78
|
+
name, selector, wrapper = event
|
79
|
+
el.off(name, selector, &wrapper)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def remove
|
84
|
+
element.remove
|
85
|
+
end
|
86
|
+
|
87
|
+
def destroy
|
88
|
+
teardown_events
|
89
|
+
remove
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class EventableSpec
|
4
|
+
include Vienna::Eventable
|
5
|
+
|
6
|
+
def events
|
7
|
+
@eventable
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe Vienna::Eventable do
|
12
|
+
subject { EventableSpec.new }
|
13
|
+
|
14
|
+
describe "#on" do
|
15
|
+
it "should register event handlers for given name" do
|
16
|
+
handler = Proc.new {}
|
17
|
+
subject.on(:foo, &handler)
|
18
|
+
|
19
|
+
expect(subject.events[:foo]).to eq([handler])
|
20
|
+
end
|
21
|
+
|
22
|
+
it "returns the given handler" do
|
23
|
+
handler = Proc.new {}
|
24
|
+
expect(subject.on(:foo, &handler)).to eq(handler)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "#off" do
|
29
|
+
it "has no affect if no handlers defined at all" do
|
30
|
+
subject.off(:bar, proc {})
|
31
|
+
subject.on(:foo) { raise "err" }
|
32
|
+
subject.off(:bar, proc {})
|
33
|
+
end
|
34
|
+
|
35
|
+
it "removes the handler for the event" do
|
36
|
+
called = false
|
37
|
+
handler = subject.on(:foo) { called = true }
|
38
|
+
|
39
|
+
subject.off(:foo, handler)
|
40
|
+
subject.trigger(:foo)
|
41
|
+
expect(called).to be_falsy
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "#trigger" do
|
46
|
+
it "should call handler" do
|
47
|
+
called = false
|
48
|
+
|
49
|
+
subject.on(:foo) { called = true }
|
50
|
+
called.should == false
|
51
|
+
|
52
|
+
subject.trigger(:foo)
|
53
|
+
called.should == true
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should pass all arguments to handler" do
|
57
|
+
args = nil
|
58
|
+
subject.on(:foo) { |*a| args = a }
|
59
|
+
|
60
|
+
subject.trigger(:foo)
|
61
|
+
args.should == []
|
62
|
+
|
63
|
+
subject.trigger(:foo, 1)
|
64
|
+
args.should == [1]
|
65
|
+
|
66
|
+
subject.trigger(:foo, 1, 2, 3)
|
67
|
+
args.should == [1, 2, 3]
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should allow multiple different events to be registered" do
|
71
|
+
result = []
|
72
|
+
subject.on(:foo) { result << :foo }
|
73
|
+
subject.on(:bar) { result << :bar }
|
74
|
+
|
75
|
+
subject.trigger(:foo)
|
76
|
+
result.should == [:foo]
|
77
|
+
|
78
|
+
subject.trigger(:bar)
|
79
|
+
result.should == [:foo, :bar]
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should allow multiple handlers for an event" do
|
83
|
+
count = 0
|
84
|
+
|
85
|
+
subject.on(:foo) { count += 1 }
|
86
|
+
subject.on(:foo) { count += 1 }
|
87
|
+
subject.on(:foo) { count += 1 }
|
88
|
+
subject.on(:foo) { count += 1 }
|
89
|
+
subject.trigger(:foo)
|
90
|
+
|
91
|
+
count.should == 4
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'vienna/history_router'
|
3
|
+
|
4
|
+
describe Vienna::HistoryRouter do
|
5
|
+
describe '#update' do
|
6
|
+
it 'updates path' do
|
7
|
+
subject.navigate('/foo')
|
8
|
+
expect(subject.path).to eq('/foo')
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'calls #match with the new path' do
|
12
|
+
expect(subject).to receive('match').with('/new_url')
|
13
|
+
subject.navigate('/new_url')
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#route' do
|
18
|
+
it 'should add a new route' do
|
19
|
+
subject.route('/users') {}
|
20
|
+
expect(subject.routes.size).to eq(1)
|
21
|
+
|
22
|
+
subject.route('/hosts') {}
|
23
|
+
expect(subject.routes.size).to eq(2)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#match' do
|
28
|
+
it 'returns nil when no routes on router' do
|
29
|
+
expect(subject.match('/foo')).to be_nil
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'returns the matching route for the path' do
|
33
|
+
a = subject.route('/foo') {}
|
34
|
+
b = subject.route('/bar') {}
|
35
|
+
|
36
|
+
expect(subject.match('/foo')).to eq(a)
|
37
|
+
expect(subject.match('/bar')).to eq(b)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'returns nil when no matching route' do
|
41
|
+
subject.route('/foo') {}
|
42
|
+
subject.route('/bar') {}
|
43
|
+
|
44
|
+
expect(subject.match('/baz')).to be_nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|