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,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,13 @@
1
+ require 'template'
2
+
3
+ module Vienna
4
+ class OutputBuffer < Template::OutputBuffer
5
+ def capture(*args, &block)
6
+ old = @buffer
7
+ tmp = @buffer = []
8
+ yield(*args) if block_given?
9
+ @buffer = old
10
+ tmp.join
11
+ end
12
+ end
13
+ 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
@@ -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