Avatar Upload
Click on the avatar to select an image. Shows loading state during upload simulation.
Code
<%= render Hakumi::Space::Component.new(size: :large, align: :center) do %>
<%# Circular avatar upload %>
<%= render Hakumi::Upload::Component.new(
action: "#",
name: "avatar",
accept: "image/*",
list_type: :avatar,
avatar_shape: :circle,
avatar_size: 80,
avatar_icon: :user,
max_size: 5.megabytes,
show_upload_list: false
) %>
<%# Square avatar upload %>
<%= render Hakumi::Upload::Component.new(
action: "#",
name: "avatar",
accept: "image/*",
list_type: :avatar,
avatar_shape: :square,
avatar_size: 80,
avatar_icon: :user,
max_size: 5.megabytes,
show_upload_list: false
) %>
<% end %>
Pictures Wall
Upload pictures to a wall grid. The upload button disappears when the limit (6) is reached.
-
landscape.jpg
-
forest.jpg
- Upload
Max 6 pictures. Upload button hides when limit is reached. Hover over images to view or delete.
Code
<%= render Hakumi::Upload::Component.new(
action: "#",
name: "pictures_wall",
id: "pictures-wall-upload",
list_type: :picture_card,
accept: "image/*",
multiple: true,
max_count: 6,
inline_trigger: true,
default_file_list: [
{ uid: "1", name: "landscape.jpg", status: "done", url: "https://images.unsplash.com/photo-1501785888041-af3ef285b470?auto=format&fit=crop&w=300&q=80" },
{ uid: "2", name: "forest.jpg", status: "done", url: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=300&q=80" }
]
) %>
<div style="margin-top: 12px;">
<%= render Hakumi::Typography::Text::Component.new(type: :secondary) do %>
Max 6 pictures. Upload button hides when limit is reached. Hover over images to view or delete.
<% end %>
</div>
<script>
(() => {
const init = () => {
const upload = document.getElementById('pictures-wall-upload')
if (!upload?.hakumiComponent?.api) return false
const api = upload.hakumiComponent.api
// Listen for file changes to trigger simulated upload
upload.addEventListener('hakumi:upload:change', (e) => {
const fileList = e.detail.fileList || []
// Find files in 'ready' status and simulate upload
fileList.forEach((file) => {
if (file.status === 'ready') {
api.simulateUpload(file.uid, { duration: 1500, success: true })
}
})
})
return true
}
if (init()) return
const onReady = () => {
if (init()) {
document.removeEventListener('turbo:load', onReady)
window.removeEventListener('load', onReady)
}
}
document.addEventListener('turbo:load', onReady)
window.addEventListener('load', onReady)
})()
</script>
Basic Upload
Use the upload component to select files and show progress in the file list.
Code
<%= render Hakumi::Upload::Component.new(
action: "#",
name: "file",
list_type: :picture,
accept: "image/*",
max_size: 5.megabytes,
max_count: 5
) do %>
Select Images
<% end %>
Drag and Drop
Enable drag mode to accept files dropped onto the card.
Click or drag file to this area to upload
Support for a single or bulk upload.
Code
<%= render Hakumi::Upload::Component.new(
action: "#",
name: "file",
drag: true,
list_type: :picture,
accept: "image/*",
max_size: 5.megabytes
) %>
Picture List
Display thumbnails with the picture list type.
-
mountains.jpg
-
forest.jpg
Code
<%= render Hakumi::Upload::Component.new(
action: "#",
name: "file",
list_type: :picture,
accept: "image/*",
max_size: 5.megabytes,
default_file_list: [
{ uid: "1", name: "mountains.jpg", status: "done", url: "https://images.unsplash.com/photo-1501785888041-af3ef285b470?auto=format&fit=crop&w=300&q=80" },
{ uid: "2", name: "forest.jpg", status: "done", url: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=300&q=80" }
]
) %>
Picture Card
Show files in a card grid for gallery-style uploads.
-
gallery-1.jpg
-
gallery-2.jpg
Code
<%= render Hakumi::Upload::Component.new(
action: "#",
name: "file",
list_type: :picture_card,
accept: "image/*",
max_size: 5.megabytes,
default_file_list: [
{ uid: "1", name: "gallery-1.jpg", status: "done", url: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=400&q=80" },
{ uid: "2", name: "gallery-2.jpg", status: "done", url: "https://images.unsplash.com/photo-1501785888041-af3ef285b470?auto=format&fit=crop&w=400&q=80" }
]
) %>
Disabled
Disable the component to prevent selection and upload actions.
-
portfolio.pdf
Code
<%= render Hakumi::Upload::Component.new(
action: "#",
name: "file",
disabled: true,
default_file_list: [
{ uid: "1", name: "portfolio.pdf", status: "done" }
]
) do %>
Upload disabled
<% end %>
Simulated Upload
Use the simulation API to demo upload progress without a real server.
Upload component with simulated file operations:
Control the upload programmatically:
Code
<%= render Hakumi::Space::Component.new(size: :large, direction: :vertical, align: :start) do %>
<%= render Hakumi::Space::Component.new(size: :middle, direction: :vertical, align: :start) do %>
<p style="margin-bottom: 8px;">Upload component with simulated file operations:</p>
<%= render Hakumi::Upload::Component.new(
action: "#",
name: "simulated_files",
id: "simulated-upload",
list_type: :picture,
accept: "image/*",
auto_upload: false
) do %>
Select Images
<% end %>
<% end %>
<%= render Hakumi::Space::Component.new(size: :middle, direction: :vertical, align: :start) do %>
<p style="margin-bottom: 8px;">Control the upload programmatically:</p>
<%= render Hakumi::Space::Component.new(direction: :horizontal, size: :small) do %>
<%= render Hakumi::Button::Component.new(
id: "add-file-btn",
type: :default
) do %>
Add File
<% end %>
<%= render Hakumi::Button::Component.new(
id: "simulate-btn",
type: :primary
) do %>
Simulate Upload
<% end %>
<%= render Hakumi::Button::Component.new(
id: "simulate-error-btn",
type: :default,
danger: true
) do %>
Simulate Error
<% end %>
<% end %>
<% end %>
<% end %>
<script>
document.addEventListener('DOMContentLoaded', function() {
const upload = document.getElementById('simulated-upload')
const addBtn = document.getElementById('add-file-btn')
const simulateBtn = document.getElementById('simulate-btn')
const errorBtn = document.getElementById('simulate-error-btn')
let fileCounter = 1
// Sample image URLs for simulated files
const sampleImages = [
'https://images.unsplash.com/photo-1501785888041-af3ef285b470?auto=format&fit=crop&w=300&q=80',
'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=300&q=80',
'https://images.unsplash.com/photo-1469474968028-56623f02e42e?auto=format&fit=crop&w=300&q=80'
]
addBtn.addEventListener('click', function() {
const api = upload.hakumiComponent?.api
if (!api) return
// Add a simulated file record with preview image
const file = api.addFile({
name: `image-${fileCounter}.jpg`,
status: 'ready',
size: Math.floor(Math.random() * 1000000),
url: sampleImages[(fileCounter - 1) % sampleImages.length]
})
fileCounter++
if (!file) {
console.log('Max count reached or error adding file')
}
})
simulateBtn.addEventListener('click', function() {
const api = upload.hakumiComponent?.api
if (!api) return
const fileList = api.getFileList()
const readyFiles = fileList.filter(f => f.status === 'ready')
if (readyFiles.length === 0) {
// Add a file first if none ready
const file = api.addFile({
name: `upload-${fileCounter}.jpg`,
status: 'ready',
url: sampleImages[(fileCounter - 1) % sampleImages.length]
})
fileCounter++
if (file) {
api.simulateUpload(file.uid, { duration: 2000, success: true })
}
} else {
// Simulate upload for first ready file
api.simulateUpload(readyFiles[0].uid, { duration: 2000, success: true })
}
})
errorBtn.addEventListener('click', function() {
const api = upload.hakumiComponent?.api
if (!api) return
// Add a file and simulate failed upload
const file = api.addFile({
name: `failed-upload-${fileCounter}.jpg`,
status: 'ready',
url: sampleImages[(fileCounter - 1) % sampleImages.length]
})
fileCounter++
if (file) {
api.simulateUpload(file.uid, {
duration: 1500,
success: false,
error: 'Server rejected the file'
})
}
})
// Listen to events
upload.addEventListener('hakumi:upload:success', function(e) {
console.log('Upload success:', e.detail.file?.name)
})
upload.addEventListener('hakumi:upload:error', function(e) {
console.log('Upload error:', e.detail.file?.name, e.detail.error)
})
})
</script>
Upload API
| Prop | Type | Default | Description |
|---|---|---|---|
action |
String |
- |
Upload endpoint URL (required). |
name |
String |
- |
Form field name for file payloads (required). |
accept |
String |
- |
Accepted file types (e.g., 'image/*', '.pdf,.doc'). Validated client-side. |
multiple |
Boolean |
false |
Allow selecting multiple files. |
max_count |
Integer |
- |
Maximum number of files in the list. |
max_size |
Integer |
- |
Maximum file size in bytes. |
disabled |
Boolean |
false |
Disable interactions. |
list_type |
Symbol |
:text |
Rendering style: :text, :picture, :picture_card. |
show_upload_list |
Boolean |
true |
Show the file list. |
drag |
Boolean |
false |
Enable drag-and-drop zone. |
headers |
Hash |
- |
Extra headers for the upload request (security headers blocked). |
data |
Hash |
- |
Extra form data sent with uploads. |
default_file_list |
Array |
[] |
Initial list of files with keys: uid, name, status, url, percent. |
with_credentials |
Boolean |
false |
Send cookies with cross-origin requests. |
auto_upload |
Boolean |
true |
Automatically upload files when selected. Set to false for form integration or manual control. |
inline_trigger |
Boolean |
false |
Place upload trigger inside the file list grid (Pictures Wall pattern). Hides when max_count is reached. |
Upload JavaScript API (element.hakumiComponent.api)
| Prop | Type | Default | Description |
|---|---|---|---|
upload(file) |
Function |
- |
Upload a specific file (by uid, record, or File object). |
uploadAll() |
Function |
- |
Upload all files with status 'ready' or 'error'. |
remove(file) |
Function |
- |
Remove a file from the list (by uid or record). |
removeAll() |
Function |
- |
Remove all files from the list. |
getFileList() |
Function |
- |
Get a copy of the current file list array. |
setFileList(list) |
Function |
- |
Replace the file list with a new array. |
abort(file) |
Function |
- |
Abort an in-progress upload for a specific file. |
abortAll() |
Function |
- |
Abort all in-progress uploads. |
Simulation API (element.hakumiComponent.api)
| Prop | Type | Default | Description |
|---|---|---|---|
addFile(fileData) |
Function |
- |
Add a file record programmatically. Returns the created record or null if max_count reached. |
setProgress(uid, percent) |
Function |
- |
Set progress (0-100) on a specific file by uid. |
setStatus(uid, status, error?) |
Function |
- |
Set status on a file. Valid: ready, uploading, done, error, aborted. |
simulateUpload(uid, options) |
Function |
- |
Animate upload progress. Options: { duration: ms, success: bool, error: string }. Returns Promise. |
Preview API (element.hakumiComponent.api)
| Prop | Type | Default | Description |
|---|---|---|---|
openPreview(file) |
Function |
- |
Open the gallery preview for a specific file (by uid or record). Shows all uploaded images with navigation. |
getPreviewItems() |
Function |
- |
Get array of previewable items with src, alt, and uid. Only returns files with 'done' status and valid URLs. |
Events
| Prop | Type | Default | Description |
|---|---|---|---|
hakumi:upload:change |
CustomEvent |
- |
Fired when the file list changes. Detail: { file, fileList }. |
hakumi:upload:progress |
CustomEvent |
- |
Fired during upload progress. Detail: { file, fileList, event }. |
hakumi:upload:success |
CustomEvent |
- |
Fired when a file uploads successfully. Detail: { file, fileList }. |
hakumi:upload:error |
CustomEvent |
- |
Fired when an upload fails. Detail: { file, fileList, error }. |
Security Features
| Prop | Type | Default | Description |
|---|---|---|---|
CSRF Token |
Automatic |
- |
Automatically includes Rails CSRF token from meta tag. |
File Type Validation |
Client-side |
- |
Validates files against accept attribute before upload. |
URL Sanitization |
Automatic |
- |
Only allows http, https, and data:image/* URLs for thumbnails. |
File Name Sanitization |
Automatic |
- |
Removes potentially dangerous characters from file names. |
Header Restrictions |
Automatic |
- |
Blocks setting cookie, host, origin, referer headers. |