Skip to content

Commit 23306c3

Browse files
committed
fixed error mapping, added proper error reset behaviour
1 parent a1999bf commit 23306c3

File tree

2 files changed

+205
-16
lines changed

2 files changed

+205
-16
lines changed

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

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ const componentDef = {
2020
nestedFormRuntimeTemplates: {},
2121
nestedFormRuntimeTemplateDomElements: {},
2222
deletedNestedForms: {},
23-
nestedFormRuntimeId: ""
23+
nestedFormRuntimeId: "",
24+
nestedFormServerErrorIndex: "",
2425
};
2526
},
2627
methods: {
@@ -41,7 +42,14 @@ const componentDef = {
4142
},
4243
resetErrors: function (key) {
4344
if (this.errors[key]) {
44-
this.errors[key] = null;
45+
delete this.errors[key];
46+
}
47+
if (this.isNestedForm){
48+
var serverErrorKey = this.props["fields_for"].replace("_attributes", "")+"["+this.nestedFormServerErrorIndex+"]."+key
49+
if (this.$parent.errors[serverErrorKey]) {
50+
delete this.$parent.errors[serverErrorKey];
51+
Vue.set(this.$parent.errors)
52+
}
4553
}
4654
},
4755
setProps: function (flat, newVal) {
@@ -66,6 +74,9 @@ const componentDef = {
6674
setErrors: function(errors){
6775
this.errors = errors;
6876
},
77+
setNestedFormServerErrorIndex: function(value){
78+
this.nestedFormServerErrorIndex = value;
79+
},
6980
setErrorKey: function(key, value){
7081
Vue.set(this.errors, key, value);
7182
},
@@ -80,16 +91,30 @@ const componentDef = {
8091
let childModelName = errorKey.split(".")[0].split("[")[0]
8192
let childModelIndex = errorKey.split(".")[0].split("[")[1].split("]")[0]
8293
let mappedChildModelIndex = self.mapToNestedForms(parseInt(childModelIndex), childModelName+"_attributes")
94+
self.nestedForms[childModelName+"_attributes"][mappedChildModelIndex].setNestedFormServerErrorIndex(parseInt(childModelIndex))
8395
self.nestedForms[childModelName+"_attributes"][mappedChildModelIndex].setErrorKey(childErrorKey, errors[errorKey])
8496
}
8597
})
8698
},
8799
mapToNestedForms: function(serverIndex, nestedFormKey){
88-
var index = serverIndex;
89-
while(this.deletedNestedForms[nestedFormKey].includes(index)){
90-
index++;
100+
var primaryKey;
101+
if(this.props["primary_key"] != undefined){
102+
primaryKey = this.props["primary_key"];
103+
}else{
104+
primaryKey = "id";
105+
}
106+
107+
var formIdMap = []
108+
var childModelKey = 0;
109+
while(this.data[nestedFormKey].length > childModelKey){
110+
var ignore = this.data[nestedFormKey][childModelKey]["_destroy"] == true && this.data[nestedFormKey][childModelKey][primaryKey] == null
111+
if(!ignore){
112+
formIdMap.push(childModelKey)
113+
}
114+
childModelKey++;
91115
}
92-
return index;
116+
117+
return formIdMap[serverIndex];
93118
},
94119
resetNestedForms: function(){
95120
var self = this;
@@ -111,16 +136,16 @@ const componentDef = {
111136
removeItem: function(){
112137
Vue.set(this.data, "_destroy", true)
113138
this.hideNestedForm = true;
114-
var primaryKey;
115-
if(this.props["primary_key"] != undefined){
116-
primaryKey = this.props["primary_key"];
117-
}else{
118-
primaryKey = id;
119-
}
120-
// if(this.data[primaryKey] == null){
121-
var id = parseInt(this.nestedFormRuntimeId.replace("_"+this.props["fields_for"]+"_child_", ""));
122-
this.$parent.deletedNestedForms[this.props["fields_for"]].push(id);
123-
// }
139+
var id = parseInt(this.nestedFormRuntimeId.replace("_"+this.props["fields_for"]+"_child_", ""));
140+
this.$parent.deletedNestedForms[this.props["fields_for"]].push(id);
141+
var serverErrorKey = this.props["fields_for"].replace("_attributes", "")+"["+this.nestedFormServerErrorIndex+"]."
142+
var self = this;
143+
Object.keys(self.$parent.errors).forEach(function(errorKey){
144+
if (errorKey.lastIndexOf(serverErrorKey, 0) == 0) {
145+
delete self.$parent.errors[errorKey];
146+
Vue.set(self.$parent.errors)
147+
}
148+
});
124149
},
125150
addItem: function(key){
126151
var templateString = JSON.parse(this.$el.querySelector('#prototype-template-for-'+key).dataset[":template"])
@@ -421,6 +446,7 @@ const componentDef = {
421446
let childModelIndex = errorKey.split(".")[0].split("[")[1].split("]")[0]
422447
let mappedChildModelIndex = self.$parent.mapToNestedForms(parseInt(childModelIndex), childModelName+"_attributes")
423448
if(childModelName+"_attributes" == self.props["fields_for"] && mappedChildModelIndex == id){
449+
self.setNestedFormServerErrorIndex(parseInt(childModelIndex))
424450
self.setErrorKey(childErrorKey, self.$parent.errors[errorKey])
425451
}
426452
}

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

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,41 @@ def prepare
767767
.and change { DummyChildModel.count }.by(3)
768768
end
769769

770+
it "dynamically adds unlimited new nested forms and maps errors properly when nested forms are removed" do
771+
visit "/example"
772+
# sleep
773+
774+
click_on "add"
775+
click_on "add"
776+
expect(page).to have_selector('#title_dummy_child_models_attributes_child_2')
777+
expect(page).to have_selector('#title_dummy_child_models_attributes_child_3')
778+
779+
fill_in "dummy_model_title_input", with: "" # required input, trigger server validation
780+
# fill_in "title_dummy_child_models_attributes_child_0", with: "" # via init value
781+
fill_in "title_dummy_child_models_attributes_child_1", with: "" # required input, trigger server validation
782+
fill_in "title_dummy_child_models_attributes_child_2", with: "dummy-child-model-title-2-value"
783+
fill_in "title_dummy_child_models_attributes_child_3", with: "" # required input, trigger server validation
784+
785+
click_on("remove_dummy_child_models_attributes_child_1")
786+
787+
click_button "Submit me!"
788+
expect(page).to have_content("failure!") # required to work properly!
789+
790+
expect(page).to have_selector("#dummy_model_title_input + .errors > .error", text: "can't be blank")
791+
expect(page).not_to have_selector("#title_dummy_child_models_attributes_child_0 + .errors > .error", text: "can't be blank")
792+
expect(page).not_to have_selector("#title_dummy_child_models_attributes_child_1 + .errors > .error", text: "can't be blank")
793+
expect(page).not_to have_selector("#title_dummy_child_models_attributes_child_2 + .errors > .error", text: "can't be blank")
794+
expect(page).to have_selector("#title_dummy_child_models_attributes_child_3 + .errors > .error", text: "can't be blank")
795+
796+
# expect proper form submission
797+
# expect {
798+
# click_button "Submit me!"
799+
# expect(page).to have_content("success!") # required to work properly!
800+
# }
801+
# .to change { DummyModel.count }.by(1)
802+
# .and change { DummyChildModel.count }.by(3)
803+
end
804+
770805

771806
end
772807

@@ -870,6 +905,7 @@ def form_config
870905
id_of_child_0 = id_of_child_1-1
871906

872907
visit "/example"
908+
# sleep
873909

874910
expect(page.find("#dummy_model_title_input").value).to eq("existing-dummy-model-title")
875911
expect(page.find("#title_dummy_child_models_attributes_child_0").value).to eq("existing-dummy-child-model-title-1")
@@ -1082,6 +1118,133 @@ def form_config
10821118
expect(DummyChildModel.last.title).to eq("new-dummy-child-model-title-3-value")
10831119
end
10841120

1121+
it "dynamically adds unlimited new nested forms and maps errors properly back and properly resets errors when missing value is provided" do
1122+
1123+
class ExamplePage < Matestack::Ui::Page
1124+
1125+
def response
1126+
matestack_form form_config do
1127+
form_input key: :title, type: :text, label: "dummy_model_title_input", id: "dummy_model_title_input"
1128+
1129+
@dummy_model.dummy_child_models.each do |dummy_child_model|
1130+
dummy_child_model_form dummy_child_model
1131+
end
1132+
1133+
form_fields_for_add_item key: :dummy_child_models_attributes, prototype: method(:dummy_child_model_form) do
1134+
button "add", type: :button # type: :button is important! otherwise remove on first item is triggered on enter
1135+
end
1136+
1137+
button "Submit me!"
1138+
1139+
plain "Errors: {{errors}}"
1140+
1141+
toggle show_on: "success", hide_after: 1000 do
1142+
plain "success!"
1143+
end
1144+
toggle show_on: "failure", hide_after: 1000 do
1145+
plain "failure!"
1146+
end
1147+
end
1148+
end
1149+
1150+
end
1151+
1152+
visit "/example"
1153+
# sleep
1154+
1155+
click_on "add"
1156+
expect(page).to have_selector('#title_dummy_child_models_attributes_child_2')
1157+
click_on "add"
1158+
expect(page).to have_selector('#title_dummy_child_models_attributes_child_3')
1159+
1160+
fill_in "title_dummy_child_models_attributes_child_0", with: "", fill_options: { clear: :backspace } # trigger server validation
1161+
# fill_in "title_dummy_child_models_attributes_child_1", with: "" # via init value
1162+
fill_in "title_dummy_child_models_attributes_child_2", with: "" # would trigger but will be removed
1163+
fill_in "title_dummy_child_models_attributes_child_3", with: "" # trigger server validation
1164+
1165+
click_on("remove_dummy_child_models_attributes_child_2")
1166+
1167+
click_button "Submit me!"
1168+
expect(page).to have_content("failure!") # required to work properly!
1169+
1170+
expect(page).to have_selector("#title_dummy_child_models_attributes_child_0 + .errors > .error", text: "can't be blank")
1171+
expect(page).to have_selector("#title_dummy_child_models_attributes_child_3 + .errors > .error", text: "can't be blank")
1172+
1173+
expect(page).to have_content('Errors: { "dummy_child_models[0].title": [ "can\'t be blank" ], "dummy_child_models[2].title": [ "can\'t be blank" ] }')
1174+
1175+
fill_in "title_dummy_child_models_attributes_child_3", with: "some-value" # provide missing input and reset error
1176+
# defocus input in order to trigger errors to disappear
1177+
page.find("#title_dummy_child_models_attributes_child_3").native.send_keys :tab
1178+
1179+
expect(page).to have_selector("#title_dummy_child_models_attributes_child_0 + .errors > .error", text: "can't be blank")
1180+
expect(page).not_to have_selector("#title_dummy_child_models_attributes_child_3 + .errors > .error", text: "can't be blank")
1181+
1182+
expect(page).to have_content('Errors: { "dummy_child_models[0].title": [ "can\'t be blank" ]')
1183+
expect(page).not_to have_content('Errors: { "dummy_child_models[0].title": [ "can\'t be blank" ], "dummy_child_models[2].title": [ "can\'t be blank" ] }')
1184+
end
1185+
1186+
it "dynamically adds unlimited new nested forms and maps errors properly back and properly resets errors when errornous item is removed" do
1187+
1188+
class ExamplePage < Matestack::Ui::Page
1189+
1190+
def response
1191+
matestack_form form_config do
1192+
form_input key: :title, type: :text, label: "dummy_model_title_input", id: "dummy_model_title_input"
1193+
1194+
@dummy_model.dummy_child_models.each do |dummy_child_model|
1195+
dummy_child_model_form dummy_child_model
1196+
end
1197+
1198+
form_fields_for_add_item key: :dummy_child_models_attributes, prototype: method(:dummy_child_model_form) do
1199+
button "add", type: :button # type: :button is important! otherwise remove on first item is triggered on enter
1200+
end
1201+
1202+
button "Submit me!"
1203+
1204+
plain "Errors: {{errors}}"
1205+
1206+
toggle show_on: "success", hide_after: 1000 do
1207+
plain "success!"
1208+
end
1209+
toggle show_on: "failure", hide_after: 1000 do
1210+
plain "failure!"
1211+
end
1212+
end
1213+
end
1214+
1215+
end
1216+
1217+
visit "/example"
1218+
1219+
click_on "add"
1220+
expect(page).to have_selector('#title_dummy_child_models_attributes_child_2')
1221+
click_on "add"
1222+
expect(page).to have_selector('#title_dummy_child_models_attributes_child_3')
1223+
1224+
fill_in "title_dummy_child_models_attributes_child_0", with: "", fill_options: { clear: :backspace } # trigger server validation
1225+
# fill_in "title_dummy_child_models_attributes_child_1", with: "" # via init value
1226+
fill_in "title_dummy_child_models_attributes_child_2", with: "" # would trigger but will be removed
1227+
fill_in "title_dummy_child_models_attributes_child_3", with: "" # trigger server validation
1228+
1229+
click_on("remove_dummy_child_models_attributes_child_2")
1230+
1231+
click_button "Submit me!"
1232+
expect(page).to have_content("failure!") # required to work properly!
1233+
1234+
expect(page).to have_selector("#title_dummy_child_models_attributes_child_0 + .errors > .error", text: "can't be blank")
1235+
expect(page).to have_selector("#title_dummy_child_models_attributes_child_3 + .errors > .error", text: "can't be blank")
1236+
1237+
expect(page).to have_content('Errors: { "dummy_child_models[0].title": [ "can\'t be blank" ], "dummy_child_models[2].title": [ "can\'t be blank" ] }')
1238+
1239+
click_on("remove_dummy_child_models_attributes_child_3") # remove element which causes errors
1240+
1241+
expect(page).to have_selector("#title_dummy_child_models_attributes_child_0 + .errors > .error", text: "can't be blank")
1242+
expect(page).not_to have_selector("#title_dummy_child_models_attributes_child_3 + .errors > .error", text: "can't be blank")
1243+
1244+
expect(page).to have_content('Errors: { "dummy_child_models[0].title": [ "can\'t be blank" ]')
1245+
expect(page).not_to have_content('Errors: { "dummy_child_models[0].title": [ "can\'t be blank" ], "dummy_child_models[2].title": [ "can\'t be blank" ] }')
1246+
end
1247+
10851248

10861249
end
10871250

0 commit comments

Comments
 (0)