Skip to content

Commit 7eed049

Browse files
committed
fixed nested form with multipart/fileupload, fixed init value of input type file #550
1 parent 266bd3f commit 7eed049

File tree

6 files changed

+245
-20
lines changed

6 files changed

+245
-20
lines changed

lib/matestack/ui/vue_js/components/form/base.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def attributes
5959
multiple: ctx.multiple,
6060
placeholder: ctx.placeholder,
6161
'@change': change_event,
62-
'init-value': init_value || (ctx.multiple ? [] : nil),
62+
'init-value': init_value,
6363
'v-bind:class': "{ '#{input_error_class}': #{error_key} }",
6464
}).tap do |attrs|
6565
attrs[:"#{v_model_type}"] = input_key unless type == :file

lib/matestack/ui/vue_js/components/form/form.js

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -235,29 +235,55 @@ const componentDef = {
235235
matestackEventHub.$emit('static_form_errors');
236236
}
237237
},
238+
transformToFormData: function (formData, dataNode, parentKey=null) {
239+
var self = this;
240+
for (let key in dataNode) {
241+
if (key.endsWith("[]")) {
242+
for (let i in dataNode[key]) {
243+
let file = dataNode[key][i];
244+
if (parentKey != null) {
245+
formData.append(self.props["for"] + parentKey + "[" + key.slice(0, -2) + "][]", file);
246+
} else {
247+
formData.append(self.props["for"] + "[" + key.slice(0, -2) + "][]", file);
248+
}
249+
}
250+
} else {
251+
if (Array.isArray(dataNode[key])){
252+
dataNode[key].forEach(function(item, index){
253+
if (parentKey != null) {
254+
let _key = parentKey + "[" + key + "]" + "[]";
255+
formData = self.transformToFormData(formData, item, _key)
256+
} else {
257+
let _key = "[" + key + "]" + "[]";
258+
formData = self.transformToFormData(formData, item, _key)
259+
}
260+
})
261+
} else {
262+
if (dataNode[key] != null){
263+
if (parentKey != null) {
264+
formData.append(self.props["for"] + parentKey + "[" + key + "]", dataNode[key]);
265+
} else {
266+
formData.append(self.props["for"] + "[" + key + "]", dataNode[key]);
267+
}
268+
}
269+
}
270+
}
271+
}
272+
273+
return formData;
274+
},
238275
sendRequest: function(){
239276
const self = this;
240277
let payload = {};
241278
payload[self.props["for"]] = self.data;
242279
let axios_config = {};
243280
if (self.props["multipart"] == true ) {
244-
let form_data = new FormData();
245-
for (let key in self.data) {
246-
if (key.endsWith("[]")) {
247-
for (let i in self.data[key]) {
248-
let file = self.data[key][i];
249-
form_data.append(self.props["for"] + "[" + key.slice(0, -2) + "][]", file);
250-
}
251-
} else {
252-
if (self.data[key] != null){
253-
form_data.append(self.props["for"] + "[" + key + "]", self.data[key]);
254-
}
255-
}
256-
}
281+
let formData = new FormData();
282+
formData = this.transformToFormData(formData, this.data)
257283
axios_config = {
258284
method: self.props["method"],
259285
url: self.props["submit_path"],
260-
data: form_data,
286+
data: formData,
261287
headers: {
262288
"X-CSRF-Token": document.getElementsByName("csrf-token")[0].getAttribute("content"),
263289
"Content-Type": "multipart/form-data",

lib/matestack/ui/vue_js/components/form/input.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ def input_attributes
2222
attributes
2323
end
2424

25+
def init_value
26+
return nil if ctx.type.to_s == "file"
27+
super
28+
end
29+
2530
def vue_props
2631
{
2732
init_value: init_value,

spec/dummy/app/models/dummy_child_model.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@ class DummyChildModel < ApplicationRecord
22

33
validates :title, presence: true, uniqueness: true
44

5+
has_one_attached :file
6+
has_many_attached :files
7+
58
end

spec/test/components/dynamic/form/input/file_upload_spec.rb

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ def success_submit
1212
render json: {
1313
title: "server received title: #{form_params[:title].to_s}",
1414
file_1: {
15-
instance: "server received file_1 instance: #{form_params[:file_1] ||= 'nil'}",
15+
instance: "server received file_1 instance: #{form_params[:file_1].nil? ? 'nil' : form_params[:file_1]}",
1616
name: "server received file_1 with name: #{form_params[:file_1].present? ? form_params[:file_1].original_filename : 'nil'}"
1717
},
1818
file_2: {
19-
instance: "server received file_2 instance: #{form_params[:file_2] ||= 'nil'}",
19+
instance: "server received file_2 instance: #{form_params[:file_2].nil? ? 'nil' : form_params[:file_2]}",
2020
name: "server received file_2 with name: #{form_params[:file_2].present? ? form_params[:file_2].original_filename : 'nil'}"
2121
},
2222
files: [
@@ -205,7 +205,7 @@ def form_config
205205
class TestModel < ApplicationRecord
206206
has_one_attached :file
207207
end
208-
208+
209209
class ExamplePage < Matestack::Ui::Page
210210
def response
211211
matestack_form form_config do
@@ -297,6 +297,51 @@ def form_config
297297
expect(page).to have_content("files[0] with name: nil")
298298
expect(page).to have_content("files[1] with name: nil")
299299
end
300+
301+
it "properly submits empty file uploads" do
302+
class ExamplePage < Matestack::Ui::Page
303+
def response
304+
matestack_form form_config do
305+
form_input key: :title, type: :text, placeholder: "title", id: "title-input"
306+
br
307+
form_input key: :file_1, type: :file, id: "file-1-input"
308+
br
309+
form_input key: :files, type: :file, multiple: true, id: "files-input"
310+
br
311+
button 'Submit me!'
312+
end
313+
toggle show_on: "uploaded_successfully", hide_on: "form_submitted", id: 'async-form' do
314+
plain "{{event.data.file_1.instance}}"
315+
plain "{{event.data.file_1.name}}"
316+
plain "{{event.data.files}}"
317+
end
318+
end
319+
320+
def form_config
321+
return {
322+
for: :my_form,
323+
method: :post,
324+
multipart: true,
325+
path: form_input_file_upload_success_form_test_path,
326+
emit: "form_submitted",
327+
success: {
328+
emit: "uploaded_successfully"
329+
}
330+
}
331+
end
332+
end
333+
334+
visit '/example'
335+
fill_in "title-input", with: "bar"
336+
337+
click_button "Submit me!"
338+
expect(page).to have_content("file_1 instance: nil")
339+
expect(page).to have_content("file_1 with name: nil")
340+
expect(page).to have_content("files[0] instance: nil")
341+
expect(page).to have_content("files[1] instance: nil")
342+
expect(page).to have_content("files[0] with name: nil")
343+
expect(page).to have_content("files[1] with name: nil")
344+
end
300345
end
301346

302347
# it 'should handle multiple values correctly with multipart: true set' do

spec/test/components/dynamic/form/nested_forms_spec.rb

Lines changed: 148 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ def dummy_model_params
8080
params.require(:dummy_model).permit(
8181
:title,
8282
:description,
83-
dummy_child_models_attributes: [:id, :_destroy, :title, :description]
83+
:file,
84+
files: [],
85+
dummy_child_models_attributes: [:id, :_destroy, :title, :description, :file, files: []]
8486
)
8587
end
8688
end
@@ -97,8 +99,9 @@ def dummy_model_params
9799
end
98100

99101
before :each do
100-
DummyModel.destroy_all
102+
ActiveStorage::Attachment.all.each { |attachment| attachment.purge }
101103
DummyChildModel.destroy_all
104+
DummyModel.destroy_all
102105
allow_any_instance_of(NestedFormTestController).to receive(:expect_params)
103106
end
104107

@@ -1248,6 +1251,149 @@ def response
12481251

12491252
end
12501253

1254+
describe "supports multipart form submit alongside file upload within parent model" do
1255+
1256+
before do
1257+
@dummy_model = DummyModel.create(title: "existing-dummy-model-title")
1258+
@dummy_model.dummy_child_models.create(title: "existing-dummy-child-model-title-1")
1259+
@dummy_model.dummy_child_models.create(title: "existing-dummy-child-model-title-2")
1260+
1261+
class ExamplePage < Matestack::Ui::Page
1262+
1263+
def prepare
1264+
@dummy_model = DummyModel.last
1265+
end
1266+
1267+
def response
1268+
matestack_form form_config do
1269+
form_input key: :title, type: :text, label: "dummy_model_title_input", id: "dummy_model_title_input"
1270+
1271+
form_input key: :file, type: :file, label: "dummy_model_file_input", id: "dummy_model_file_input"
1272+
form_input key: :files, type: :file, label: "dummy_model_files_input", id: "dummy_model_files_input", multiple: true
1273+
1274+
@dummy_model.dummy_child_models.each do |dummy_child_model|
1275+
dummy_child_model_form dummy_child_model
1276+
end
1277+
1278+
form_fields_for_add_item key: :dummy_child_models_attributes, prototype: method(:dummy_child_model_form) do
1279+
button "add", type: :button # type: :button is important! otherwise remove on first item is triggered on enter
1280+
end
1281+
1282+
button "Submit me!"
1283+
1284+
toggle show_on: "success", hide_after: 1000 do
1285+
plain "success!"
1286+
end
1287+
toggle show_on: "failure", hide_after: 1000 do
1288+
plain "failure!"
1289+
end
1290+
end
1291+
end
1292+
1293+
def dummy_child_model_form dummy_child_model = DummyChildModel.new
1294+
form_fields_for dummy_child_model, key: :dummy_child_models_attributes do
1295+
form_input key: :title, type: :text, label: "dummy-child-model-title-input"
1296+
form_input key: :file, type: :file, label: "dummy-child-model-file-input"
1297+
form_input key: :files, type: :file, label: "dummy-child-model-files-input", multiple: true
1298+
1299+
form_fields_for_remove_item do
1300+
button "remove", ":id": "'remove'+nestedFormRuntimeId", type: :button # id is just required in this spec, but type: :button is important! otherwise remove on first item is triggered on enter
1301+
end
1302+
end
1303+
end
1304+
1305+
def form_config
1306+
{
1307+
for: @dummy_model,
1308+
method: :put,
1309+
multipart: true,
1310+
path: nested_forms_spec_submit_update_path(id: @dummy_model.id),
1311+
success: { emit: "success" },
1312+
failure: { emit: "failure" }
1313+
}
1314+
end
1315+
end
1316+
end
1317+
1318+
it "and properly sends dynamically added child data as multipart format" do
1319+
id_of_parent = DummyModel.last.id
1320+
id_of_child_1 = DummyChildModel.last.id
1321+
id_of_child_0 = id_of_child_1-1
1322+
1323+
visit "/example"
1324+
1325+
expect(page).to have_selector('#dummy_model_title_input')
1326+
expect(page).to have_selector('#title_dummy_child_models_attributes_child_0')
1327+
expect(page).to have_selector('#title_dummy_child_models_attributes_child_1')
1328+
expect(page).not_to have_selector('#title_dummy_child_models_attributes_child_2')
1329+
expect(page).not_to have_selector('#title_dummy_child_models_attributes_child_3')
1330+
1331+
click_on "add"
1332+
expect(page).to have_selector('#title_dummy_child_models_attributes_child_2')
1333+
click_on "add"
1334+
expect(page).to have_selector('#title_dummy_child_models_attributes_child_3')
1335+
1336+
1337+
fill_in "title_dummy_child_models_attributes_child_2", with: "new-dummy-child-model-title-3-value"
1338+
fill_in "title_dummy_child_models_attributes_child_3", with: "new-dummy-child-model-title-4-value"
1339+
1340+
attach_file('dummy_model_file_input', "#{File.dirname(__FILE__)}/input/test_files/matestack-logo.png")
1341+
attach_file "dummy_model_files_input", ["#{File.dirname(__FILE__)}/input/test_files/matestack-logo.png", "#{File.dirname(__FILE__)}/input/test_files/corgi.mp4"]
1342+
1343+
attach_file('file_dummy_child_models_attributes_child_0', "#{File.dirname(__FILE__)}/input/test_files/matestack-logo.png")
1344+
attach_file('file_dummy_child_models_attributes_child_2', "#{File.dirname(__FILE__)}/input/test_files/corgi.mp4")
1345+
attach_file('file_dummy_child_models_attributes_child_3', "#{File.dirname(__FILE__)}/input/test_files/matestack-logo.png")
1346+
1347+
attach_file "files_dummy_child_models_attributes_child_0", ["#{File.dirname(__FILE__)}/input/test_files/matestack-logo.png", "#{File.dirname(__FILE__)}/input/test_files/corgi.mp4"]
1348+
attach_file "files_dummy_child_models_attributes_child_2", ["#{File.dirname(__FILE__)}/input/test_files/matestack-logo.png", "#{File.dirname(__FILE__)}/input/test_files/corgi.mp4"]
1349+
attach_file "files_dummy_child_models_attributes_child_3", ["#{File.dirname(__FILE__)}/input/test_files/matestack-logo.png", "#{File.dirname(__FILE__)}/input/test_files/corgi.mp4"]
1350+
1351+
expect {
1352+
click_button "Submit me!"
1353+
expect(page).to have_content("success!") # required to work properly!
1354+
# expect proper form reset (added items are kept, but value is resetted)
1355+
expect(page).to have_selector('#dummy_model_title_input')
1356+
expect(page).to have_selector('#title_dummy_child_models_attributes_child_0')
1357+
expect(page).to have_selector('#title_dummy_child_models_attributes_child_1')
1358+
expect(page).to have_selector('#title_dummy_child_models_attributes_child_2')
1359+
expect(page).to have_selector('#title_dummy_child_models_attributes_child_3')
1360+
expect(page.find("#dummy_model_title_input").value).to eq("existing-dummy-model-title")
1361+
expect(page.find("#title_dummy_child_models_attributes_child_0").value).to eq("existing-dummy-child-model-title-1")
1362+
expect(page.find("#title_dummy_child_models_attributes_child_1").value).to eq("existing-dummy-child-model-title-2")
1363+
expect(page.find("#title_dummy_child_models_attributes_child_2").value).to eq("new-dummy-child-model-title-3-value")
1364+
expect(page.find("#title_dummy_child_models_attributes_child_3").value).to eq("new-dummy-child-model-title-4-value")
1365+
}
1366+
.to change { DummyModel.count }.by(0)
1367+
.and change { DummyChildModel.count }.by(2)
1368+
1369+
id_of_child_3 = DummyChildModel.last.id
1370+
id_of_child_2 = id_of_child_3-1
1371+
1372+
expect(DummyChildModel.find(id_of_child_3).title).to eq("new-dummy-child-model-title-4-value")
1373+
expect(DummyChildModel.find(id_of_child_2).title).to eq("new-dummy-child-model-title-3-value")
1374+
1375+
expect(DummyModel.find(id_of_parent).file.blob.filename).to eq("matestack-logo.png")
1376+
expect(DummyModel.find(id_of_parent).files[0].blob.filename).to eq("matestack-logo.png")
1377+
expect(DummyModel.find(id_of_parent).files[1].blob.filename).to eq("corgi.mp4")
1378+
1379+
expect(DummyChildModel.find(id_of_child_0).file.blob.filename).to eq("matestack-logo.png")
1380+
expect(DummyChildModel.find(id_of_child_0).files[0].blob.filename).to eq("matestack-logo.png")
1381+
expect(DummyChildModel.find(id_of_child_0).files[1].blob.filename).to eq("corgi.mp4")
1382+
1383+
expect(DummyChildModel.find(id_of_child_1).file.blob.nil?).to be true
1384+
expect(DummyChildModel.find(id_of_child_1).files.empty?).to be true
1385+
1386+
expect(DummyChildModel.find(id_of_child_2).file.blob.filename).to eq("corgi.mp4")
1387+
expect(DummyChildModel.find(id_of_child_2).files[0].blob.filename).to eq("matestack-logo.png")
1388+
expect(DummyChildModel.find(id_of_child_2).files[1].blob.filename).to eq("corgi.mp4")
1389+
1390+
expect(DummyChildModel.find(id_of_child_3).file.blob.filename).to eq("matestack-logo.png")
1391+
expect(DummyChildModel.find(id_of_child_3).files[0].blob.filename).to eq("matestack-logo.png")
1392+
expect(DummyChildModel.find(id_of_child_3).files[1].blob.filename).to eq("corgi.mp4")
1393+
end
1394+
1395+
end
1396+
12511397
end
12521398

12531399
end

0 commit comments

Comments
 (0)