opal-ferro 0.10.0 → 0.10.1

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.
@@ -1,36 +1,60 @@
1
- class FerroDocument
2
-
3
- include FerroElementary
4
-
5
- def initialize
6
- @children = {}
7
- creation
8
- # router.navigated
9
- end
10
-
11
- def parent
12
- self
13
- end
14
-
15
- def element
16
- @factory.body
17
- end
18
-
19
- def factory
20
- @factory ||= FerroFactory.new
1
+ # Module Ferro contains all Ferro functionality.
2
+ module Ferro
3
+ # This is the entry point for any Ferro application.
4
+ # It represents the top level object of the
5
+ # Master Object Model (MOM).
6
+ # There should be only one class that inhertits
7
+ # from FerroDocument in an application.
8
+ # This class attaches itself to the DOM
9
+ # `document.body` object.
10
+ # Any existing child nodes of `document.body` are removed.
11
+ class Document
12
+
13
+ include Elementary
14
+
15
+ # Create the document and start the creation
16
+ # process (casading).
17
+ def initialize
18
+ @children = {}
19
+ creation
20
+ end
21
+
22
+ # The document doesn't have a parent so returns itself.
23
+ def parent
24
+ self
25
+ end
26
+
27
+ # Returns the DOM element.
28
+ def element
29
+ factory.body
30
+ end
31
+
32
+ # Returns the one and only instance of the factory.
33
+ def factory
34
+ @factory ||= Factory.new
35
+ end
36
+
37
+ # The document class is the root element.
38
+ def root
39
+ self
40
+ end
41
+
42
+ # The document class is a component.
43
+ def component
44
+ self
45
+ end
46
+
47
+ # Returns the one and only instance of the router.
48
+ def router
49
+ @router ||= Router.new(method(:page404))
50
+ end
51
+
52
+ # Callback for the router when no matching routes
53
+ # can be found. Override this method to add custom
54
+ # behavior.
55
+ #
56
+ # @param [String] pathname The route that was not matched
57
+ # by the router
58
+ def page404(pathname);end
21
59
  end
22
-
23
- def root
24
- self
25
- end
26
-
27
- def component
28
- self
29
- end
30
-
31
- def router
32
- @router ||= FerroRouter.new(method(:page404))
33
- end
34
-
35
- def page404(pathname);end
36
- end
60
+ end
@@ -1,79 +1,115 @@
1
- module FerroElementary
2
-
3
- RESERVED_NAMES = %i[
4
- initialize factory root router page404
5
- creation _before_create before_create create _after_create
6
- after_create style _stylize cascade
7
- add_child forget_children remove_child method_missing destroy
8
- value set_text html parent children element domtype options
9
- add_states add_state update_state toggle_state state_active
10
- ]
11
-
12
- def creation
13
- _before_create
14
- before_create
15
- create
16
- _after_create
17
- after_create
18
- _stylize
19
- cascade
20
- end
21
-
22
- def _before_create;end
23
- def before_create;end
1
+ module Ferro
2
+ # Module defines element creation methods and child management.
3
+ # Note that there are no private methods in Opal. Methods that
4
+ # should be private are marked in the docs with 'Internal method'.
5
+ module Elementary
24
6
 
25
- def create
26
- @element = factory.create_element(self, @domtype, @parent, @options) if @domtype
27
- end
7
+ # Array of reseved names, child element should not have a name
8
+ # that is included in this list
9
+ RESERVED_NAMES = %i[
10
+ initialize factory root router page404
11
+ creation _before_create before_create create _after_create
12
+ after_create style _stylize cascade
13
+ add_child forget_children remove_child method_missing destroy
14
+ value set_text html parent children element domtype options
15
+ add_states add_state update_state toggle_state state_active
16
+ ]
28
17
 
29
- def _after_create;end
30
- def after_create;end
18
+ # Create DOM element and children elements.
19
+ # Calls before- and after create hooks.
20
+ def creation
21
+ _before_create
22
+ before_create
23
+ create
24
+ _after_create
25
+ after_create
26
+ _stylize
27
+ cascade
28
+ end
31
29
 
32
- def style;end
30
+ # Internal method.
31
+ def _before_create;end
33
32
 
