Skip to content

Commit 0c7654f

Browse files
committed
implemented dynamic add nested form feature, specs and docs missing
1 parent 1964b41 commit 0c7654f

File tree

5 files changed

+163
-32
lines changed

5 files changed

+163
-32
lines changed

lib/matestack/ui/core.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ module VueJs
5050
require "#{vue_js_base_path}/components/form/radio"
5151
require "#{vue_js_base_path}/components/form/select"
5252
require "#{vue_js_base_path}/components/form/fields_for_remove_item"
53+
require "#{vue_js_base_path}/components/form/fields_for_add_item"
5354
require "#{vue_js_base_path}/components/collection/helper"
5455
require "#{vue_js_base_path}/components/collection/content"
5556
require "#{vue_js_base_path}/components/collection/filter"

lib/matestack/ui/vue_js/components.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ def form_fields_for_remove_item(text=nil, options=nil, &block)
4444
Matestack::Ui::VueJs::Components::Form::FieldsForRemoveItem.(text, options, &block)
4545
end
4646

47+
def form_fields_for_add_item(text=nil, options=nil, &block)
48+
Matestack::Ui::VueJs::Components::Form::FieldsForAddItem.(text, options, &block)
49+
end
50+
4751
def form_input(text=nil, options=nil, &block)
4852
Matestack::Ui::VueJs::Components::Form::Input.(text, options, &block)
4953
end
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
module Matestack
2+
module Ui
3+
module VueJs
4+
module Components
5+
module Form
6+
class FieldsForAddItem < Matestack::Ui::Component
7+
8+
required :key
9+
10+
required :prototype
11+
12+
attr_accessor :prototype_template_json
13+
14+
def create_children(&block)
15+
# first render prototype_template_json
16+
self.prototype_template_json = context.prototype.call().to_json
17+
# delete from children in order not to render the prototype
18+
self.children.shift
19+
super
20+
end
21+
22+
def response
23+
div id: "prototype-template-for-#{context.key}", "v-pre": true, data: { ":template": self.prototype_template_json }
24+
Matestack::Ui::Core::Base.new('v-runtime-template', ':template': "nestedFormRuntimeTemplates['#{context.key}']")
25+
a class: 'matestack-ui-core-form-fields-for-add-item', "@click.prevent": "addItem('#{context.key}')" do
26+
yield if block_given?
27+
end
28+
end
29+
30+
end
31+
end
32+
end
33+
end
34+
end
35+
end

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

Lines changed: 120 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import Vue from "vue/dist/vue.esm";
22
import Vuex from "vuex";
3+
import VRuntimeTemplate from "v-runtime-template"
4+
35
import axios from "axios";
46

