Add file uploads to a form
v0.0.1
The uploads section of a Gervaz form adds file upload to a create or edit form. Gervaz registers the upload with Phoenix LiveView, draws a dropzone, copies each chosen file to a temporary path when the form is saved, and hands the Ash action a list of those paths. This guide declares an upload, receives the files in an action and shows existing files in edit mode. The examples come from the media form of Mishka CMS.
Before you begin
- Have a form section with an identity and the three actions. See Build an admin form.
- Have a create action with an argument that accepts a list of maps, as the next section shows.
Receive the files in an action
When the reader saves, Gervaz copies each uploaded entry to the system temp directory as gervaz_<n>_<client name> and puts a list of maps in the params under the key of the upload's field, or under the upload's name when it has no field. Each map has :path, :client_name, :client_type and :client_size. After the submit, Gervaz deletes those temp files whose paths start with the temp directory, so the action must read or move a file before it returns.
The media resource declares the argument and a change that processes it:
create :form_upload do
description "Creates media from file upload via form for tenant users"
accept [:description, :featured, :media_category_id]
argument :media_files, {:array, :map}, allow_nil?: true
change {MishkaCmsCore.Runtime.Resources.Changes.ProcessFileUpload, []}
change slugify(:name, into: :name)
endDeclare the upload
Add an uploads section to the form. Each upload has a name. Gervaz joins it with the component id to make the key of the Phoenix allow_upload call.
uploads do
upload :media_files do
accept ~w(.jpg .jpeg .png .gif .webp .svg .mp4 .webm .mov .pdf .doc .docx .xls .xlsx .ppt .pptx .txt .csv)
max_entries 1
max_file_size 50_000_000
style :dropzone
ui do
label fn -> dgettext("mishka_gervaz", "Files") end
end
end
end| Option | Meaning |
|---|---|
accept | A string such as "image/*,.pdf", or a list such as ~w(.jpg .png). Gervaz checks the format when it compiles. Always set it, because LiveView raises for an upload that has no accept. |
max_entries | The most files at once. Default 1. |
max_file_size | The largest file in bytes. Default 8,000,000. |
style | :dropzone (default), :file_input for the standard picker, or :custom for a bare live_file_input. |
auto_upload | Starts uploading when files are chosen. Default false. |
field | The form field the upload belongs to, and the params key. |
chunk_size, chunk_timeout | Bytes per chunk and the chunk timeout in milliseconds. |
external, writer | external takes a function fn entry, socket -> ... end for direct uploads such as S3. The DSL also accepts a module name for either option, but LiveView accepts only a function in external, and a function of three arguments in writer. Treat both as unconfirmed. |
Make accept and max_file_size match the validation of the Ash action, because the browser limits are only the first check.
Show existing files in edit mode
existing tells Gervaz how to read the files already stored on a record. It takes a field name, or a function of the record that returns a list of maps:
existing fn record ->
if record.relative_path do
cache_buster =
if record.updated_at, do: "?v=#{DateTime.to_unix(record.updated_at)}", else: ""
[
%{
filename: record.name,
id: record.id,
url: record.relative_path <> cache_buster,
type: record.type,
size: record.size,
format: record.format
}
]
else
[]
end
endWithout existing, Gervaz reads the record attribute named by the upload's field, or by its name when it has no field. A missing value shows nothing already uploaded.
Place the upload among the fields
An upload draws in its own section after the fields. To place it among them, declare a field of type :upload with the same name as the upload. Gervaz marks such a field virtual, because it is not an attribute of the resource. The Media resource does not declare this field, so the block below is an illustration:
fields do
field :media_files, :upload do
ui do
label fn -> dgettext("mishka_gervaz", "Files") end
end
end
endGervaz reports an upload whose field names a field that the form does not declare when it compiles.
Mount more than one form on a page
Gervaz scopes each upload name with the component id, so media_files in a component with the id media_form is registered as media_files_media_form. Characters other than letters, digits and underscores in the id become underscores. Several forms on one page never collide. Write the plain name in the DSL and let Gervaz scope it. The media upload of the page builder hides the submit row with submit={false}. A form with no submit row of its own can be submitted by something else, such as a dropzone that fires on drop. The mount also hides the site field for a site user and supplies its value:
<.live_component
module={MishkaGervaz.Form.Web.Live}
id={MishkaGervaz.Resource.Info.Form.component_id(Media)}
resource={Media}
current_user={@current_user}
hidden_fields={(@editor.site_id && [:site_id]) || []}
submit={false}
defaults={%{site_id: @editor.site_id, media_category_id: @assets_category}}
/>
The Media Library page mounts the media form, with its one dropzone upload, in a card above the list.
The card holds more than the dropzone. It also has the Site and Folder fields, a Featured checkbox and a submit button that reads Create. The Media Library page mounts the form without submit={false}, so the button stays.
Next steps
- Form uploads, submit and presentation DSL: every option of an upload.
- Mount a table and a form:
hidden_fields,defaultsandsubmit. - Override a builder, handler or loader: the upload handler.