Changeset校验失败时Arc模块的file_input无法回填问题咨询
这个问题我之前做项目时也踩过坑——用户填了一堆表单内容还传了文件,结果因为某个必填项漏填导致校验失败,回头还要重新上传文件,体验确实拉胯。下面给你说说我当时的解决思路和具体实现:
首先得明确:浏览器的安全限制不允许直接设置file input的value,这是为了防止恶意网站偷偷上传用户本地文件,所以你没法像text_input那样直接让file_input自动回填。但我们可以用“曲线救国”的方式模拟这个效果,核心思路是:校验失败时先保留已上传的文件信息,前端给用户显示已上传的文件预览,同时通过隐藏字段传递文件标识,让后端复用这个文件,不用用户重新上传。
具体实现步骤
1. 调整Changeset的处理顺序,先处理文件上传
默认情况下,我们可能会先做字段校验再处理文件,但为了在校验失败时也能保留文件信息,要把Arc的cast_attachments放在字段校验之前:
def changeset(user, attrs) do user |> cast(attrs, [:name, :email]) |> cast_attachments(attrs, [:avatar]) # 先处理文件上传,即使后续校验失败也能保留文件信息 |> validate_required([:name, :email]) # 再做其他字段的校验 end
这样一来,哪怕其他字段校验不通过,文件已经被Arc处理过,相关信息会存在changeset.changes[:avatar]里。
2. 前端模板显示已上传文件,用隐藏字段传递标识
在表单模板里,判断Changeset中是否存在已上传的文件信息,如果有,就显示文件预览(比如图片的话展示缩略图),同时用隐藏字段保存文件的标识(比如文件名或者临时存储路径),这样用户提交表单时,后端能识别到之前的文件:
<%= form_for @changeset, @action, [multipart: true], fn f -> %> <!-- 其他表单字段 --> <%= text_input f, :name %> <%= text_input f, :email %> <!-- 文件上传区域 --> <%= if @changeset.changes[:avatar] do %> <div class="uploaded-preview"> <p>已上传文件:<%= @changeset.changes[:avatar].file_name %></p> <%= img_tag(Router.Helpers.url(@conn) <> "/uploads/temp/#{@changeset.changes[:avatar].file_name}") %> <%= hidden_input f, :avatar, value: @changeset.changes[:avatar].file_name %> </div> <p>如需更换文件,请重新选择:</p> <% end %> <%= file_input f, :avatar %> <%= submit "提交" %> <% end %>
这里的关键是hidden_input,它会把已上传的文件名传递给后端,这样用户如果不重新选择文件,后端就能复用之前的文件。
3. 后端识别隐藏字段,复用已上传文件
在Changeset里,我们需要判断传入的avatar参数是新的Plug.Upload实例还是隐藏字段传递的文件名,如果是后者,就从临时存储路径中重新构建Plug.Upload对象,让Arc可以直接复用:
def changeset(user, attrs) do attrs = process_existing_upload(attrs) user |> cast(attrs, [:name, :email]) |> cast_attachments(attrs, [:avatar]) |> validate_required([:name, :email]) end defp process_existing_upload(attrs) do # 判断是否是隐藏字段传递的文件名(不是Plug.Upload实例) if not is_nil(attrs["avatar"]) and not is_map(attrs["avatar"]) do temp_path = Path.join(System.tmp_dir(), "arc_temp") file_path = Path.join(temp_path, attrs["avatar"]) # 重新构建Plug.Upload对象 Map.put(attrs, "avatar", %Plug.Upload{ path: file_path, filename: attrs["avatar"] }) else attrs end end
注意:这里需要确保Arc的临时存储目录和你代码里的路径一致,另外要记得定时清理临时目录里的旧文件,避免磁盘空间被占满。
补充说明
这种方式不是真正意义上的“回填file_input”,而是通过前端展示+隐藏字段的方式,让用户感知到“文件已经保留,不用重新上传”,同时后端能正确复用已上传的文件。毕竟浏览器的安全限制是没法绕过的,这种方案是目前最合理的用户体验优化方式。
内容的提问来源于stack exchange,提问作者lapinkoira