34
- def _stylize
35
- styles = style
33
+ # Internal method.
34
+ def before_create;end
36
35
 
37
- if styles.class == Hash
38
- set_attribute(
39
- 'style',
40
- styles.map { |k, v| "#{k}:#{v};" }.join
41
- )
36
+ # Calls the factory to create the DOM element.
37
+ def create
38
+ @element = factory.create_element(self, @domtype, @parent, @options) if @domtype
42
39
  end
43
- end
44
40
 
45
- def cascade;end
41
+ # Internal method.
42
+ def _after_create;end
46
43
 
47
- def add_child(name, element_class, options = {})
48
- sym = symbolize(name)
49
- raise "Child '#{sym}' already defined" if @children.has_key?(sym)
50
- raise "Illegal name (#{sym})" if RESERVED_NAMES.include?(sym)
51
- @children[sym] = element_class.new(self, sym, options)
52
- end
44
+ # Internal method.
45
+ def after_create;end
53
46
 
54
- def symbolize(name)
55
- name.downcase.to_sym
56
- end
47
+ # Override this method to return a Hash of styles.
48
+ # Hash-key is the CSS style name, hash-value is the CSS style value.
49
+ def style;end
57
50
 
58
- def forget_children
59
- children = {}
60
- end
51
+ # Internal method.
52
+ def _stylize
53
+ styles = style
61
54
 
62
- def remove_child(sym)
63
- @children.delete(sym)
64
- end
55
+ if styles.class == Hash
56
+ set_attribute(
57
+ 'style',
58
+ styles.map { |k, v| "#{k}:#{v};" }.join
59
+ )
60
+ end
61
+ end
65
62
 
66
- def destroy
67
- `#{parent.element}.removeChild(#{element})`
68
- parent.remove_child(@sym)
69
- end
63
+ # Override this method to continue the MOM creation process.
64
+ def cascade;end
65
+
66
+ # Add a child element.
67
+ #
68
+ # @param [String] name A unique name for the element that is not
69
+ # in RESERVED_NAMES
70
+ # @param [String] element_class Ruby class name for the new element
71
+ # @param [Hash] options Options to pass to the element. Any option key
72
+ # that is not recognized is set as an attribute on the DOM element.
73
+ # Recognized keys are:
74
+ # prepend Prepend the new element before this DOM element
75
+ # content Add the value of content as a textnode to the DOM element
76
+ def add_child(name, element_class, options = {})
77
+ sym = symbolize(name)
78
+ raise "Child '#{sym}' already defined" if @children.has_key?(sym)
79
+ raise "Illegal name (#{sym})" if RESERVED_NAMES.include?(sym)
80
+ @children[sym] = element_class.new(self, sym, options)
81
+ end
82
+
83
+ # Convert a string containing a variable name to a symbol.
84
+ def symbolize(name)
85
+ name.downcase.to_sym
86
+ end
87
+
88
+ # Remove all child elements.
89
+ def forget_children
90
+ children = {}
91
+ end
92
+
93
+ # Remove a specific child element.
94
+ #
95
+ # param [Symbol] sym The element to remove
96
+ def remove_child(sym)
97
+ @children.delete(sym)
98
+ end
99
+
100
+ # Remove a DOM element.
101
+ def destroy
102
+ `#{parent.element}.removeChild(#{element})`
103
+ parent.remove_child(@sym)
104
+ end
70
105
 
71
- # Getter for children
72
- def method_missing(method_name, *args, &block)
73
- if @children.has_key?(method_name)
74
- @children[method_name]
75
- else
76
- super
106
+ # Getter for children.
107
+ def method_missing(method_name, *args, &block)
108
+ if @children.has_key?(method_name)
109
+ @children[method_name]
110
+ else
111
+ super
112
+ end
77
113
  end
78
114
  end
79
115
  end
@@ -1,58 +1,83 @@
1
- class FerroFactory
1
+ module Ferro
2
+ # Create DOM elements.
3
+ class Factory
2
4
 
3
- attr_reader :body
5
+ attr_reader :body
4
6
 
