<div class="lui-image-wrapper"> <turbo-frame id="multiple_image_images_pokemon_1_full"> <div data-controller="images" data-images-editable-value="true" data-images-standalone-value="false" data-images-form-id-value="" data-images-with-attached-value="false" data-images-turbo-frame-id-value="multiple_image_images_pokemon_1_full" data-images-unique-id-value="multiple_image_images_pokemon_1_full" data-images-list-view-value="false" data-images-has-many-value="true" data-images-force-new-value="false" data-images-submit-on-upload-value="true" data-images-direct-upload-authorization-value="" data-images-direct-upload-url-value="/rails/active_storage/direct_uploads" data-images-max-size-error-message-value="Image is too big." data-images-max-size-batch-error-message-value="Images batch is too big." data-images-urls-value="{"attach":"/loopos_ui/images","detach":"/loopos_ui/images"}"> <div class="lui-image lui-image--full" data-images-target="imageComponent" data-action="pubsub:action@window->images#executeAction"> <div role="status" class="hidden lui-image__loader" data-images-target="loader" > <span class="lui-button__tooltip-wrapper inline-flex w-fit"> <button class="lui-button lui-button--icon-only lui-button--neutral--secondary lui-button--size-tiny lui-button--disabled w-fit w-fit relative" data-controller="lui--button" disabled="disabled"> <div class="opacity-100 inline-flex" data-lui--button-target="leadingIcon"> <div class="flex items-center justify-center" style="width: 12px; height: 12px;"><i class="lui-button__icon lui-button__icon--tiny fa-regular fa-spinner" data-lui--button-target="leadingIcon"></i></div> </div> <div class="absolute w-full flex items-center justify-center opacity-0" data-lui--button-target="loadingIcon"> <i class="lui-m_icon animate-spin material-symbols-rounded" style="--lui-micon-size: 12px;"> progress_activity </i> </div> </button></span> </div> <img class="hidden lui-image__image" data-images-target="image" src="" /> <div class="lui-image__placeholder lui-image__placeholder--editable flex" data-images-target="placeholder" > <div class="lui-icon h-[16px] w-[16px]"> <i class="fa-regular fa-image lui-icon__icon" style="font-size: 16px; color: white;"></i> </div> </div> <div class="overflow-visible lui-image__image-edit"> <form data-images-target="form" enctype="multipart/form-data" action="#" accept-charset="UTF-8" method="post"><input type="hidden" name="authenticity_token" value="vMh0zOBlR0lIUzCfgUi3Ntb-SXoA0mtGLeGCzfRJxa0tXLbXGuo08sTpWSULJxy2dUx6BWYqhh5yTyAbWaDIig" /> <input type="hidden" name="authenticity_token" id="authenticity_token" value="E58Isco4NrD4P7LxElnVjBoxgyyn_LcVFb4nTEN2B0v77umsqxdTtVecouwS_RCRn9gRpz6C8DAL1cfcXQZBRg" /> <input type="hidden" autocomplete="off" value="4PCQWnaHhVD3Guj77C/o1FoZfCyljwzbk6IVEXrWL4bwgbc5i7tINYa/ZAgLqi7l6U/3jmAzswfkmdM8EG2esRpD09d+Md0kO2oLIn0DIx57TjYAxE3xzvtUi317WCbbJEsD69uJJqM=--aXoNFcrFYQ72bEd0--DUZDf+JjjXBKsK1MYutXeg==" data-images-target="uploadContext" name="context"> <div class="hidden" data-images-target="noImageUploader"> <span class="lui-button__tooltip-wrapper inline-flex w-fit"> <div class="lui-tooltip hidden" data-controller="tooltips" data-tooltips-tippy-target-id-value="" data-tooltips-position-value="top" data-tooltips-interactive-value="false" > <div class="lui-tooltip__title"> Upload images here </div> </div> <button class="lui-button lui-button--icon-only lui-button--neutral--secondary lui-button--size-tiny w-fit w-fit relative" data-controller="lui--button" data-action="click->images#openFilePickerMultiple"> <div class="opacity-100 inline-flex" data-lui--button-target="leadingIcon"> <div class="flex items-center justify-center" style="width: 12px; height: 12px;"><i class="lui-button__icon lui-button__icon--tiny fa-regular fa-upload" data-lui--button-target="leadingIcon"></i></div> </div> <div class="absolute w-full flex items-center justify-center opacity-0" data-lui--button-target="loadingIcon"> <i class="lui-m_icon animate-spin material-symbols-rounded" style="--lui-micon-size: 12px;"> progress_activity </i> </div> </button></span> </div> <div class="hidden" data-images-target="withImageUploader"> <div data-controller="action-menu" data-action="modal:open@window->action-menu#disableTippy modal:close@window->action-menu#enableTippy" data-action-menu-placement-value="bottom" class="lui-action-menu"> <div data-action-menu-target="trigger" class="flex"> <span class="lui-button__tooltip-wrapper inline-flex w-fit"> <button class="lui-button lui-button--icon-only lui-button--neutral--secondary lui-button--size-tiny w-fit w-fit relative" data-controller="lui--button" type="button"> <div class="opacity-100 inline-flex" data-lui--button-target="leadingIcon"> <div class="flex items-center justify-center" style="width: 12px; height: 12px;"><i class="lui-button__icon lui-button__icon--tiny fa-regular fa-ellipsis-vertical" data-lui--button-target="leadingIcon"></i></div> </div> <div class="absolute w-full flex items-center justify-center opacity-0" data-lui--button-target="loadingIcon"> <i class="lui-m_icon animate-spin material-symbols-rounded" style="--lui-micon-size: 12px;"> progress_activity </i> </div> </button></span> </div> <div data-action-menu-target="menu" class="hidden lui-action-menu__wrapper" data-controller="modal form-submit pubsub"> <div class="lui-action-menu__options" role="menu" aria-orientation="vertical" aria-labelledby="options-menu"> <div class="contents cursor-pointer"> <div class="lui-action-menu__option " data-action="click->pubsub#publish" data-pubsub-unique-id-param="multiple_image_images_pokemon_1_full" data-pubsub-action-param="openFilePicker"> <div class="lui-action-menu__option-text cursor-default"> <i class="fa-regular fa-arrows-rotate"></i> <span>Upload new image</span> </div> </div> </div> <div class="contents cursor-pointer"> <div class="lui-action-menu__option " data-action="click->pubsub#publish" data-pubsub-unique-id-param="multiple_image_images_pokemon_1_full" data-pubsub-action-param="openFilePickerMultiple"> <div class="lui-action-menu__option-text cursor-default"> <i class="fa-regular fa-plus"></i> <span>Add more images</span> </div> </div> </div> <div class="contents cursor-pointer"> <div class="lui-action-menu__option " data-action="click->pubsub#publish" data-pubsub-unique-id-param="multiple_image_images_pokemon_1_full" data-pubsub-action-param="detachImage"> <div class="lui-action-menu__option-text cursor-default"> <i class="fa-regular fa-trash"></i> <span>Remove image</span> </div> </div> </div> </div> </div> </div> </div> <input accept="image/jpeg,image/png,image/webp" class="hidden" data-direct-upload-url="/rails/active_storage/direct_uploads" data-images-target="file" data-action="change->images#previewAndSubmit" type="file" name="file" id="file" /> <input name="files[][]" type="hidden" value="" /><input accept="image/jpeg,image/png,image/webp" multiple="multiple" class="hidden" data-direct-upload-url="/rails/active_storage/direct_uploads" data-images-target="fileMultiple" data-action="change->images#previewAndSubmitMultiple" type="file" name="files[][]" id="files[]" /> <input type="submit" name="commit" value="Save" data-images-target="submit" data-disable-with="Save" class="hidden" /> </form> </div> </div> <span class="lui-image__error hidden" data-images-target="error"></span> </div> </turbo-frame></div>No Usage documentation to display.
<% pokemon = Pokemon.first pokemon_args = { model: pokemon, association: :images, # size: :small, list_view: params[:list_view] || false, }%><% pokemon.images.purge if params.delete(:delete_all_images).present? %><%= render LooposUi::V2::Image.new( **pokemon_args, editable: true, help: params[:help].presence, error: params[:error].presence,) %>No notes provided.
| Param | Description | Input |
|---|---|---|
|
— |
|
|
|
— |
|
|
|
Helper text below the image input |
|
|
|
Validation error message |
|
Description
The Image V2 component is an enhanced version of the base Image component that provides image upload and management capabilities. It supports both single and multiple image handling, with options for different sizes, shapes, and editing capabilities.
Arguments
| Property | Default | Description |
|---|---|---|
model |
nil |
ActiveRecord model that the image belongs to |
association |
nil |
Name of the image association on the model (:image or :images) |
editable |
false |
Enables image upload and management controls |
size |
:full |
Size of the image container (:full or :small) |
rounded |
false |
Makes the image circular when true |
image_url |
nil |
Direct URL for preview when there is no attachment (image_tag). This is the display source; it does not set the upload field’s HTML name. |
name |
nil |
Optional base HTML name for the upload inputs. Defaults: file (has-one field) and files[] (has-many field). If set, that string is used for the single-file input; the multi-file input uses the same base with [] appended when missing (e.g. protocol_answers[0][values][] stays unchanged for the multi-file input when the name already ends in []). Client info protocol images use protocol_answers[n][values][] so params submit as values: ["<signed_id>"]. |
form_id |
nil |
When set, the component does not render an inner <form>; file inputs use the HTML form attribute to associate with the element with that id (avoids nested forms). Use the same id as your outer form_with. In standalone mode with form_id, the direct-upload encryption field is not submitted as name="context" (so it cannot override the host page’s params[:context]). After a successful upload, the linked form is auto-submitted via requestSubmit(). |
help |
nil |
Optional helper text below the image control. Hidden when error is present. |
error |
nil |
Optional validation message from the protocol form (display only; does not run validation). Adds error styling on the image tile. Upload failures use a separate inline upload error message (data-images-target="error"). |
in_list_view |
false |
Internal flag used when rendering an image tile inside a list view. |
in_list_view_index |
nil |
Index used to generate stable ids for list-view children. |
attachment |
nil |
Explicit ActiveStorage attachment rendered by list-view child tiles. |
force_new |
false |
Forces rendering an empty uploader tile, usually the trailing "add image" slot in list view. |
can_detach |
true |
Controls whether detach/remove actions are rendered. |
turbo_frame_id |
nil |
Explicit turbo frame id. Defaults to the generated component id/list id. |
Standalone upload (editable without model)
When editable is true and model is not passed, the component runs in standalone mode (for example when you only pass name so the signed blob id participates in a parent form’s params).
- The encrypted payload for direct upload (
X-Lui-Context) containsacceptonly when there is no model. It does not includemodel_class/model_id/association. - The Stimulus controller does not POST to LoopOS
images#createin standalone mode (that endpoint attaches to a record and requires a full context). After direct upload it sets the hidden field with the signed id under yournameand updates the preview. - With
form_id, inputs are associated to that outer form; after success the controller callsrequestSubmit()on that form. Withoutform_id, the component keeps an internal form for upload/attach flows that needname="context"in the attach request body.
If you need LoopOS to attach blobs to an ActiveStorage association server-side via images#create, pass model and association as before.
Direct upload URL
File inputs set data-direct-upload-url to Rails.application.routes.url_helpers.rails_direct_uploads_path (typically /rails/active_storage/direct_uploads), with a prefix fallback if the helper is unavailable. This avoids generating a path under the engine mount when the component is rendered inside a nested route (which would cause RoutingError on POST to /…/loopos_ui/rails/active_storage/direct_uploads), and avoids *_url helpers that require default_url_options[:host].
| accept | configurable | Array of accepted file types (e.g., [:jpeg, :png, "image/svg+xml"]), or :all for all registerer image MIME types |
| list_view | false | Enables list view mode for multiple images. Requires model OR (multiple: true + name:). |
| direct_upload_url | /rails/active_storage/direct_uploads | Direct upload endpoint. Use a Core direct upload URL when uploading to a remote app. |
| direct_upload_authorization | nil | Authorization header used when creating blobs against a remote direct upload URL. |
| multiple | nil | When true, forces multi-image/multi-picker in standalone mode (no model). When nil, inferred from has_many_attached?. Set alongside name: to enable list_view: true without a model (protocol form). |
| preloaded_images | [] | Pre-filled images for standalone list view: [{ signed_id: "…", url: "…" }, …]. Renders display tiles and hidden signedid inputs inside the list. Only consumed in standalone listview mode. |
| submit_on_upload | true | When false, upload completion does not call requestSubmit() and pre-fill hidden inputs are preserved when adding new files. Set to false in protocol forms. |
| path | loopos_ui.images_path | Path to backend controller. Use when you want to extend the current LoopOS UI one. |
accept get its default value from LooposUi.config.image_default_accept_formats, by default it accepts png, jpeg and webp.
You can change this in the initializer.
When extending the LoopOS UI images controller, set the ActiveSupport::Callbacks needed in your own controller, and then call run_callbacks around the block you want your changes to act. These callbacks have to be named :create or :destroy, since those are the hooks available. Each of them allowing the programmer to insert operations before and after their respective methods.
# Your custom controllerclass LooposUi::ExtendedImagesController < LooposUi::ImagesController #We can declare using Lambda, or methods set_callback :create, :before, -> { Rails.logger.info "Before" } set_callback :destroy, :after, :foo def foo Rails.logger.info "After" endendFeatures
Single Image Upload
- Default placeholder for empty state
- Upload button appears on hover when editable
- Supports direct image URLs
- Configurable accepted file types
Multiple Image Upload
- Supports ActiveStorage has_many relationships
- Optional list view mode
- Bulk upload capability
- Image deletion functionality
Styling Options
- Two size options: full and small
- Optional rounded/circular display
- Responsive design
- Hover states for interactive elements
Validations
The accept option is used to validate the file type during upload.
- In the frontend: checking the declared MIME type
- In the backend: checking the actual uploaded content type
In the case of multiple upload, the accept option is applied to each uploaded file, and the upload will fail if any of the files does not match the declared MIME type.
Leaving the accept option empty will allow all image MIME types to be uploaded (image/*), and in the backend will check if the uploaded MIME type is an image.
Note: This component uses it's own controller to handle the upload, using Direct Uploads. It also modifies the blob directly, so the model validation will not trigger. In a future version, this can change.
Protocol form (input:images)
For use inside protocol forms, render with editable: true, list_view: true, multiple: true, submit_on_upload: false. The name: must match the protocol answer param path ("protocol_answers[N][values][]"). Pre-fill existing answers via preloaded_images:.