57
import matestackEventHub from "../../event_hub";
@@ -11,11 +13,13 @@ const componentDef = {
1113
return {
1214
data: {},
1315
errors: {},
14-
nestedForms: {},
1516
loading: false,
17+
nestedForms: {},
1618
isNestedForm: false,
1719
hideNestedForm: false,
18-
positionInNestedForm: 0
20+
nestedFormRuntimeTemplates: {},
21+
nestedFormRuntimeTemplateDomElements: {},
22+
deletedNestedNewInstanceFormsCount: {}
1923
};
2024
},
2125
methods: {
@@ -71,17 +75,64 @@ const componentDef = {
7175
let childErrorKey = errorKey.split(".")[1]
7276
let childModelName = errorKey.split(".")[0].split("[")[0]
7377
let childModelIndex = errorKey.split(".")[0].split("[")[1].split("]")[0]
78+
childModelIndex = parseInt(childModelIndex) + parseInt(self.deletedNestedNewInstanceFormsCount[childModelName+"_attributes"])
7479
self.nestedForms[childModelName+"_attributes"][childModelIndex].setErrorKey(childErrorKey, errors[errorKey])
7580
}
7681
})
7782
},
83+
resetNestedForms: function(){
84+
var self = this;
85+
Object.keys(self.nestedForms).forEach(function(childModelKey){
86+
self.nestedForms[childModelKey].forEach(function(nestedFormInstance){
87+
if(nestedFormInstance.data["_destroy"] == true){
88+
var destroyed = true;
89+
}
90+
nestedFormInstance.setProps(nestedFormInstance.data, null)
91+
Vue.set(nestedFormInstance.data)
92+
if(destroyed){
93+
nestedFormInstance.hideNestedForm = true
94+
Vue.set(nestedFormInstance.data, "_destroy", true)
95+
}
96+
})
97+
})
98+
},
7899
removeItem: function(){
79100
Vue.set(this.data, "_destroy", true)
80101
this.hideNestedForm = true;
102+
if(this.data["id"] == null){
103+
this.$parent.deletedNestedNewInstanceFormsCount[this.props["fields_for"]]++;
104+
}
105+
},
106+
addItem: function(key){
107+
var templateString = JSON.parse(this.$el.querySelector('#prototype-template-for-'+key).dataset[":template"])
108+
if (this.nestedFormRuntimeTemplateDomElements[key] == null){
109+
var dom_elem = document.createElement('div')
110+
dom_elem.innerHTML = templateString
111+
var existingItemsCount;
112+
if (this.nestedForms[key] == undefined){
113+
existingItemsCount = 0
114+
}else{
115+
existingItemsCount = this.nestedForms[key].length
116+
}
117+
dom_elem.querySelector('.matestack-form-fields-for').id = "child-"+existingItemsCount
118+
Vue.set(this.nestedFormRuntimeTemplateDomElements, key, dom_elem)
119+
Vue.set(this.nestedFormRuntimeTemplates, key, this.nestedFormRuntimeTemplateDomElements[key].outerHTML)
120+
}else{
121+
var dom_elem = document.createElement('div')
122+
dom_elem.innerHTML = templateString
123+
var existingItemsCount = this.nestedForms[key].length
124+
dom_elem.querySelector('.matestack-form-fields-for').id = "child-"+existingItemsCount
125+
this.nestedFormRuntimeTemplateDomElements[key].insertAdjacentHTML(
126+
'beforeend',
127+
dom_elem.innerHTML
128+
)
129+
Vue.set(this.nestedFormRuntimeTemplates, key, this.nestedFormRuntimeTemplateDomElements[key].outerHTML)
130+
}
81131
},
82132
initValues: function () {
83133
let self = this;
84134
let data = {};
135+
85136
for (let key in self.$refs) {
86137
if (key.startsWith("input-component")) {
87138
self.$refs[key].initialize()
@@ -98,36 +149,9 @@ const componentDef = {
98149
if (key.startsWith("checkbox-component")) {
99150
self.$refs[key].initialize()
100151
}
101-
if (key.startsWith("matestack-form-fields-for")) {
102-
self.$refs[key].initializeNestedForm()
103-
}
104152
}
105153
},
106-
initializeNestedForm(){
107-
const self = this;
108-
self.isNestedForm = true;
109-
if (this.props["fields_for"] != undefined) {
110-
this.data = {}
111-
this.initValues()
112-
if(this.$parent.data[this.props["fields_for"]] == undefined){
113-
this.$parent.data[this.props["fields_for"]] = [];
114-
}
115-
this.$parent.data[this.props["fields_for"]].push(this.data);
116-
117-
if(this.$parent.nestedForms[this.props["fields_for"]] == undefined){
118-
this.$parent.nestedForms[this.props["fields_for"]] = [];
119-
}
120-
let existingItemsCount = this.$parent.nestedForms[this.props["fields_for"]].length
121-
this.$parent.nestedForms[this.props["fields_for"]].push(this);
122-
this.positionInNestedForm = existingItemsCount
123154

124-
//without the timeout it's somehow not working
125-
setTimeout(function () {
126-
self.$parent.$forceUpdate();
127-
self.$forceUpdate();
128-
}, 1);
129-
}
130-
},
131155
shouldResetFormOnSuccessfulSubmit() {
132156
const self = this;
133157
if (self.props["success"] != undefined && self.props["success"]["reset"] != undefined) {
@@ -261,13 +285,14 @@ const componentDef = {
261285
if (self.shouldResetFormOnSuccessfulSubmit())
262286
{
263287
self.setProps(self.data, null);
288+
self.resetNestedForms();
264289
self.initValues();
265290
}
266291
})
267292
.catch(function (error) {
268293
self.loading = false;
269294
if (error.response && error.response.data && error.response.data.errors) {
270-
// self.errors = error.response.data.errors;
295+
self.errors = error.response.data.errors;
271296
self.setErrors(error.response.data.errors);
272297
self.setNestedFormsError(error.response.data.errors);
273298
}
@@ -324,8 +349,72 @@ const componentDef = {
324349
},
325350
},
326351
mounted: function () {
327-
this.initValues();
352+
var self = this;
353+
if (this.props["fields_for"] != undefined) {
354+
this.isNestedForm = true;
355+
356+
var id = parseInt(self.$el.id.replace("child-", ""))
357+
358+
this.data = {}
359+
360+
//initialize nestedForm data in parent form if required
361+
if(this.$parent.data[this.props["fields_for"]] == undefined){
362+
this.$parent.data[this.props["fields_for"]] = [];
363+
}
364+
if(this.$parent.nestedForms[this.props["fields_for"]] == undefined){
365+
this.$parent.nestedForms[this.props["fields_for"]] = [];
366+
}
367+
if(this.$parent.deletedNestedNewInstanceFormsCount[this.props["fields_for"]] == undefined){
368+
this.$parent.deletedNestedNewInstanceFormsCount[this.props["fields_for"]] = 0
369+
}
370+
371+
//setup data binding for serverside rendered nested forms
372+
if (isNaN(id)){
373+
this.initValues()
374+
this.$parent.data[this.props["fields_for"]].push(this.data);
375+
this.$parent.nestedForms[this.props["fields_for"]].push(this);
376+
}
377+
378+
//setup data binding for runtime nested forms (dynamic add via v-runtime-template)
379+
if (!isNaN(id)){
380+
if(this.$parent.data[this.props["fields_for"]][id] == undefined){
381+
//new runtime form
382+
this.initValues()
383+
this.$parent.data[this.props["fields_for"]].push(this.data);
384+
this.$parent.nestedForms[this.props["fields_for"]].push(this);
385+
}else{
386+
//retreive state for existing runtime form (after remount for example)
387+
this.data = this.$parent.data[this.props["fields_for"]][id]
388+
if (this.data["_destroy"] == true){
389+
this.hideNestedForm = true;
390+
}
391+
this.$parent.nestedForms[this.props["fields_for"]][id] = this;
392+
Object.keys(this.$parent.errors).forEach(function(errorKey){
393+
if (errorKey.includes(".")){
394+
let childErrorKey = errorKey.split(".")[1]
395+
let childModelName = errorKey.split(".")[0].split("[")[0]
396+
let childModelIndex = errorKey.split(".")[0].split("[")[1].split("]")[0]
397+
childModelIndex = parseInt(childModelIndex) + parseInt(self.$parent.deletedNestedNewInstanceFormsCount[childModelName+"_attributes"])
398+
if(childModelName+"_attributes" == self.props["fields_for"] && childModelIndex == id){
399+
self.setErrorKey(childErrorKey, self.$parent.errors[errorKey])
400+
}
401+
}
402+
})
403+
}
404+
}
405+
406+
//without the timeout it's somehow not working
407+
setTimeout(function () {
408+
self.$parent.$forceUpdate();
409+
self.$forceUpdate();
410+
}, 1);
411+
} else {
412+
this.initValues();
413+
}
328414
},
415+
components: {
416+
VRuntimeTemplate: VRuntimeTemplate
417+
}
329418
};
330419

331420
let component = Vue.component("matestack-ui-core-form", componentDef);

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ class Form < Matestack::Ui::VueJs::Vue
99
optional :for, :path, :success, :failure, :multipart, :emit, :delay, :errors
1010
optional :fields_for, :reject_blank
1111

12+
attr_accessor :prototype_template
13+
1214
# setup form context to allow child components like inputs to access the form configuration
1315
def initialize(html_tag = nil, text = nil, options = {}, &block)
1416
previous_form_context = Matestack::Ui::VueJs::Components::Form::Context.form_context
@@ -23,7 +25,7 @@ def component_id
2325

2426
def response
2527
if context.fields_for
26-
div class: "matestack-form-fields-for", "v-show": "hideNestedForm != true" do
28+
div class: "matestack-form-fields-for", "v-show": "hideNestedForm != true", id: options[:id] do
2729
form_input key: context.for&.class&.primary_key, type: :hidden # required for existing model mapping
2830
form_input key: :_destroy, type: :hidden, init: true if context.reject_blank == true
2931
yield

0 commit comments

Comments
 (0)