5
- def initialize
6
- @body = `document.body`
7
- `while (document.body.firstChild) {document.body.removeChild(document.body.firstChild);}`
8
- end
7
+ # Creates the factory. Do not create a factory directly, instead
8
+ # call the 'factory' method that is available in all Ferro classes
9
+ def initialize
10
+ @body = `document.body`
11
+ `while (document.body.firstChild) {document.body.removeChild(document.body.firstChild);}`
12
+ end
9
13
 
10
- def create_element(target, type, parent, options = {})
11
- # Create element
12
- element = `document.createElement(#{type})`
14
+ # Create a DOM element.
15
+ #
16
+ # @param [String] target The Ruby class instance
17
+ # @param [String] type Type op DOM element to create
18
+ # @param [String] parent The Ruby parent element
19
+ # @param [Hash] options Options to pass to the element.
20
+ # See FerroElementary::add_child
21
+ # @return [String] the DOM element
22
+ def create_element(target, type, parent, options = {})
23
+ # Create element
24
+ element = `document.createElement(#{type})`
13
25
 
14
- # Add element to DOM
15
- if options[:prepend]
16
- `#{parent.element}.insertBefore(#{element}, #{options[:prepend].element})`
17
- else
18
- `#{parent.element}.appendChild(#{element})`
19
- end
26
+ # Add element to DOM
27
+ if options[:prepend]
28
+ `#{parent.element}.insertBefore(#{element}, #{options[:prepend].element})`
29
+ else
30
+ `#{parent.element}.appendChild(#{element})`
31
+ end
20
32
 
21
- # Add ruby class to the node
22
- `#{element}.classList.add(#{dasherize(target.class.name)})`
33
+ # Add ruby class to the node
34
+ `#{element}.classList.add(#{dasherize(target.class.name)})`
23
35
 
24
- # Add ruby superclass to the node to allow for more generic styling
25
- if target.class.superclass != FerroElement
26
- `#{element}.classList.add(#{dasherize(target.class.superclass.name)})`
27
- end
36
+ # Add ruby superclass to the node to allow for more generic styling
37
+ if target.class.superclass != BaseElement
38
+ `#{element}.classList.add(#{dasherize(target.class.superclass.name)})`
39
+ end
28
40
 
29
- # Set ruby object_id as default element id
30
- if !options.has_key?(:id)
31
- `#{element}.id = #{target.object_id}`
32
- end
33
-
34
- # Set attributes
35
- options.each do |name, value|
36
- case name
37
- when :prepend
38
- nil
39
- when :content
40
- `#{element}.appendChild(document.createTextNode(#{value}))`
41
- else
42
- `#{element}.setAttribute(#{name}, #{value})`
41
+ # Set ruby object_id as default element id
42
+ if !options.has_key?(:id)
43
+ `#{element}.id = #{target.object_id}`
44
+ end
45
+
46
+ # Set attributes
47
+ options.each do |name, value|
48
+ case name
49
+ when :prepend
50
+ nil
51
+ when :content
52
+ `#{element}.appendChild(document.createTextNode(#{value}))`
53
+ else
54
+ `#{element}.setAttribute(#{name}, #{value})`
55
+ end
43
56
  end
57
+
58
+ element
44
59
  end
45
60
 
46
- element
47
- end
61
+ # Convert a Ruby classname to a dasherized name for use with CSS.
62
+ #
63
+ # @param [String] class_name The Ruby class name
64
+ # @return [String] CSS class name
65
+ def dasherize(class_name)
66
+ return class_name if class_name !~ /[A-Z:_]/
67
+ c = class_name.to_s.gsub('::', '')
48
68
 
49
- def dasherize(class_name)
50
- return class_name if class_name !~ /[A-Z_]/
51
- (class_name[0] + class_name[1..-1].gsub(/[A-Z]/){ |c| "-#{c}" }).downcase.gsub('_', '-')
52
- end
69
+ (c[0] + c[1..-1].gsub(/[A-Z]/){ |c| "-#{c}" }).
70
+ downcase.
71
+ gsub('_', '-')
72
+ end
53
73
 
