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