Funicular Image Uploader

Image upload component for Funicular.

The plugin only owns the browser-side interaction: file selection, object URL preview, optional FormData upload, and display of the current image URL. The Rails app owns persistence. The same component works with a controller that stores bytes in SQLite or one that attaches the upload to Active Storage.

Install

group :funicular do
  gem "funicular-image-uploader"
end

Render plugin assets from the Rails layout:

<%= funicular_plugin_include_tags %>

Deferred upload with a profile form

Use on_select when an image should be submitted together with other form fields:

component(
  Funicular::Plugins::ImageUploader::Component,
  src: current_user.has_avatar ? "/users/#{current_user.id}/avatar" : nil,
  input_id: "avatar-input",
  file_field: "avatar",
  image_class: "profile-avatar-image",
  input_class: "profile-avatar-input",
  on_select: ->(file, preview_url) { @selected_avatar_file = file }
)

If no classes are supplied, the plugin's default CSS styles the file selector, clear button, and upload button. Applications can pass class props when they need to align the component with their own design system.

The parent component can then send its form data with Funicular::FileUpload.upload_with_formdata.

Standalone upload

Use auto_upload: true when the image has its own endpoint:

component(
  Funicular::Plugins::ImageUploader::Component,
  src: "/users/#{current_user.id}/avatar",
  upload_url: "/users/#{current_user.id}/avatar",
  file_field: "avatar",
  auto_upload: true,
  on_upload: ->(result) { puts "uploaded" },
  on_error: ->(message, result) { puts message }
)

The endpoint should return JSON. If it includes image_url, the component uses that URL after upload. Override the key with response_url_key: "url".

Active Storage endpoint example

class Users::AvatarsController < ApplicationController
  before_action :require_login

  def update
    current_user.avatar.attach(params[:avatar])
    render json: {
      image_url: rails_blob_path(current_user.avatar, only_path: true)
    }
  end
end
resource :avatar, only: [:update], controller: "users/avatars"

For direct SQLite storage, read params[:avatar] in the controller and return the URL that serves the stored bytes.