54
- def camelize(class_name)
55
- return class_name if class_name !~ /-/
56
- class_name.gsub(/(?:-|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.strip
74
+ # Convert a CSS classname to a camelized Ruby class name.
75
+ #
76
+ # @param [String] class_name CSS class name
77
+ # @return [String] A Ruby class name
78
+ def camelize(class_name)
79
+ return class_name if class_name !~ /-/
80
+ class_name.gsub(/(?:-|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.strip
81
+ end
57
82
  end
58
83
  end
@@ -1,118 +1,177 @@
1
- require 'native'
2
-
3
- class FerroRouter
4
-
5
- def initialize(page404)
6
- @routes = []
7
- @page404 = page404
8
- setup_navigation_listener
9
- end
1
+ module Ferro
2
+
3
+ require 'native'
4
+
5
+ # Wrapper for the web browsers history API.
6
+ # Note that there are no private methods in Opal. Methods that
7
+ # should be private are marked in the docs with 'Internal method'.
8
+ class Router
9
+
10
+ # Create the router. Do not create a router directly, instead
11
+ # call the 'router' method that is available in all Ferro classes
12
+ # That method points to the router instance that is attached to
13
+ # the FerroDocument.
14
+ #
15
+ # @param [Method] page404 Method that is called when the web browser
16
+ # navigates and no route can be found. Override the
17
+ # page404 method in FerroDocument.
18
+ def initialize(page404)
19
+ @routes = []
20
+ @page404 = page404
21
+ setup_navigation_listener
22
+ end
10
23
 
11
- def add_route(path, callback)
12
- @routes << { parts: path_to_parts(path), callback: callback }
13
- end
24
+ # Add a new route to the router.
25
+ #
26
+ # Examples: when the following routes are created and the web browser
27
+ # navigates to a url matching the route, the callback method will be
28
+ # called with parameters:
29
+ # add_route('/ferro/page1', my_cb) # '/ferro/page1' => my_cb({})
30
+ # add_route('/user/:id', my_cb) # '/user/1' => my_cb({id: 1})
31
+ # add_route('/ferro', my_cb) # '/ferro?page=1' => my_cb({page: 1})
32
+ #
33
+ # @param [String] path Relative url (without protocol and host)
34
+ # @param [Method] callback Method that is called when the web browser
35
+ # navigates and the path is matched. The callback
36
+ # method should accept one parameter [Hash]
37
+ # containing the parameters found in the matched url
38
+ def add_route(path, callback)
39
+ @routes << { parts: path_to_parts(path), callback: callback }
40
+ end
14
41
 
15
- def setup_navigation_listener
16
- `window.onpopstate = function(e){#{navigated}}`
17
- end
42
+ # Internal method to set 'onpopstate'
43
+ def setup_navigation_listener
44
+ `window.onpopstate = function(e){#{navigated}}`
45
+ end
18
46
 
19
- def replace_state(url)
20
- `history.replaceState(null,null,#{url})`
21
- end
47
+ # Replace the current location in the web browsers history
48
+ #
49
+ # @param [String] url Relative url (without protocol and host)
50
+ def replace_state(url)
51
+ `history.replaceState(null,null,#{url})`
52
+ end
22
53
 
23
- def push_state(url)
24
- `history.pushState(null,null,#{url})`
25
- end
54
+ # Add a location to the web browsers history
55
+ #
56
+ # @param [String] url Relative url (without protocol and host)
57
+ def push_state(url)
58
+ `history.pushState(null,null,#{url})`
59
+ end
26
60
 
27
- def go_to(url)
28
- push_state(url)
29
- navigated
30
- end
61
+ # Navigate to url
62
+ #
63
+ # @param [String] url Relative url (without protocol and host)
64
+ def go_to(url)
65
+ push_state(url)
66
+ navigated
67
+ end
31
68
 
32
- def go_back
33
- `history.back()`
34
- end
69
+ # Navigate back
70
+ def go_back
71
+ `history.back()`
72
+ end
35
73
 
36
- def get_location
37
- Native(`new URL(window.location.href)`)
38
- end
74
+ # Internal method to get the new location
75
+ def get_location
76
+ Native(`new URL(window.location.href)`)
77
+ end
39
78
 
40
- def path_to_parts(path)
41
- path.
42
- downcase.
43
- split('/').
44
- map { |part| part.empty? ? nil : part.strip }.
45
- compact
46
- end
79
+ # Internal method to split a path into components
80
+ def path_to_parts(path)
81
+ path.
82
+ downcase.
83
+ split('/').
84
+ map { |part| part.empty? ? nil : part.strip }.
85
+ compact
86
+ end
47
87
 
48
- def decode(value)
49
- `decodeURI(#{value})`
50
- end
88
+ # URI decode a value
89
+ #
90
+ # @param [String] value Value to decode
91
+ def decode(value)
92
+ `decodeURI(#{value})`
93
+ end
51
94
 
52
- def navigated
53
- url = get_location
54
- @params = []
95
+ # Internal method called when the web browser navigates
96
+ def navigated
97
+ url = get_location
98
+ @params = []
55
99
 
56
- idx = match(path_to_parts(decode(url.pathname)), decode(url.search))
100
+ idx = match(path_to_parts(decode(url.pathname)), decode(url.search))
57
101
 
58
- if idx
59
- @routes[idx][:callback].call(@params)
60
- else
61
- @page404.call(url.pathname)
102
+ if idx
103
+ @routes[idx][:callback].call(@params)
104
+ else
105
+ @page404.call(url.pathname)
106
+ end
62
107
  end
63
- end
64
108
 
65
- def match(path, search)
66
- matches = get_matches(path)
109
+ # Internal method to match a path to the most likely route
110
+ #
111
+ # @param [String] path Url to match
112
+ # @param [String] search Url search parameters
113
+ def match(path, search)
114
+ matches = get_matches(path)
67
115
 
68
- if matches.length > 0
69
- match = matches.sort { |m| m[1] }.first
116
+ if matches.length > 0
117
+ match = matches.sort { |m| m[1] }.first
70
118
 
71
- @params = match[2]
72
- add_search_to_params(search)
119
+ @params = match[2]
120
+ add_search_to_params(search)
73
121
 
74
- match[0]
75
- else
76
- nil
122
+ match[0]
123
+ else
124
+ nil
125
+ end
77
126
  end
78
- end
79
127
 
80
- def get_matches(path)
81
- matches = []
128
+ # Internal method to match a path to possible routes
129
+ #
130
+ # @param [String] path Url to match
131
+ def get_matches(path)
132
+ matches = []
82
133
 
83
- @routes.each_with_index do |route, i|
84
- score, pars = score_route(route[:parts], path)
85
- matches << [i, score, pars] if score > 0
86
- end
134
+ @routes.each_with_index do |route, i|
135
+ score, pars = score_route(route[:parts], path)
136
+ matches << [i, score, pars] if score > 0
137
+ end
87
138
 
88
- matches
89
- end
139
+ matches
140
+ end
90
141
 
91
- def score_route(parts, path)
92
- score = 0
93
- pars = {}
94
-
95
- if parts.length == path.length
96
- parts.each_with_index do |part, i|
97
- if part[0] == ':'
98
- score += 1
99
- pars["#{part[1..-1]}"] = path[i]
100
- elsif part == path[i].downcase
101
- score += 2
142
+ # Internal method to add a match score
143
+ #
144
+ # @param [String] parts Parts of a route
145
+ # @param [String] path Url to match
146
+ def score_route(parts, path)
147
+ score = 0
148
+ pars = {}
149
+
150
+ if parts.length == path.length
151
+ parts.each_with_index do |part, i|
152
+ if part[0] == ':'
153
+ score += 1
154
+ pars["#{part[1..-1]}"] = path[i]
155
+ elsif part == path[i].downcase
156
+ score += 2
157
+ end
102
158
  end
103
159
  end
104
- end
105
160
 
106
- return score, pars
107
- end
161
+ return score, pars
162
+ end
108
163
 
109
- def add_search_to_params(search)
110
- if !search.empty?
111
- pars = search[1..-1].split('&')
164
+ # Internal method to split search parameters
165
+ #
166
+ # @param [String] search Url search parameters
167
+ def add_search_to_params(search)
168
+ if !search.empty?
169
+ pars = search[1..-1].split('&')
112
170
 
113
- pars.each do |par|
114
- pair = par.split('=')
115
- @params[ pair[0] ] = pair[1] if pair.length == 2
171
+ pars.each do |par|
172
+ pair = par.split('=')
173
+ @params[ pair[0] ] = pair[1] if pair.length == 2
174
+ end
116
175
  end
117
176
  end
118
177
  end