funicular 0.0.1 → 0.1.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 +4 -4
- data/CHANGELOG.md +56 -1
- data/README.md +58 -20
- data/Rakefile +74 -2
- data/demo/keymap_editor.html +582 -0
- data/demo/test_cable.html +179 -0
- data/demo/test_chartjs.html +235 -0
- data/demo/test_component.html +201 -0
- data/demo/test_diff_patch.html +146 -0
- data/demo/test_error_boundary.html +284 -0
- data/demo/test_router.html +257 -0
- data/demo/test_vdom.html +100 -0
- data/demo/tic-tac-toe.html +201 -0
- data/docs/README.md +419 -0
- data/docs/advanced-features.md +632 -0
- data/docs/architecture.md +409 -0
- data/docs/components-and-state.md +539 -0
- data/docs/data-fetching.md +528 -0
- data/docs/forms.md +446 -0
- data/docs/rails-integration.md +426 -0
- data/docs/realtime.md +543 -0
- data/docs/routing-and-navigation.md +427 -0
- data/docs/styling.md +285 -0
- data/exe/funicular +32 -0
- data/lib/funicular/assets/funicular.rb +21 -0
- data/lib/funicular/assets/funicular_debug.css +73 -0
- data/lib/funicular/assets/funicular_debug.js +183 -0
- data/lib/funicular/commands/routes.rb +69 -0
- data/lib/funicular/compiler.rb +135 -0
- data/lib/funicular/configuration.rb +76 -0
- data/lib/funicular/helpers/picoruby_helper.rb +50 -0
- data/lib/funicular/middleware.rb +98 -0
- data/lib/funicular/railtie.rb +26 -0
- data/lib/funicular/route_parser.rb +137 -0
- data/lib/funicular/vendor/picorbc/VERSION +1 -0
- data/lib/funicular/vendor/picorbc/picorbc.js +5283 -0
- data/lib/funicular/vendor/picorbc/picorbc.wasm +0 -0
- data/lib/funicular/vendor/picoruby/VERSION +1 -0
- data/lib/funicular/vendor/picoruby/debug/init.iife.js +130 -0
- data/lib/funicular/vendor/picoruby/debug/picoruby.js +6404 -0
- data/lib/funicular/vendor/picoruby/debug/picoruby.wasm +0 -0
- data/lib/funicular/vendor/picoruby/dist/init.iife.js +130 -0
- data/lib/funicular/vendor/picoruby/dist/picoruby.js +2 -0
- data/lib/funicular/vendor/picoruby/dist/picoruby.wasm +0 -0
- data/lib/funicular/version.rb +1 -1
- data/lib/funicular.rb +29 -1
- data/lib/tasks/funicular.rake +135 -0
- data/minitest/funicular_test.rb +13 -0
- data/minitest/test_helper.rb +7 -0
- data/mrbgem.rake +15 -0
- data/mrblib/cable.rb +417 -0
- data/mrblib/component.rb +911 -0
- data/mrblib/debug.rb +205 -0
- data/mrblib/differ.rb +244 -0
- data/mrblib/environment_inquirer.rb +34 -0
- data/mrblib/error_boundary.rb +125 -0
- data/mrblib/file_upload.rb +184 -0
- data/mrblib/form_builder.rb +284 -0
- data/mrblib/funicular.rb +156 -0
- data/mrblib/http.rb +89 -0
- data/mrblib/model.rb +146 -0
- data/mrblib/patcher.rb +203 -0
- data/mrblib/router.rb +229 -0
- data/mrblib/styles.rb +83 -0
- data/mrblib/vdom.rb +273 -0
- data/sig/cable.rbs +65 -0
- data/sig/component.rbs +141 -0
- data/sig/debug.rbs +28 -0
- data/sig/differ.rbs +18 -0
- data/sig/environment_iquirer.rbs +10 -0
- data/sig/error_boundary.rbs +14 -0
- data/sig/file_upload.rbs +18 -0
- data/sig/form_builder.rbs +29 -0
- data/sig/funicular.rbs +11 -1
- data/sig/http.rbs +22 -0
- data/sig/model.rbs +23 -0
- data/sig/patcher.rbs +15 -0
- data/sig/router.rbs +43 -0
- data/sig/styles.rbs +25 -0
- data/sig/vdom.rbs +59 -0
- metadata +119 -8
data/mrblib/vdom.rb
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
module Funicular
|
|
2
|
+
module VDOM
|
|
3
|
+
BOOLEAN_ATTRIBUTES = %w[
|
|
4
|
+
disabled checked selected readonly required autofocus multiple
|
|
5
|
+
]
|
|
6
|
+
|
|
7
|
+
URL_ATTRIBUTES = %w[href src action formaction data poster xlink:href]
|
|
8
|
+
|
|
9
|
+
class VNode
|
|
10
|
+
attr_reader :type, :key
|
|
11
|
+
|
|
12
|
+
def initialize(type)
|
|
13
|
+
@type = type
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class Element < VNode
|
|
18
|
+
attr_reader :tag, :props, :children
|
|
19
|
+
|
|
20
|
+
def initialize(tag, props = {}, children = [])
|
|
21
|
+
super(:element)
|
|
22
|
+
@tag = tag.to_s
|
|
23
|
+
@key = props.delete(:key)
|
|
24
|
+
@props = props || {}
|
|
25
|
+
@children = normalize_children(children || [])
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def normalize_children(children)
|
|
31
|
+
result = [] #: Array[child_t]
|
|
32
|
+
children.each do |child|
|
|
33
|
+
case child
|
|
34
|
+
when VNode
|
|
35
|
+
result << child
|
|
36
|
+
when String
|
|
37
|
+
result << child
|
|
38
|
+
when Array
|
|
39
|
+
# Flatten arrays (typically from .each or .map return values)
|
|
40
|
+
# Recursively normalize nested arrays
|
|
41
|
+
# @type var child: Array[Funicular::VDOM::child_t]
|
|
42
|
+
result.concat(normalize_children(child))
|
|
43
|
+
when nil
|
|
44
|
+
# Skip nil values
|
|
45
|
+
else
|
|
46
|
+
# Convert other types to strings
|
|
47
|
+
result << child.to_s
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
result
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def ==(other)
|
|
54
|
+
return false unless other.is_a?(Element)
|
|
55
|
+
@tag == other.tag && @props == other.props && @children == other.children
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class Text < VNode
|
|
60
|
+
attr_reader :content
|
|
61
|
+
|
|
62
|
+
def initialize(content)
|
|
63
|
+
super(:text)
|
|
64
|
+
@content = content.to_s
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def ==(other)
|
|
68
|
+
return false unless other.is_a?(Text)
|
|
69
|
+
@content == other.content
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class Component < VNode
|
|
74
|
+
attr_reader :component_class, :props
|
|
75
|
+
attr_accessor :instance
|
|
76
|
+
|
|
77
|
+
def initialize(component_class, props = {})
|
|
78
|
+
super(:component)
|
|
79
|
+
@component_class = component_class
|
|
80
|
+
@key = props.delete(:key)
|
|
81
|
+
@props = props
|
|
82
|
+
@instance = nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def ==(other)
|
|
86
|
+
return false unless other.is_a?(Component)
|
|
87
|
+
@component_class == other.component_class && @props == other.props
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
class Renderer
|
|
92
|
+
def initialize(doc = nil)
|
|
93
|
+
@doc = doc || JS.document
|
|
94
|
+
@error_boundary_stack = []
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def render(vnode, parent = nil)
|
|
98
|
+
case vnode&.type
|
|
99
|
+
when :element
|
|
100
|
+
# @type var vnode: Funicular::VDOM::Element
|
|
101
|
+
render_element(vnode, parent)
|
|
102
|
+
when :text
|
|
103
|
+
# @type var vnode: Funicular::VDOM::Text
|
|
104
|
+
render_text(vnode, parent)
|
|
105
|
+
when :component
|
|
106
|
+
# @type var vnode: Funicular::VDOM::Component
|
|
107
|
+
render_component(vnode, parent)
|
|
108
|
+
else
|
|
109
|
+
raise "Unknown vnode type: #{vnode&.type}"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
# Find the nearest error boundary instance on the stack
|
|
116
|
+
def current_error_boundary
|
|
117
|
+
@error_boundary_stack.last
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def render_element(element, parent)
|
|
121
|
+
dom_node = @doc.createElement(element.tag)
|
|
122
|
+
|
|
123
|
+
element.props.each do |key, value|
|
|
124
|
+
key_str = key.to_s
|
|
125
|
+
if key_str.start_with?('on')
|
|
126
|
+
# Event handlers are handled by Funicular::Component and should not be set as attributes.
|
|
127
|
+
# warn "Funicular: Attempted to set event handler '#{key_str}' as an attribute. This will be ignored."
|
|
128
|
+
elsif URL_ATTRIBUTES.include?(key_str) && value.to_s.strip.downcase.start_with?('javascript:')
|
|
129
|
+
# Prevent XSS attacks by blocking javascript: URIs in URL attributes
|
|
130
|
+
puts "[WARN] Funicular: Blocked potentially malicious value for attribute '#{key_str}'."
|
|
131
|
+
elsif BOOLEAN_ATTRIBUTES.include?(key_str)
|
|
132
|
+
# Handle boolean attributes
|
|
133
|
+
if value.nil? || value.to_s == "false"
|
|
134
|
+
# Do not set attribute (leave it absent)
|
|
135
|
+
else
|
|
136
|
+
dom_node.setAttribute(key_str, key_str)
|
|
137
|
+
end
|
|
138
|
+
else
|
|
139
|
+
# Attribute
|
|
140
|
+
dom_node.setAttribute(key_str, value.to_s)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
element.children.each do |child|
|
|
145
|
+
if child.is_a?(VNode)
|
|
146
|
+
child_dom = render(child)
|
|
147
|
+
dom_node.appendChild(child_dom)
|
|
148
|
+
elsif child.is_a?(String)
|
|
149
|
+
text_node = @doc.createTextNode(child)
|
|
150
|
+
dom_node.appendChild(text_node)
|
|
151
|
+
elsif child.is_a?(Array)
|
|
152
|
+
child.each do |c|
|
|
153
|
+
if c.is_a?(VNode)
|
|
154
|
+
child_dom = render(c)
|
|
155
|
+
dom_node.appendChild(child_dom)
|
|
156
|
+
elsif c.is_a?(String)
|
|
157
|
+
text_node = @doc.createTextNode(c)
|
|
158
|
+
dom_node.appendChild(text_node)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
parent.appendChild(dom_node) if parent
|
|
165
|
+
|
|
166
|
+
dom_node
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def render_text(text, parent)
|
|
170
|
+
dom_node = @doc.createTextNode(text.content)
|
|
171
|
+
parent.appendChild(dom_node) if parent
|
|
172
|
+
dom_node
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def render_component(component_vnode, parent)
|
|
176
|
+
instance = component_vnode.component_class.new(component_vnode.props)
|
|
177
|
+
component_vnode.instance = instance
|
|
178
|
+
|
|
179
|
+
is_error_boundary = instance.is_a?(Funicular::ErrorBoundary)
|
|
180
|
+
|
|
181
|
+
# Push error boundary to stack if this component is one
|
|
182
|
+
@error_boundary_stack.push(instance) if is_error_boundary
|
|
183
|
+
|
|
184
|
+
begin
|
|
185
|
+
component_vdom = instance.build_vdom
|
|
186
|
+
dom_node = render(component_vdom, parent)
|
|
187
|
+
|
|
188
|
+
# Check if this ErrorBoundary caught an error during child rendering
|
|
189
|
+
# If so, its @vdom was already set to fallback in the rescue block
|
|
190
|
+
error_was_caught = is_error_boundary && instance.error_caught_during_render
|
|
191
|
+
|
|
192
|
+
if error_was_caught
|
|
193
|
+
# ErrorBoundary caught an error - use the fallback vdom/dom that were set in rescue
|
|
194
|
+
# Note: The div.error-boundary-content created during initial render
|
|
195
|
+
# will be orphaned, but that's acceptable as it's not attached to the DOM
|
|
196
|
+
fallback_vdom = instance.vdom
|
|
197
|
+
fallback_dom = instance.dom_element
|
|
198
|
+
|
|
199
|
+
# Bind events on the fallback DOM
|
|
200
|
+
instance.bind_events(fallback_dom, fallback_vdom)
|
|
201
|
+
instance.collect_refs(fallback_dom, fallback_vdom)
|
|
202
|
+
|
|
203
|
+
# Return the fallback DOM
|
|
204
|
+
fallback_dom
|
|
205
|
+
else
|
|
206
|
+
# Normal case - store VDOM and DOM element
|
|
207
|
+
instance.vdom = component_vdom
|
|
208
|
+
instance.dom_element = dom_node
|
|
209
|
+
instance.bind_events(dom_node, component_vdom)
|
|
210
|
+
instance.collect_refs(dom_node, component_vdom)
|
|
211
|
+
dom_node
|
|
212
|
+
end
|
|
213
|
+
rescue => e
|
|
214
|
+
# Pop error boundary from stack before handling
|
|
215
|
+
@error_boundary_stack.pop if is_error_boundary
|
|
216
|
+
|
|
217
|
+
# Try to find an error boundary to handle this error
|
|
218
|
+
boundary = current_error_boundary
|
|
219
|
+
if boundary && !is_error_boundary
|
|
220
|
+
error_info = {
|
|
221
|
+
component_class: component_vnode.component_class.to_s,
|
|
222
|
+
props: component_vnode.props
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# Let the error boundary handle the error
|
|
226
|
+
boundary.catch_error(e, error_info)
|
|
227
|
+
|
|
228
|
+
# Re-render the error boundary with fallback UI
|
|
229
|
+
boundary_vdom = boundary.build_vdom
|
|
230
|
+
fallback_dom = render(boundary_vdom, nil)
|
|
231
|
+
|
|
232
|
+
# Update boundary's internal state
|
|
233
|
+
boundary.vdom = boundary_vdom
|
|
234
|
+
boundary.dom_element = fallback_dom
|
|
235
|
+
boundary.mounted = true
|
|
236
|
+
boundary.bind_events(fallback_dom, boundary_vdom)
|
|
237
|
+
|
|
238
|
+
fallback_dom
|
|
239
|
+
else
|
|
240
|
+
# No error boundary to catch this error, let it propagate
|
|
241
|
+
raise e
|
|
242
|
+
end
|
|
243
|
+
ensure
|
|
244
|
+
# Pop error boundary from stack after successful render
|
|
245
|
+
@error_boundary_stack.pop if is_error_boundary && @error_boundary_stack.last == instance
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def self.create_element(tag, props = {}, *children)
|
|
251
|
+
Element.new(tag, props, children.flatten)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def self.create_text(content)
|
|
255
|
+
Text.new(content)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def self.render(vnode, container)
|
|
259
|
+
renderer = Renderer.new
|
|
260
|
+
container.innerHTML = ''
|
|
261
|
+
renderer.render(vnode, container)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def self.diff(old_vnode, new_vnode)
|
|
265
|
+
Differ.diff(old_vnode, new_vnode)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def self.patch(element, patches)
|
|
269
|
+
patcher = Patcher.new
|
|
270
|
+
patcher.apply(element, patches)
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
data/sig/cable.rbs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module Funicular
|
|
2
|
+
module Cable
|
|
3
|
+
STORAGE_KEY: String
|
|
4
|
+
|
|
5
|
+
def self.create_consumer: (String url) -> Consumer
|
|
6
|
+
|
|
7
|
+
class Consumer
|
|
8
|
+
attr_reader url: String
|
|
9
|
+
attr_reader subscriptions: Subscriptions
|
|
10
|
+
@reconnect_attempts: Integer
|
|
11
|
+
|
|
12
|
+
def initialize: (String url) -> void
|
|
13
|
+
def connect: () -> void
|
|
14
|
+
def send_command: (Hash[Symbol, untyped] command) -> void
|
|
15
|
+
def disconnect: () -> void
|
|
16
|
+
def cleanup: () -> void
|
|
17
|
+
def cleanup_event_listeners: () -> void
|
|
18
|
+
|
|
19
|
+
private def handle_message: (String data) -> void
|
|
20
|
+
private def flush_pending_commands: () -> void
|
|
21
|
+
private def schedule_reconnect: () -> void
|
|
22
|
+
private def calculate_backoff_delay: () -> Integer
|
|
23
|
+
private def setup_visibility_handler: () -> void
|
|
24
|
+
private def schedule_suspend: () -> void
|
|
25
|
+
private def cancel_suspend: () -> void
|
|
26
|
+
private def suspend_connection: () -> void
|
|
27
|
+
private def ensure_connected: () -> void
|
|
28
|
+
private def setup_beforeunload_handler: () -> void
|
|
29
|
+
private def save_pending_to_storage: () -> void
|
|
30
|
+
private def load_pending_from_storage: () -> Array[Hash[Symbol, untyped]]
|
|
31
|
+
private def clear_pending_storage: () -> void
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class Subscriptions
|
|
35
|
+
@subscriptions: Hash[String, Subscription]
|
|
36
|
+
|
|
37
|
+
def initialize: (Consumer consumer) -> void
|
|
38
|
+
def create: (Hash[Symbol, untyped] params) ?{ (untyped message) -> void } -> Subscription
|
|
39
|
+
def find: (String identifier) -> Subscription?
|
|
40
|
+
def remove: (Subscription subscription) -> void
|
|
41
|
+
def notify_subscription_confirmed: (String identifier) -> void
|
|
42
|
+
def notify_subscription_rejected: (String identifier) -> void
|
|
43
|
+
def notify_message: (String identifier, untyped message) -> void
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class Subscription
|
|
47
|
+
attr_reader consumer: Consumer
|
|
48
|
+
attr_reader identifier: String
|
|
49
|
+
attr_reader params: Hash[Symbol, untyped]
|
|
50
|
+
|
|
51
|
+
def initialize: (Consumer consumer, String identifier, Hash[Symbol, untyped] params) ?{ (untyped message) -> void } -> void
|
|
52
|
+
def subscribe: () -> void
|
|
53
|
+
def unsubscribe: () -> void
|
|
54
|
+
def perform: (String action, ?Hash[Symbol, untyped] data) -> void
|
|
55
|
+
def on_connected: () { () -> void } -> void
|
|
56
|
+
def on_disconnected: () { () -> void } -> void
|
|
57
|
+
def on_rejected: () { () -> void } -> void
|
|
58
|
+
def notify_connected: () -> void
|
|
59
|
+
def notify_rejected: () -> void
|
|
60
|
+
def notify_received: (untyped message) -> void
|
|
61
|
+
|
|
62
|
+
private def generate_idempotency_key: () -> String
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
data/sig/component.rbs
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
module Funicular
|
|
2
|
+
class Component
|
|
3
|
+
class StateAccessor
|
|
4
|
+
def initialize: (Hash[Symbol, untyped] state_hash) -> void
|
|
5
|
+
def []: (Symbol key) -> untyped
|
|
6
|
+
def method_missing: (Symbol method, *untyped args) -> untyped
|
|
7
|
+
def respond_to_missing?: (Symbol method, ?bool include_private) -> bool
|
|
8
|
+
def errors: () -> Hash[Symbol, String]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
HTML_TAGS: Array[String]
|
|
12
|
+
|
|
13
|
+
attr_accessor props: Hash[Symbol, untyped]
|
|
14
|
+
attr_accessor vdom: VDOM::VNode | VDOM::Text | nil
|
|
15
|
+
attr_accessor dom_element: JS::Object
|
|
16
|
+
attr_accessor mounted: bool
|
|
17
|
+
attr_reader refs: Hash[Symbol, JS::Object]
|
|
18
|
+
@event_listeners: Array[Integer]
|
|
19
|
+
@child_components: Array[Component]
|
|
20
|
+
@suspense_data: Hash[Symbol, untyped]
|
|
21
|
+
@suspense_states: Hash[Symbol, Symbol]
|
|
22
|
+
@suspense_errors: Hash[Symbol, untyped]
|
|
23
|
+
@suspense_pending_timers: Array[Integer]
|
|
24
|
+
self.@suspense_definitions: Hash[Symbol, suspense_definition]
|
|
25
|
+
|
|
26
|
+
def initialize: (?Hash[Symbol, untyped] props) -> void
|
|
27
|
+
|
|
28
|
+
# Styles DSL class methods
|
|
29
|
+
def self.styles: () { (StyleBuilder) -> void } -> void
|
|
30
|
+
def self.styles_definitions: () -> Hash[Symbol, Hash[Symbol, untyped]]
|
|
31
|
+
|
|
32
|
+
# Suspense DSL class methods
|
|
33
|
+
# Note: Using untyped for Proc types to avoid instance_exec block type mismatch warnings
|
|
34
|
+
type suspense_definition = { loader: untyped, on_resolve: untyped, min_delay: Integer? }
|
|
35
|
+
def self.use_suspense: (Symbol name, untyped loader, ?on_resolve: untyped, ?min_delay: Integer) -> void
|
|
36
|
+
def self.suspense_definitions: () -> Hash[Symbol, suspense_definition]
|
|
37
|
+
|
|
38
|
+
def state: () -> StateAccessor
|
|
39
|
+
|
|
40
|
+
# Styles DSL instance method
|
|
41
|
+
def s: () -> StyleAccessor
|
|
42
|
+
|
|
43
|
+
# Public API
|
|
44
|
+
def initialize_state: () -> Hash[Symbol, untyped]
|
|
45
|
+
|
|
46
|
+
# Suspense instance methods
|
|
47
|
+
def load_suspense_data: () -> void
|
|
48
|
+
def load_single_suspense: (Symbol name, ?suspense_definition? definition) -> void
|
|
49
|
+
def reload_suspense: (Symbol name) -> void
|
|
50
|
+
def suspense_loading?: (*Symbol names) -> bool
|
|
51
|
+
def suspense_error?: (Symbol name) -> bool
|
|
52
|
+
def suspense_error: (Symbol name) -> untyped
|
|
53
|
+
def suspense: (fallback: ^() -> void, ?error: ^(untyped) -> void) { () -> void } -> void
|
|
54
|
+
|
|
55
|
+
def patch: (Hash[Symbol, untyped] new_state) -> void
|
|
56
|
+
def mount: (JS::Object container) -> void
|
|
57
|
+
def unmount: () -> void
|
|
58
|
+
def render: () -> (VDOM::VNode | String | Integer | Float | Array[untyped] | nil)
|
|
59
|
+
def bind_events: (JS::Object dom_element, VDOM::VNode | VDOM::Text | nil vnode) -> void
|
|
60
|
+
def build_vdom: () -> (VDOM::VNode | VDOM::Text | nil)
|
|
61
|
+
|
|
62
|
+
# Lifecycle hooks (public, can be overridden in subclasses)
|
|
63
|
+
def component_will_mount: () -> void
|
|
64
|
+
def component_mounted: () -> void
|
|
65
|
+
def component_will_update: () -> void
|
|
66
|
+
def component_updated: () -> void
|
|
67
|
+
def component_will_unmount: () -> void
|
|
68
|
+
def component_unmounted: () -> void
|
|
69
|
+
def component_raised: (Exception e) -> void
|
|
70
|
+
|
|
71
|
+
# Child component helper
|
|
72
|
+
def component: (Class component_class, ?Hash[Symbol, untyped] props) ?{ () -> untyped } -> VDOM::Component
|
|
73
|
+
|
|
74
|
+
# Rails-style form helper
|
|
75
|
+
def form_for: (Symbol model_key, ?Hash[Symbol, untyped] options) { (FormBuilder) -> void } -> VDOM::Element
|
|
76
|
+
|
|
77
|
+
# Rails-style routing helpers
|
|
78
|
+
def link_to: (String path, ?method: Symbol, ?navigate: bool, **untyped options) { -> untyped } -> VDOM::Element
|
|
79
|
+
def method_missing: (Symbol method, *untyped args) -> untyped
|
|
80
|
+
def respond_to_missing?: (Symbol method, ?bool include_private) -> bool
|
|
81
|
+
|
|
82
|
+
# Transition helpers
|
|
83
|
+
def remove_via: (String element_id, String from, String to, ?duration: Integer) ?{ () -> void } -> void
|
|
84
|
+
def add_via: (String element_id, String from, String to, ?duration: Integer) ?{ () -> void } -> void
|
|
85
|
+
|
|
86
|
+
# Private methods
|
|
87
|
+
private def handle_link_click: (String path) -> void
|
|
88
|
+
private def handle_link_with_method: (String path, Symbol method) -> void
|
|
89
|
+
private def handle_link_response: (HTTP::Response response, String path, Symbol method) -> void
|
|
90
|
+
private def normalize_state_value: (untyped value) -> untyped
|
|
91
|
+
private def re_render: () -> void
|
|
92
|
+
private def normalize_vnode: (untyped value) -> (VDOM::Element | VDOM::Text | VDOM::Component | nil)
|
|
93
|
+
private def add_data_component_attribute: (VDOM::VNode vnode) -> void
|
|
94
|
+
private def collect_refs: (JS::Object dom_element, VDOM::VNode | VDOM::Text | nil vnode, ?Hash[Symbol, JS::Object] refs_map) -> Hash[Symbol, JS::Object]
|
|
95
|
+
private def cleanup_events: () -> void
|
|
96
|
+
private def cleanup_suspense_timers: () -> void
|
|
97
|
+
private def add_child: (untyped child) -> void
|
|
98
|
+
private def collect_child_components: (VDOM::VNode | VDOM::Text | nil vnode) -> void
|
|
99
|
+
private def collect_child_components_recursive: (VDOM::VNode | VDOM::Text | nil vnode, Array[Component] components) -> void
|
|
100
|
+
|
|
101
|
+
# HTML element methods (DSL)
|
|
102
|
+
def div: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
103
|
+
def span: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
104
|
+
def p: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
105
|
+
def a: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
106
|
+
def h1: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
107
|
+
def h2: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
108
|
+
def h3: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
109
|
+
def h4: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
110
|
+
def h5: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
111
|
+
def h6: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
112
|
+
def ul: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
113
|
+
def ol: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
114
|
+
def li: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
115
|
+
def table: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
116
|
+
def thead: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
117
|
+
def tbody: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
118
|
+
def tr: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
119
|
+
def th: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
120
|
+
def td: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
121
|
+
def form: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
122
|
+
def input: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
123
|
+
def textarea: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
124
|
+
def button: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
125
|
+
def select: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
126
|
+
def option: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
127
|
+
def label: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
128
|
+
def header: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
129
|
+
def footer: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
130
|
+
def nav: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
131
|
+
def section: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
132
|
+
def article: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
133
|
+
def aside: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
134
|
+
def img: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
135
|
+
def video: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
136
|
+
def audio: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
137
|
+
def canvas: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
138
|
+
def br: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
139
|
+
def hr: (?Hash[Symbol, untyped] props) ?{ -> untyped } -> VDOM::Element
|
|
140
|
+
end
|
|
141
|
+
end
|
data/sig/debug.rbs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
$__funicular_debug__: Funicular::Debug
|
|
2
|
+
|
|
3
|
+
module Funicular
|
|
4
|
+
module Debug
|
|
5
|
+
self.@error_registry: Array[Hash[Symbol, untyped]]
|
|
6
|
+
|
|
7
|
+
def self.enabled?: () -> bool
|
|
8
|
+
def self.component_registry: () -> Hash[Integer, Funicular::Component]
|
|
9
|
+
def self.register_component: (Funicular::Component) -> Integer?
|
|
10
|
+
def self.unregister_component: (Integer) -> void
|
|
11
|
+
def self.get_component: (Integer) -> Funicular::Component?
|
|
12
|
+
def self.all_components: () -> Array[Funicular::Component]
|
|
13
|
+
def self.component_tree: () -> String
|
|
14
|
+
def self.get_component_state: (Integer) -> String
|
|
15
|
+
def self.get_component_instance_variables: (Integer) -> String
|
|
16
|
+
def self.expose_to_global: () -> void
|
|
17
|
+
def self.report_error: (ErrorBoundary boundary, Exception error, ?(Hash[Symbol, Component] | nil) error_info) -> Hash[Symbol, untyped]?
|
|
18
|
+
def self.clear_errors: () -> void
|
|
19
|
+
def self.error_list: () -> String
|
|
20
|
+
def self.last_error: () -> Hash[Symbol, untyped]?
|
|
21
|
+
def self.error_count: () -> Integer
|
|
22
|
+
|
|
23
|
+
private def self.error_registry: () -> Array[Hash[Symbol, untyped]]
|
|
24
|
+
private def self.get_state_keys: (Funicular::Component) -> Array[String]
|
|
25
|
+
private def self.get_child_ids: (Funicular::Component) -> Array[Integer]
|
|
26
|
+
private def self.collect_direct_children: (Funicular::VDOM::VNode, Array[Funicular::Component]) -> void
|
|
27
|
+
end
|
|
28
|
+
end
|
data/sig/differ.rbs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Funicular
|
|
2
|
+
module VDOM
|
|
3
|
+
class Differ
|
|
4
|
+
type children_t = Array[child_t]
|
|
5
|
+
def self.diff: (untyped old_node, untyped new_node) -> Array[patch_t]
|
|
6
|
+
|
|
7
|
+
private def self.diff_text: (Text old_node, Text new_node) -> Array[patch_t]
|
|
8
|
+
private def self.diff_element: (Element old_node, Element new_node) -> Array[patch_t]
|
|
9
|
+
private def self.diff_props: (Hash[Symbol, untyped] old_props, Hash[Symbol, untyped] new_props) -> Hash[Symbol, untyped]
|
|
10
|
+
private def self.diff_children: (children_t old_children, children_t new_children) -> Array[patch_t]
|
|
11
|
+
private def self.diff_children_with_keys: (children_t old_children, children_t new_children) -> Array[patch_t]
|
|
12
|
+
private def self.diff_children_by_index: (children_t old_children, children_t new_children) -> Array[patch_t]
|
|
13
|
+
private def self.diff_component: (VDOM::Component old_node, VDOM::Component new_node) -> Array[patch_t]
|
|
14
|
+
|
|
15
|
+
private def self.props_changed_excluding_procs?: (Hash[Symbol, untyped] old_props, Hash[Symbol, untyped] new_props) -> bool
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Funicular
|
|
2
|
+
class ErrorBoundary < Component
|
|
3
|
+
attr_accessor error_caught_during_render: bool?
|
|
4
|
+
|
|
5
|
+
def initialize_state: () -> Hash[Symbol, untyped]
|
|
6
|
+
def catch_error: (Exception error, ?Hash[Symbol, untyped]? error_info) -> bool
|
|
7
|
+
def reset: () -> void
|
|
8
|
+
def render: () -> (VDOM::VNode | String | Integer | Float | Array[untyped] | nil)
|
|
9
|
+
|
|
10
|
+
private def render_fallback: () -> (VDOM::VNode | String)
|
|
11
|
+
private def default_fallback: () -> VDOM::VNode
|
|
12
|
+
private def render_children: () -> VDOM::VNode
|
|
13
|
+
end
|
|
14
|
+
end
|
data/sig/file_upload.rbs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Funicular
|
|
2
|
+
module FileUpload
|
|
3
|
+
self.@callback_counters: Array[Integer]
|
|
4
|
+
|
|
5
|
+
JS_HELPER_CODE: String
|
|
6
|
+
|
|
7
|
+
def self.mount: () -> void
|
|
8
|
+
def self.select_file_with_preview: (String input_id) { (JS::Object? file, String? preview_url) -> void } -> void
|
|
9
|
+
def self.upload_with_formdata: (
|
|
10
|
+
String url,
|
|
11
|
+
?fields: Hash[untyped, untyped],
|
|
12
|
+
?file_field: String?,
|
|
13
|
+
?file: JS::Object?
|
|
14
|
+
) { (Hash[untyped, untyped] result) -> void } -> void
|
|
15
|
+
def self.store_file: (String input_id, ?String storage_key) -> JS::Object?
|
|
16
|
+
def self.retrieve_file: (?String storage_key) -> JS::Object?
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Funicular
|
|
2
|
+
class FormBuilder
|
|
3
|
+
attr_reader component: Component
|
|
4
|
+
attr_reader model_key: Symbol
|
|
5
|
+
attr_reader options: Hash[Symbol, untyped]
|
|
6
|
+
|
|
7
|
+
def initialize: (Component component, Symbol model_key, ?Hash[Symbol, untyped] options) -> void
|
|
8
|
+
|
|
9
|
+
# Field builder methods
|
|
10
|
+
def text_field: (Symbol field_name, ?Hash[Symbol, untyped] options) -> VDOM::Element
|
|
11
|
+
def password_field: (Symbol field_name, ?Hash[Symbol, untyped] options) -> VDOM::Element
|
|
12
|
+
def email_field: (Symbol field_name, ?Hash[Symbol, untyped] options) -> VDOM::Element
|
|
13
|
+
def number_field: (Symbol field_name, ?Hash[Symbol, untyped] options) -> VDOM::Element
|
|
14
|
+
def textarea: (Symbol field_name, ?Hash[Symbol, untyped] options) -> VDOM::Element
|
|
15
|
+
def checkbox: (Symbol field_name, ?Hash[Symbol, untyped] options) -> VDOM::Element
|
|
16
|
+
def select: (Symbol field_name, Array[untyped] choices, ?Hash[Symbol, untyped] options) -> VDOM::Element
|
|
17
|
+
def file_field: (Symbol field_name, ?Hash[Symbol, untyped] options) -> VDOM::Element
|
|
18
|
+
def submit: (?String label, ?Hash[Symbol, untyped] options) -> VDOM::Element
|
|
19
|
+
def label: (Symbol field_name, ?String? text, ?Hash[Symbol, untyped] options) -> VDOM::Element
|
|
20
|
+
|
|
21
|
+
# Generic field builder
|
|
22
|
+
private def build_field: (Symbol field_name, String field_type, Hash[Symbol, untyped] field_options) -> VDOM::Element
|
|
23
|
+
|
|
24
|
+
# Helper methods for nested state access
|
|
25
|
+
private def get_nested_value: (Component::StateAccessor state, String key_path) -> untyped
|
|
26
|
+
private def set_nested_value: (Symbol model_key, String field_key, untyped new_value) -> void
|
|
27
|
+
private def deep_merge_value: (Hash[Symbol, untyped] hash, Array[String] keys, untyped value) -> Hash[Symbol, untyped]
|
|
28
|
+
end
|
|
29
|
+
end
|
data/sig/funicular.rbs
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
module Funicular
|
|
2
2
|
VERSION: String
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
def self.version: () -> String
|
|
5
|
+
def self.env: () -> EnvironmentInquirer
|
|
6
|
+
def self.env=: (EnvironmentInquirer | String environment) -> EnvironmentInquirer?
|
|
7
|
+
def self.router: () -> Router?
|
|
8
|
+
def self.load_schemas: (Hash[singleton(Model), String] models) ?{ () -> void } -> void
|
|
9
|
+
def self.start: (?singleton(Component)? component_class, ?container: String | JS::Object, ?props: Hash[Symbol, untyped]) ?{ (Router router) -> void } -> (Component | Router)
|
|
10
|
+
def self.configure_forms: () ?{ (Hash[Symbol, String]) -> void } -> void
|
|
11
|
+
def self.configure_debug: () ?{ (self) -> void } -> void
|
|
12
|
+
def self.export_debug_config: () -> void
|
|
13
|
+
|
|
4
14
|
end
|
data/sig/http.rbs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Funicular
|
|
2
|
+
module HTTP
|
|
3
|
+
class Response
|
|
4
|
+
attr_reader data: untyped
|
|
5
|
+
attr_reader status: Integer
|
|
6
|
+
attr_reader ok: bool
|
|
7
|
+
|
|
8
|
+
def initialize: (Integer status, untyped data) -> void
|
|
9
|
+
def error?: () -> bool
|
|
10
|
+
def error_message: () -> String?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.get: (String url) { (Response) -> void } -> void
|
|
14
|
+
def self.post: (String url, ?Hash[untyped, untyped]? body) { (Response) -> void } -> void
|
|
15
|
+
def self.patch: (String url, ?Hash[untyped, untyped]? body) { (Response) -> void } -> void
|
|
16
|
+
def self.delete: (String url) { (Response) -> void } -> void
|
|
17
|
+
def self.put: (String url, ?Hash[untyped, untyped]? body) { (Response) -> void } -> void
|
|
18
|
+
def self.csrf_token: () -> String?
|
|
19
|
+
|
|
20
|
+
private def self.request: (String method, String url, Hash[untyped, untyped]? body) { (Response) -> void } -> void
|
|
21
|
+
end
|
|
22
|
+
end
|
data/sig/model.rbs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Funicular
|
|
2
|
+
class Model
|
|
3
|
+
attr_reader id: untyped
|
|
4
|
+
@changed_attributes: Hash[String, untyped]
|
|
5
|
+
|
|
6
|
+
def self.schema: () -> Hash[String, Hash[String, untyped]]
|
|
7
|
+
def self.schema=: (Hash[String, Hash[String, untyped]] schema) -> Hash[String, Hash[String, untyped]]
|
|
8
|
+
def self.endpoints: () -> Hash[String, Hash[String, String]]
|
|
9
|
+
def self.endpoints=: (Hash[String, Hash[String, String]] endpoints) -> Hash[String, Hash[String, String]]
|
|
10
|
+
|
|
11
|
+
def initialize: (?Hash[untyped, untyped] attributes) -> void
|
|
12
|
+
def self.load_schema: (Hash[String, untyped] schema_data) -> void
|
|
13
|
+
|
|
14
|
+
def self.all: (?Hash[untyped, untyped] params) ?{ (Array[Model]? instances, String? error) -> void } -> void
|
|
15
|
+
def self.find: (?untyped id, ?endpoint_name: String, ?model_class: singleton(Model)) ?{ (Model? instance, String? error) -> void } -> void
|
|
16
|
+
def self.create: (Hash[untyped, untyped] attrs, ?model_class: singleton(Model)) ?{ (Model? instance, String? error) -> void } -> void
|
|
17
|
+
def self.destroy: (?untyped id) ?{ (bool success, untyped result) -> void } -> void
|
|
18
|
+
|
|
19
|
+
def update: (?Hash[untyped, untyped]? attrs) ?{ (bool success, untyped result) -> void } -> void
|
|
20
|
+
def destroy: () ?{ (bool success, untyped result) -> void } -> void
|
|
21
|
+
def reload: () ?{ (Model? instance, String? error) -> void } -> void
|
|
22
|
+
end
|
|
23
|
+
end
|