Module: Ruflet::Rails
- Defined in:
- lib/ruflet/rails.rb,
lib/ruflet/rails.rb,
lib/ruflet/rails/assets.rb,
lib/ruflet/rails/railtie.rb,
lib/ruflet/rails/native_app.rb,
lib/ruflet/rails/route_stack.rb,
lib/ruflet/rails/webview_app.rb,
lib/ruflet/rails/form_helpers.rb,
lib/ruflet/rails/view_helpers.rb,
lib/ruflet/rails/configuration.rb,
lib/ruflet/rails/install_support.rb,
lib/ruflet/rails/protocol/runner.rb,
lib/ruflet/rails/desktop_launcher.rb,
lib/ruflet/rails/protocol/context.rb,
lib/ruflet/rails/protocol/web_app.rb,
lib/ruflet/rails/session_registry.rb,
lib/ruflet/rails/protocol/endpoint.rb,
lib/ruflet/rails/resource_component.rb,
lib/ruflet/rails/protocol/middleware.rb,
lib/ruflet/rails/protocol/wire_codec.rb,
lib/ruflet/rails/protocol/local_server.rb,
lib/ruflet/rails/protocol/mobile_loader.rb,
lib/ruflet/rails/protocol/web_app_endpoint.rb,
lib/ruflet/rails/protocol/websocket_detection.rb,
lib/ruflet/rails/protocol/web_socket_connection.rb
Defined Under Namespace
Modules: DesktopLauncher, FormHelpers, InstallSupport, Protocol, ViewHelpers Classes: Configuration, NativeApp, Railtie, ResourceComponent, RouteStack, Session, SessionRegistry, ViewRouter
Class Method Summary collapse
-
.app(file_path) ⇒ Object
Backward-compatible shorthand for a standalone app-file mobile endpoint.
- .asset_url(source, host: nil) ⇒ Object
-
.backend_url(host: nil) ⇒ Object
The base URL the Flutter client uses to reach this Rails app — the single source of truth for asset URLs, the build-time RUFLET_URL define and the desktop launcher.
- .broadcast(&block) ⇒ Object
-
.config ⇒ Object
Returns the global configuration object.
-
.configure {|config| ... } ⇒ Object
Yields the configuration object for block-style setup.
-
.endpoint(view: nil, app_file: nil, &block) ⇒ Object
WebSocket endpoint for native mobile/desktop clients.
-
.image_url(source, host: nil) ⇒ Object
Readability alias for image sources — identical resolution.
- .load_views(root) ⇒ Object
-
.mobile(file_path) ⇒ Object
Backward-compatible alias for older Rails installs.
-
.native_app(page, **opts) ⇒ Object
Start a Hotwire Native-style app.
- .register_view(view_class) ⇒ Object
- .render(page, routes: nil, default: nil) ⇒ Object
-
.routed(page, &builder) ⇒ Object
Flet-style routed navigation stack for complex multi-screen apps.
- .sessions ⇒ Object
-
.view_class_for(view) ⇒ Object
Resolved lazily on each session so Rails code reloading picks up edits.
- .view_classes ⇒ Object
-
.web(build:) ⇒ Object
Serves the pre-built Flutter web build’s index.html at the given route.
-
.web_app(view: nil, app_file: nil, build_dir: nil, &app_block) ⇒ Object
Self-contained web frontend, mountable under any route.
- .web_app_entrypoint(view: nil, app_file: nil) ⇒ Object
-
.web_html(request) ⇒ Object
Reads index.html from the web build dir, injects window.RUFLET_URL from config.backend_url so the Flutter client knows the WS backend without a rebuild.
- .webview_app(url:, appbar: nil, navigation_bar: nil, bottom_appbar: nil, route: "/", prevent_links: nil, on_navigate: nil, on_page_started: nil, on_page_ended: nil, **webview_props) {|body| ... } ⇒ Object
Class Method Details
.app(file_path) ⇒ Object
105 106 107 |
# File 'lib/ruflet/rails.rb', line 105 def app(file_path) endpoint(app_file: file_path) end |
.asset_url(source, host: nil) ⇒ Object
43 44 45 46 47 48 49 50 51 52 |
# File 'lib/ruflet/rails/assets.rb', line 43 def asset_url(source, host: nil) raw = source.to_s return raw if absolute_url?(raw) path = asset_pipeline_path(raw) return path if absolute_url?(path) base = backend_url(host: host) base.empty? ? path : "#{base}#{path}" end |
.backend_url(host: nil) ⇒ Object
The base URL the Flutter client uses to reach this Rails app — the single source of truth for asset URLs, the build-time RUFLET_URL define and the desktop launcher. A Rails Ruflet app always needs one, so this always resolves to a usable value:
1. an explicit host: argument
2. Ruflet::Rails.config.backend_url (set it in config/initializers/ruflet.rb)
3. the host the client connected on (the live WebSocket request)
Returns “” only when none of those are available (e.g. a build with no configured backend_url) — set config.backend_url to cover that case.
37 38 39 40 41 |
# File 'lib/ruflet/rails/assets.rb', line 37 def backend_url(host: nil) candidate = host || config.backend_url candidate = request_base_url if candidate.to_s.strip.empty? candidate.to_s.strip.sub(%r{/+\z}, "") end |
.broadcast(&block) ⇒ Object
64 65 66 |
# File 'lib/ruflet/rails.rb', line 64 def broadcast(&block) sessions.broadcast(&block) end |
.config ⇒ Object
Returns the global configuration object.
10 11 12 |
# File 'lib/ruflet/rails.rb', line 10 def config @config ||= Configuration.new end |
.configure {|config| ... } ⇒ Object
20 21 22 |
# File 'lib/ruflet/rails.rb', line 20 def configure yield config end |
.endpoint(view: nil, app_file: nil, &block) ⇒ Object
WebSocket endpoint for native mobile/desktop clients. The developer declares the entry the same way they declare a web mount — so the screens the app shows live in dev code, not in framework auto-discovery:
# a standalone Ruflet app file (Ruflet.run/MyApp.new.run), per session:
match "/ws", to: Ruflet::Rails.endpoint(app_file: Rails.root.join("app/ruflet/main.rb")), via: :all
# a single component/view class (resolved lazily, so reloading works):
match "/ws", to: Ruflet::Rails.endpoint(view: "ProductComponent"), via: :all
# a custom block:
match "/ws", to: Ruflet::Rails.endpoint { |page| MyHome.render(page) }, via: :all
With nothing declared it falls back to the convenience view router, which auto-discovers RufletView subclasses and renders a route index. That index is a framework fallback for the zero-config case — declare an entry above to own the home screen in your code.
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/ruflet/rails.rb', line 85 def endpoint(view: nil, app_file: nil, &block) sources = [view, app_file, block].compact raise ArgumentError, "endpoint accepts only one of view:, app_file:, or a block" if sources.length > 1 return Protocol::Runner.new.build_app_endpoint(file_path: app_file) if app_file entry = if block block elsif view web_app_entrypoint(view: view) else ->(page) { render(page) } end Protocol::Runner.new(&entry).build_endpoint end |
.image_url(source, host: nil) ⇒ Object
Readability alias for image sources — identical resolution.
55 56 57 |
# File 'lib/ruflet/rails/assets.rb', line 55 def image_url(source, host: nil) asset_url(source, host: host) end |
.load_views(root) ⇒ Object
49 50 51 52 53 54 55 56 57 58 |
# File 'lib/ruflet/rails.rb', line 49 def load_views(root) return [] if root.to_s.empty? files = Dir[File.join(root.to_s, "components", "**", "*.rb")].sort files += Dir[File.join(root.to_s, "**", "*_view.rb")].sort files.each do |file| Kernel.load(file) end end |
.mobile(file_path) ⇒ Object
Backward-compatible alias for older Rails installs.
110 111 112 |
# File 'lib/ruflet/rails.rb', line 110 def mobile(file_path) app(file_path) end |
.native_app(page, **opts) ⇒ Object
Start a Hotwire Native-style app. See NativeApp.
249 250 251 |
# File 'lib/ruflet/rails/native_app.rb', line 249 def native_app(page, **opts) NativeApp.new(page, **opts).start end |
.register_view(view_class) ⇒ Object
28 29 30 31 |
# File 'lib/ruflet/rails.rb', line 28 def register_view(view_class) view_classes << view_class unless view_classes.include?(view_class) view_class end |
.render(page, routes: nil, default: nil) ⇒ Object
33 34 35 |
# File 'lib/ruflet/rails.rb', line 33 def render(page, routes: nil, default: nil) ViewRouter.new(page, routes: routes, default: default).start end |
.routed(page, &builder) ⇒ Object
45 46 47 |
# File 'lib/ruflet/rails.rb', line 45 def routed(page, &builder) RouteStack.new(page, &builder).start end |
.sessions ⇒ Object
60 61 62 |
# File 'lib/ruflet/rails.rb', line 60 def sessions @sessions ||= SessionRegistry.new end |
.view_class_for(view) ⇒ Object
Resolved lazily on each session so Rails code reloading picks up edits.
163 164 165 166 167 |
# File 'lib/ruflet/rails.rb', line 163 def view_class_for(view) return view if view.is_a?(Class) view.to_s.constantize end |
.view_classes ⇒ Object
24 25 26 |
# File 'lib/ruflet/rails.rb', line 24 def view_classes @view_classes ||= [] end |
.web(build:) ⇒ Object
119 120 121 |
# File 'lib/ruflet/rails.rb', line 119 def web(build:) Protocol::WebAppEndpoint.new(build_dir: build.to_s) end |
.web_app(view: nil, app_file: nil, build_dir: nil, &app_block) ⇒ Object
Self-contained web frontend, mountable under any route. Serves the Flutter web build (with <base href> rewritten to the mount point) and answers the Ruflet WebSocket on the same path. Routes stay routing-only; UI code lives in app/views/ruflet:
# all registered RufletView subclasses behind the view router:
mount Ruflet::Rails.web_app, at: "/app"
# a single view class (resolved lazily, so reloading works):
mount Ruflet::Rails.web_app(view: "CounterView"), at: "/myfrontend"
# a standalone Ruflet app file (MyApp.new.run), loaded per session:
mount Ruflet::Rails.web_app(app_file: "app/ruflet/showcase/main.rb"), at: "/showcase"
136 137 138 139 140 141 142 143 144 145 |
# File 'lib/ruflet/rails.rb', line 136 def web_app(view: nil, app_file: nil, build_dir: nil, &app_block) sources = [view, app_file, app_block].compact raise ArgumentError, "web_app accepts only one of view:, app_file:, or a block" if sources.length > 1 Protocol::WebApp.new( build_dir: build_dir, entrypoint: web_app_entrypoint(view: view, app_file: app_file), &app_block ) end |
.web_app_entrypoint(view: nil, app_file: nil) ⇒ Object
147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/ruflet/rails.rb', line 147 def web_app_entrypoint(view: nil, app_file: nil) if view lambda do |page| view_class_for(view).render(page) end elsif app_file absolute = app_file.to_s lambda do |page, env| loaded = Protocol::MobileLoader.new(File.(absolute)).load! entry = loaded[:entrypoint] entry.arity == 1 ? entry.call(page) : entry.call(page, env) end end end |
.web_html(request) ⇒ Object
175 176 177 178 179 180 181 182 |
# File 'lib/ruflet/rails.rb', line 175 def web_html(request) build_dir = config.web_build_dir.to_s html = File.read(File.join(build_dir, "index.html")) url = config.backend_url.to_s.strip url = request.base_url if url.empty? script = "<script>window.__RUFLET_URL__=#{url.to_json};</script>" html.include?("</head>") ? html.sub("</head>", "#{script}</head>") : "#{script}#{html}" end |
.webview_app(url:, appbar: nil, navigation_bar: nil, bottom_appbar: nil, route: "/", prevent_links: nil, on_navigate: nil, on_page_started: nil, on_page_ended: nil, **webview_props) {|body| ... } ⇒ Object
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
# File 'lib/ruflet/rails/webview_app.rb', line 33 def webview_app(url:, appbar: nil, navigation_bar: nil, bottom_appbar: nil, route: "/", prevent_links: nil, on_navigate: nil, on_page_started: nil, on_page_ended: nil, **webview_props) webview_args = { url: url, expand: true } webview_args[:prevent_links] = prevent_links unless prevent_links.nil? webview_args[:on_url_change] = ->(event) { on_navigate.call(event.data) } if on_navigate webview_args[:on_page_started] = on_page_started if on_page_started webview_args[:on_page_ended] = on_page_ended if on_page_ended webview_args.merge!(webview_props) body = Ruflet::UI::ControlFactory.build(:webview, **webview_args) yield body if block_given? view_args = { route: route, controls: [body] } view_args[:appbar] = unless .nil? view_args[:navigation_bar] = unless .nil? view_args[:bottom_appbar] = unless .nil? Ruflet::UI::ControlFactory.build(:view, **view_args) end |