Funicular plugins are ordinary Ruby gems that provide client-side Ruby source for a Rails app. A plugin can package reusable components, stores, models, helpers, and browser assets such as CSS.
There is no plugin generator. A Funicular plugin is intentionally just a gem with a small convention:
- Ruby source lives under
lib/. - CSS assets, when needed, live under
assets/. - A Rails app opts in by putting the gem in the
:funicularBundler group.
Using a plugin in Rails
Add the plugin gem to a dedicated :funicular group:
# Gemfile
group :funicular do
gem "funicular-datepicker"
end
During bin/rails funicular:compile, Funicular asks Bundler for the gems in that group. Each plugin’s lib/**/*.rb files are loaded before the Rails app’s app/funicular/**/*.rb files and compiled into the same app.mrb.
That means application code can use plugin constants directly:
class SettingsComponent < Funicular::Component
def render
component Funicular::Plugins::DatePicker::Component,
value: state.birthday,
on_change: ->(value) { patch(birthday: value) }
end
end
For local development, use a path gem:
group :funicular do
gem "funicular-datepicker"
end
Run bundle install after changing the Gemfile.
Plugin layout
A small component plugin can look like this:
funicular-datepicker/
funicular-datepicker.gemspec
lib/
date_picker.rb
components/
date_picker_component.rb
assets/
date_picker.css
The source order is nested files first, then top-level files. That lets a top-level entry file refer to classes defined below a subdirectory:
# lib/date_picker.rb
module Funicular
module Plugins
module DatePicker
Component = DatePickerComponent
end
end
end
# lib/components/date_picker_component.rb
class DatePickerComponent < Funicular::Component
def render
div(class: "funicular-date-picker") do
"Date picker"
end
end
end
Plugin CSS is copied into the Rails app’s asset build directory when Funicular compiles:
app/assets/builds/funicular/plugins/funicular_datepicker/date_picker.css
Render plugin browser assets from the Rails layout:
<%= funicular_plugin_include_tags %>
Plugin gemspec
A Funicular plugin does not need mrbgem.rake. It is a CRubygem: Ruby source is kept as source, then Funicular compiles it together with the host app.
The gemspec only needs to package the source and assets:
# funicular-datepicker.gemspec
Gem::Specification.new do |spec|
spec.name = "funicular-datepicker"
spec.version = "0.1.0"
spec.summary = "Date picker component for Funicular"
spec.license = "MIT"
spec.files = Dir[
"assets/**/*",
"lib/**/*.rb",
"README.md"
].reject { |path| File.directory?(path) }
spec.require_paths = ["lib"]
end
Testing a plugin
Plugins can use the same PicoRuby.wasm + picotest runner used by Rails apps. The plugin does not need a dummy Rails application.
Add Funicular as a test dependency:
# Gemfile
source "https://rubygems.org"
group :test do
gem "funicular"
end
Add jsdom for the Node.js DOM environment:
{
"private": true,
"devDependencies": {
"jsdom": "^26.1.0"
}
}
Install dependencies:
bundle install
npm install
Use a normal Minitest wrapper to call Funicular::Testing.run!:
# test/funicular_plugin_test.rb
require_relative "test_helper"
class FunicularPluginTest < Minitest::Test
def test_picotests
result = Funicular::Testing.run!(
app_root: File.expand_path("..", __dir__),
source_dir: File.expand_path("../lib", __dir__),
test_glob: "test/**/*_picotest.rb",
timeout_ms: 10_000
)
assert result.success?, result.output
end
end
Then write plugin picotests under test/:
# test/datepicker_picotest.rb
class DatePickerComponentTest < Funicular::Testing::DOMTest
def test_calendar_button_opens_panel
mount Funicular::Plugins::DatePicker::Component, value: "2000-01-02"
click ".funicular-date-picker__button"
assert_selector ".funicular-date-picker__panel"
end
end
Run the plugin tests with:
bundle exec rake test
The runner loads the plugin’s lib/**/*.rb files, then loads
test/**/*_picotest.rb, mounts components into a jsdom document, and returns
the picotest result to Minitest.
Notes
- Keep plugin code browser-compatible. It is compiled for PicoRuby.wasm, not executed as normal server-side Ruby.
- Do not put plugin gems in the default Gemfile group unless the Rails server also needs to load them.
- The
:funiculargroup is a build-time convention. It tells Funicular which gems should be compiled into the browser bundle. - Testing a plugin requires a small amount of local setup:
funicularin the test bundle andjsdominpackage.json. This is documented convention rather than generated scaffolding.
In the demo
funicular-demo uses a local datepicker plugin and a local image uploader plugin:
group :funicular do
gem "funicular-datepicker"
gem "funicular-image-uploader"
end
funicular-image-uploader demonstrates the recommended shape for upload
plugins. The component handles browser-side file selection, preview, and
FormData upload, while the Rails application owns persistence. The receiving
controller can store bytes directly, attach to Active Storage, or forward the
file to another service as long as it accepts the configured file field and
returns JSON.