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