matestack-ui-core 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a187da8c82641bf2c77bbb1b4a43b0b7e474d5870435ac1163cc28d09575ca48
4
- data.tar.gz: 4337c9c2a671b9597fda5b88992bde5dffd89f945719f8e19991802bd98a23fa
3
+ metadata.gz: 86a4a5afb13f1c3a954fe71db9220e734654eb451c329858d3f890ee9db148e4
4
+ data.tar.gz: 1035ccf0e2f972d9ad916bbd681059713a8e5c47dfee992d0ad1eefab453f345
5
5
  SHA512:
6
- metadata.gz: 46cb76c518e3146058dd8b27bc20e4699196d1321abfa3fa019a0809e3b7c62b46769a286300620848a465cbdcfd04607be1dbe6e7a99930490c80980031dcd3
7
- data.tar.gz: f393f4268fb80477eacd18d117555cb605782f24ce6c5cf6aa4aa74c518ca94a9897ebd1bb1ed4b7edc4d81ac9d3f2d707ff3345462f0a77ec3b6a15646a4296
6
+ metadata.gz: 362ded6f509688c84767a98241d7c5f5ff6d4d102196a42fee213b421e966c05ac229d0f0ed732c1939135c79da816dcc07f262502d1fc409e8808a53817cdf9
7
+ data.tar.gz: 4eb1ec667b7efa39e44fc7a853f898d45db1b2d9d2060a2900a24a5aa738f16fd4a20e225c5bb8c1c7c2cb5717596fa11845be82b2581c151a28048356b52c72
data/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  # matestack-ui-core | UI in pure Ruby
10
10
 
11
11
  ----
12
- Version 2.0.0 was released on the 12th of April and proudly presented at RailsConf. Click here for more details
12
+ Version 2.0.0 was released on the 12th of April and proudly presented at RailsConf.
13
13
 
14
14
  Most important changes:
15
15
 
@@ -19,6 +19,8 @@ Most important changes:
19
19
  - Improved core code readability/maintainability
20
20
  ----
21
21
 
22
+ [<img src="https://img.youtube.com/vi/bwsVgCb97v0/0.jpg" width="350">](https://www.youtube.com/watch?v=bwsVgCb97v0)
23
+
22
24
  Boost your productivity & easily create component based web UIs in pure Ruby.
23
25
  Reactivity included if desired.
24
26
 
@@ -26,8 +28,6 @@ Reactivity included if desired.
26
28
 
27
29
  You end up writing 50% less code while increasing productivity, maintainability and developer happiness. Work with pure Ruby. If necessary, extend with pure JavaScript. No Opal involved.
28
30
 
29
- [<img src="https://img.youtube.com/vi/Mue5gs6Wtq4/0.jpg" width="350">](https://www.youtube.com/watch?v=Mue5gs6Wtq4)
30
-
31
31
  The main goals are:
32
32
 
33
33
  - More maintainable UI code, using a component-based structure written in Ruby
@@ -66,7 +66,7 @@ Documentation can be found [here](https://docs.matestack.io)
66
66
 
67
67
  ## Getting started
68
68
 
69
- A getting started guide can be found [here](https://docs.matestack.io/getting-started/tutorial)
69
+ A getting started guide can be found [here](https://docs.matestack.io/getting-started/quick-start)
70
70
 
71
71
  ## Changelog
72
72
 
@@ -49,6 +49,8 @@ require "#{vue_js_base_path}/components/form/textarea"
49
49
  require "#{vue_js_base_path}/components/form/checkbox"
50
50
  require "#{vue_js_base_path}/components/form/radio"
51
51
  require "#{vue_js_base_path}/components/form/select"
52
+ require "#{vue_js_base_path}/components/form/fields_for_remove_item"
53
+ require "#{vue_js_base_path}/components/form/fields_for_add_item"
52
54
  require "#{vue_js_base_path}/components/collection/helper"
53
55
  require "#{vue_js_base_path}/components/collection/content"
54
56
  require "#{vue_js_base_path}/components/collection/filter"
@@ -12,6 +12,8 @@ module Matestack
12
12
  attr_accessor :html_tag, :text, :options, :parent, :escape, :bind_to_parent
13
13
 
14
14
  def initialize(html_tag = nil, text = nil, options = {}, &block)
15
+ return unless render?
16
+
15
17
  self.bind_to_parent = ([:without_parent].include?(html_tag) ? false : true)
16
18
  self.slots = self.options.delete(:slots) if self.options
17
19
  # extract_options(text, options) is called in properties
@@ -26,6 +28,12 @@ module Matestack
26
28
  self
27
29
  end
28
30
 
31
+ # can be optionally overwritten in subclass
32
+ # in order to conditionally render the component
33
+ def render?
34
+ true
35
+ end
36
+
29
37
  # check if text is given and set text and options accordingly
30
38
  def extract_options(text, options)
31
39
  if text.is_a? Hash
@@ -46,13 +46,7 @@ module Matestack
46
46
  def component_attributes
47
47
  {
48
48
  is: 'matestack-ui-core-page-content',
49
- ref: 'some-id',
50
49
  ':params': params.to_json,
51
- ':component-config': {
52
- #TODO Remove?!
53
- show_on: 'a-event',
54
- hide_on: 'test'
55
- }.to_json,
56
50
  'inline-template': true
57
51
  }
58
52
  end
@@ -1,7 +1,7 @@
1
1
  module Matestack
2
2
  module Ui
3
3
  module Core
4
- VERSION = '2.0.0'
4
+ VERSION = '2.1.0'
5
5
  end
6
6
  end
7
7
  end
@@ -31,6 +31,23 @@ module Matestack
31
31
  Matestack::Ui::VueJs::Components::Form::Form.(text, options, &block)
32
32
  end
33
33
 
34
+ def form_fields_for(text=nil, options=nil, &block)
35
+ # in order to provide a more intuitiv API while calling the default
36
+ # form, we transform the arguments a bit:
37
+ options[:for] = text
38
+ options[:fields_for] = options.delete(:key)
39
+ text = nil
40
+ Matestack::Ui::VueJs::Components::Form::Form.(text, options, &block)
41
+ end
42
+
43
+ def form_fields_for_remove_item(text=nil, options=nil, &block)
44
+ Matestack::Ui::VueJs::Components::Form::FieldsForRemoveItem.(text, options, &block)
45
+ end
46
+
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
+
34
51
  def form_input(text=nil, options=nil, &block)
35
52
  Matestack::Ui::VueJs::Components::Form::Input.(text, options, &block)
36
53
  end
@@ -11,7 +11,9 @@ const componentDef = {
11
11
  var url;
12
12
  var filter = this.data
13
13
  for (var key in this.data) {
14
- url = queryParamsHelper.updateQueryParams(this.props["id"] + "-filter-" + key, JSON.stringify(this.data[key]), url)
14
+ if(this.data[key] != null){
15
+ url = queryParamsHelper.updateQueryParams(this.props["id"] + "-filter-" + key, JSON.stringify(this.data[key]), url)
16
+ }
15
17
  }
16
18
  url = queryParamsHelper.updateQueryParams(this.props["id"] + "-offset", 0, url)
17
19
  window.history.pushState({matestackApp: true, url: url}, null, url);
@@ -22,7 +24,6 @@ const componentDef = {
22
24
  for (var key in this.data) {
23
25
  url = queryParamsHelper.updateQueryParams(this.props["id"] + "-filter-" + key, null, url)
24
26
  this.data[key] = null;
25
- this.$forceUpdate();
26
27
  }
27
28
  this.initValues();
28
29
  window.history.pushState({matestackApp: true, url: url}, null, url);
@@ -34,7 +35,7 @@ const componentDef = {
34
35
  var queryParamsObject = queryParamsHelper.queryParamsToObject()
35
36
  Object.keys(queryParamsObject).forEach(function(key){
36
37
  if (key.startsWith(self.props["id"] + "-filter-")){
37
- self.data[key.replace(self.props["id"] + "-filter-", "")] = queryParamsObject[key]
38
+ self.data[key.replace(self.props["id"] + "-filter-", "")] = JSON.parse(queryParamsObject[key])
38
39
  }
39
40
  })
40
41
  }
@@ -34,7 +34,11 @@ module Matestack
34
34
  end
35
35
 
36
36
  def id
37
- ctx.id || key
37
+ if ctx.id.present?
38
+ "'#{ctx.id}'"
39
+ else
40
+ "'#{key}'+$parent.nestedFormRuntimeId"
41
+ end
38
42
  end
39
43
 
40
44
  def multiple
@@ -50,12 +54,12 @@ module Matestack
50
54
  def attributes
51
55
  (options || {}).merge({
52
56
  ref: "input.#{attribute_key}",
53
- id: id,
57
+ ":id": id,
54
58
  type: ctx.type,
55
59
  multiple: ctx.multiple,
56
60
  placeholder: ctx.placeholder,
57
61
  '@change': change_event,
58
- 'init-value': init_value || (ctx.multiple ? [] : nil),
62
+ 'init-value': init_value,
59
63
  'v-bind:class': "{ '#{input_error_class}': #{error_key} }",
60
64
  }).tap do |attrs|
61
65
  attrs[:"#{v_model_type}"] = input_key unless type == :file
@@ -95,7 +99,7 @@ module Matestack
95
99
  item.is_a?(Integer) ? 'v-model.number' : 'v-model'
96
100
  end
97
101
  end
98
-
102
+
99
103
  # set value-type "Integer" for all numeric init values or options
100
104
  def value_type(item=nil)
101
105
  if item.nil?
@@ -164,4 +168,4 @@ module Matestack
164
168
  end
165
169
  end
166
170
  end
167
- end
171
+ end
@@ -37,13 +37,13 @@ module Matestack
37
37
  def render_checkbox_options
38
38
  checkbox_options.to_a.each do |item|
39
39
  input checkbox_attributes(item)
40
- label item_label(item), for: item_id(item)
40
+ label item_label(item), ":for": item_id(item)
41
41
  end
42
42
  end
43
43
 
44
44
  def checkbox_attributes(item)
45
45
  {
46
- id: item_id(item),
46
+ ":id": item_id(item),
47
47
  type: :checkbox,
48
48
  name: item_label(item),
49
49
  value: item_value(item),
@@ -57,9 +57,9 @@ module Matestack
57
57
  end
58
58
 
59
59
  def render_true_false_checkbox
60
- input true_false_checkbox_attributes.merge(type: :hidden, id: nil, value: 0)
61
- input true_false_checkbox_attributes.merge(type: :checkbox, id: item_id(1))
62
- label input_label, for: item_id(1) if input_label
60
+ input true_false_checkbox_attributes.merge(type: :hidden, ":id": nil, value: 0)
61
+ input true_false_checkbox_attributes.merge(type: :checkbox, ":id": item_id(1))
62
+ label input_label, ":for": item_id(1) if input_label
63
63
  end
64
64
 
65
65
  def true_false_checkbox_attributes
@@ -88,13 +88,13 @@ module Matestack
88
88
  def item_value(item)
89
89
  item.is_a?(Array) ? item.last : item
90
90
  end
91
-
91
+
92
92
  def item_label(item)
93
93
  item.is_a?(Array) ? item.first : item
94
94
  end
95
95
 
96
96
  def item_id(item)
97
- "#{id}_#{item_value(item).to_s.gsub(" ", '_')}"
97
+ "#{id}+'_#{item_value(item).to_s.gsub(" ", '_')}'"
98
98
  end
99
99
 
100
100
  end
@@ -102,4 +102,4 @@ module Matestack
102
102
  end
103
103
  end
104
104
  end
105
- end
105
+ end
@@ -10,71 +10,58 @@ const formCheckboxMixin = {
10
10
 
11
11
  if (key.startsWith("select.")) {
12
12
  if (key.startsWith("select.multiple.")) {
13
+ self.$set(self.$parent.data, key.replace("select.multiple.", ""), null)
13
14
  if (initValue) {
14
- data[key.replace("select.multiple.", "")] = JSON.parse(initValue["value"]);
15
- Object.assign(self.$parent.data, data);
15
+ self.setValue(JSON.parse(initValue["value"]));
16
16
  self.afterInitialize(JSON.parse(initValue["value"]))
17
17
  } else {
18
- data[key.replace("select.multiple.", "")] = [];
19
- Object.assign(self.$parent.data, data);
20
- self.afterInitialize([])
18
+ self.setValue([]);
19
+ self.afterInitialize([]);
21
20
  }
22
21
  } else {
22
+ self.$set(self.$parent.data, key.replace("select.", ""), null)
23
23
  if (initValue) {
24
24
  if (valueType && valueType["value"] == "Integer") {
25
- data[key.replace("select.", "")] = parseInt(initValue["value"]);
26
- Object.assign(self.$parent.data, data);
25
+ self.setValue(parseInt(initValue["value"]));
27
26
  self.afterInitialize(parseInt(initValue["value"]))
28
27
  } else {
29
-
30
- data[key.replace("select.", "")] = initValue["value"];
31
- Object.assign(self.$parent.data, data);
28
+ self.setValue(initValue["value"]);
32
29
  self.afterInitialize(initValue["value"])
33
30
  }
34
31
  } else {
35
- data[key.replace("select.", "")] = null;
36
- Object.assign(self.$parent.data, data);
32
+ self.setValue(null);
37
33
  self.afterInitialize(null)
38
34
  }
39
35
  }
40
36
  } else {
37
+ self.$set(self.$parent.data, key.replace("input.", ""), null)
41
38
  if (initValue) {
42
39
  if(initValue["value"] === "true"){
43
- data[key.replace("input.", "")] = true;
44
- Object.assign(self.$parent.data, data);
40
+ self.setValue(true);
45
41
  self.afterInitialize(true)
46
42
  }
47
43
  if(initValue["value"] === "false"){
48
- data[key.replace("input.", "")] = false;
49
- Object.assign(self.$parent.data, data);
44
+ self.setValue(false);
50
45
  self.afterInitialize(false)
51
46
  }
52
47
  } else {
53
- data[key.replace("input.", "")] = null;
54
- Object.assign(self.$parent.data, data);
48
+ self.setValue(null);
55
49
  self.afterInitialize(null)
56
50
  }
57
51
  }
58
52
  }
59
-
60
- //without the timeout it's somehow not working
61
- setTimeout(function () {
62
- self.$parent.$forceUpdate();
63
- self.$forceUpdate();
64
- }, 1);
65
53
  },
66
54
  inputChanged: function (key) {
55
+ if (this.$parent.isNestedForm){
56
+ this.$parent.data["_destroy"] = false;
57
+ }
67
58
  this.$parent.resetErrors(key);
68
- this.$parent.$forceUpdate();
69
- this.$forceUpdate();
70
59
  },
71
60
  afterInitialize: function(value){
72
61
  // can be used in the main component for further initialization steps
73
62
  },
74
63
  setValue: function (value){
75
64
  this.$parent.data[this.props["key"]] = value
76
- this.$parent.$forceUpdate();
77
- this.$forceUpdate();
78
65
  }
79
66
  }
80
67
 
@@ -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
@@ -0,0 +1,19 @@
1
+ module Matestack
2
+ module Ui
3
+ module VueJs
4
+ module Components
5
+ module Form
6
+ class FieldsForRemoveItem < Matestack::Ui::Component
7
+
8
+ def response
9
+ a class: 'matestack-ui-core-form-fields-for-remove-item', "@click.prevent": "removeItem()" do
10
+ yield if block_given?
11
+ end
12
+ end
13
+
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,5 +1,7 @@
1
1
  import Vue from "vue/dist/vue.esm";
2
2
  import Vuex from "vuex";
3
+ import VRuntimeTemplate from "v-runtime-template"
4
+
3
5
  import axios from "axios";
4
6
 
5
7
  import matestackEventHub from "../../event_hub";
@@ -11,7 +13,15 @@ const componentDef = {
11
13
  return {
12
14
  data: {},
13
15
  errors: {},
14
- loading: false
16
+ loading: false,
17
+ nestedForms: {},
18
+ isNestedForm: false,
19
+ hideNestedForm: false,
20
+ nestedFormRuntimeTemplates: {},
21
+ nestedFormRuntimeTemplateDomElements: {},
22
+ deletedNestedForms: {},
23
+ nestedFormRuntimeId: "",
24
+ nestedFormServerErrorIndex: "",
15
25
  };
16
26
  },
17
27
  methods: {
@@ -32,31 +42,123 @@ const componentDef = {
32
42
  },
33
43
  resetErrors: function (key) {
34
44
  if (this.errors[key]) {
35
- this.errors[key] = null;
45
+ delete this.errors[key];
46
+ Vue.set(this.errors);
47
+ }
48
+ if (this.isNestedForm){
49
+ var serverErrorKey = this.props["fields_for"].replace("_attributes", "")+"["+this.nestedFormServerErrorIndex+"]."+key
50
+ if (this.$parent.errors[serverErrorKey]) {
51
+ delete this.$parent.errors[serverErrorKey];
52
+ Vue.set(this.$parent.errors);
53
+ }
36
54
  }
37
55
  },
38
- setProps: function (flat, newVal) {
39
- for (var i in flat) {
40
- if (flat[i] === null){
41
- flat[i] = newVal;
42
- } else if (flat[i] instanceof File){
43
- flat[i] = newVal;
44
- this.$refs["input-component-for-"+i].value = newVal
45
- } else if (flat[i] instanceof Array) {
46
- if(flat[i][0] instanceof File){
47
- flat[i] = newVal
48
- this.$refs["input-component-for-"+i].value = newVal
56
+ setErrors: function(errors){
57
+ this.errors = errors;
58
+ },
59
+ setNestedFormServerErrorIndex: function(value){
60
+ this.nestedFormServerErrorIndex = value;
61
+ },
62
+ setErrorKey: function(key, value){
63
+ Vue.set(this.errors, key, value);
64
+ },
65
+ flushErrors: function(key, value){
66
+ this.errors = {};
67
+ },
68
+ setNestedFormsError: function(errors){
69
+ let self = this;
70
+ Object.keys(errors).forEach(function(errorKey){
71
+ if (errorKey.includes(".")){
72
+ let childErrorKey = errorKey.split(".")[1]
73
+ let childModelName = errorKey.split(".")[0].split("[")[0]
74
+ let childModelIndex = errorKey.split(".")[0].split("[")[1].split("]")[0]
75
+ let mappedChildModelIndex = self.mapToNestedForms(parseInt(childModelIndex), childModelName+"_attributes")
76
+ self.nestedForms[childModelName+"_attributes"][mappedChildModelIndex].setNestedFormServerErrorIndex(parseInt(childModelIndex))
77
+ self.nestedForms[childModelName+"_attributes"][mappedChildModelIndex].setErrorKey(childErrorKey, errors[errorKey])
78
+ }
79
+ })
80
+ },
81
+ mapToNestedForms: function(serverIndex, nestedFormKey){
82
+ var primaryKey;
83
+ if(this.props["primary_key"] != undefined){
84
+ primaryKey = this.props["primary_key"];
85
+ }else{
86
+ primaryKey = "id";
87
+ }
88
+
89
+ var formIdMap = []
90
+ var childModelKey = 0;
91
+ while(this.data[nestedFormKey].length > childModelKey){
92
+ var ignore = this.data[nestedFormKey][childModelKey]["_destroy"] == true && this.data[nestedFormKey][childModelKey][primaryKey] == null
93
+ if(!ignore){
94
+ formIdMap.push(childModelKey)
95
+ }
96
+ childModelKey++;
97
+ }
98
+
99
+ return formIdMap[serverIndex];
100
+ },
101
+ resetNestedForms: function(){
102
+ var self = this;
103
+ Object.keys(self.nestedForms).forEach(function(childModelKey){
104
+ self.nestedForms[childModelKey].forEach(function(nestedFormInstance){
105
+ if(nestedFormInstance.data["_destroy"] == true){
106
+ var destroyed = true;
49
107
  }
50
- } else if (typeof flat[i] === "object" && !(flat[i] instanceof Array)) {
51
- setProps(flat[i], newVal);
52
- } else {
53
- flat[i] = newVal;
108
+ nestedFormInstance.initValues()
109
+ console.log(nestedFormInstance.data)
110
+ Vue.set(nestedFormInstance.data)
111
+ if(destroyed){
112
+ nestedFormInstance.hideNestedForm = true
113
+ Vue.set(nestedFormInstance.data, "_destroy", true)
114
+ }
115
+ })
116
+ })
117
+ },
118
+ removeItem: function(){
119
+ Vue.set(this.data, "_destroy", true)
120
+ this.hideNestedForm = true;
121
+ var id = parseInt(this.nestedFormRuntimeId.replace("_"+this.props["fields_for"]+"_child_", ""));
122
+ this.$parent.deletedNestedForms[this.props["fields_for"]].push(id);
123
+ var serverErrorKey = this.props["fields_for"].replace("_attributes", "")+"["+this.nestedFormServerErrorIndex+"]."
124
+ var self = this;
125
+ Object.keys(self.$parent.errors).forEach(function(errorKey){
126
+ if (errorKey.lastIndexOf(serverErrorKey, 0) == 0) {
127
+ delete self.$parent.errors[errorKey];
128
+ Vue.set(self.$parent.errors)
54
129
  }
130
+ });
131
+ },
132
+ addItem: function(key){
133
+ var templateString = JSON.parse(this.$el.querySelector('#prototype-template-for-'+key).dataset[":template"])
134
+ if (this.nestedFormRuntimeTemplateDomElements[key] == null){
135
+ var dom_elem = document.createElement('div')
136
+ dom_elem.innerHTML = templateString
137
+ var existingItemsCount;
138
+ if (this.nestedForms[key] == undefined){
139
+ existingItemsCount = 0
140
+ }else{
141
+ existingItemsCount = this.nestedForms[key].length
142
+ }
143
+ dom_elem.querySelector('.matestack-form-fields-for').id = key+"_child_"+existingItemsCount
144
+ Vue.set(this.nestedFormRuntimeTemplateDomElements, key, dom_elem)
145
+ Vue.set(this.nestedFormRuntimeTemplates, key, this.nestedFormRuntimeTemplateDomElements[key].outerHTML)
146
+ }else{
147
+ var dom_elem = document.createElement('div')
148
+ dom_elem.innerHTML = templateString
149
+ var existingItemsCount = this.nestedForms[key].length
150
+ dom_elem.querySelector('.matestack-form-fields-for').id = key+"_child_"+existingItemsCount
151
+ this.nestedFormRuntimeTemplateDomElements[key].insertAdjacentHTML(
152
+ 'beforeend',
153
+ dom_elem.innerHTML
154
+ )
155
+ Vue.set(this.nestedFormRuntimeTemplates, key, this.nestedFormRuntimeTemplateDomElements[key].outerHTML)
55
156
  }
56
157
  },
57
158
  initValues: function () {
58
159
  let self = this;
59
160
  let data = {};
161
+
60
162
  for (let key in self.$refs) {
61
163
  if (key.startsWith("input-component")) {
62
164
  self.$refs[key].initialize()
@@ -75,6 +177,7 @@ const componentDef = {
75
177
  }
76
178
  }
77
179
  },
180
+
78
181
  shouldResetFormOnSuccessfulSubmit() {
79
182
  const self = this;
80
183
  if (self.props["success"] != undefined && self.props["success"]["reset"] != undefined) {
@@ -93,6 +196,9 @@ const componentDef = {
93
196
  },
94
197
  perform: function(){
95
198
  const self = this
199
+ if (self.props["fields_for"] != null) {
200
+ return;
201
+ }
96
202
  var form = self.$el.tagName == 'FORM' ? self.$el : self.$el.querySelector('form');
97
203
  if(form.checkValidity()){
98
204
  self.loading = true;
@@ -110,29 +216,55 @@ const componentDef = {
110
216
  matestackEventHub.$emit('static_form_errors');
111
217
  }
112
218
  },
219
+ transformToFormData: function (formData, dataNode, parentKey=null) {
220
+ var self = this;
221
+ for (let key in dataNode) {
222
+ if (key.endsWith("[]")) {
223
+ for (let i in dataNode[key]) {
224
+ let file = dataNode[key][i];
225
+ if (parentKey != null) {
226
+ formData.append(self.props["for"] + parentKey + "[" + key.slice(0, -2) + "][]", file);
227
+ } else {
228
+ formData.append(self.props["for"] + "[" + key.slice(0, -2) + "][]", file);
229
+ }
230
+ }
231
+ } else {
232
+ if (Array.isArray(dataNode[key])){
233
+ dataNode[key].forEach(function(item, index){
234
+ if (parentKey != null) {
235
+ let _key = parentKey + "[" + key + "]" + "[]";
236
+ formData = self.transformToFormData(formData, item, _key)
237
+ } else {
238
+ let _key = "[" + key + "]" + "[]";
239
+ formData = self.transformToFormData(formData, item, _key)
240
+ }
241
+ })
242
+ } else {
243
+ if (dataNode[key] != null){
244
+ if (parentKey != null) {
245
+ formData.append(self.props["for"] + parentKey + "[" + key + "]", dataNode[key]);
246
+ } else {
247
+ formData.append(self.props["for"] + "[" + key + "]", dataNode[key]);
248
+ }
249
+ }
250
+ }
251
+ }
252
+ }
253
+
254
+ return formData;
255
+ },
113
256
  sendRequest: function(){
114
257
  const self = this;
115
258
  let payload = {};
116
259
  payload[self.props["for"]] = self.data;
117
260
  let axios_config = {};
118
261
  if (self.props["multipart"] == true ) {
119
- let form_data = new FormData();
120
- for (let key in self.data) {
121
- if (key.endsWith("[]")) {
122
- for (let i in self.data[key]) {
123
- let file = self.data[key][i];
124
- form_data.append(self.props["for"] + "[" + key.slice(0, -2) + "][]", file);
125
- }
126
- } else {
127
- if (self.data[key] != null){
128
- form_data.append(self.props["for"] + "[" + key + "]", self.data[key]);
129
- }
130
- }
131
- }
262
+ let formData = new FormData();
263
+ formData = this.transformToFormData(formData, this.data)
132
264
  axios_config = {
133
265
  method: self.props["method"],
134
266
  url: self.props["submit_path"],
135
- data: form_data,
267
+ data: formData,
136
268
  headers: {
137
269
  "X-CSRF-Token": document.getElementsByName("csrf-token")[0].getAttribute("content"),
138
270
  "Content-Type": "multipart/form-data",
@@ -202,16 +334,20 @@ const componentDef = {
202
334
  return;
203
335
  }
204
336
 
337
+ self.flushErrors();
338
+
205
339
  if (self.shouldResetFormOnSuccessfulSubmit())
206
340
  {
207
- self.setProps(self.data, null);
208
341
  self.initValues();
342
+ self.resetNestedForms();
209
343
  }
210
344
  })
211
345
  .catch(function (error) {
212
346
  self.loading = false;
213
347
  if (error.response && error.response.data && error.response.data.errors) {
214
348
  self.errors = error.response.data.errors;
349
+ self.setErrors(error.response.data.errors);
350
+ self.setNestedFormsError(error.response.data.errors);
215
351
  }
216
352
  if (self.props["failure"] != undefined && self.props["failure"]["emit"] != undefined) {
217
353
  matestackEventHub.$emit(self.props["failure"]["emit"], error.response.data);
@@ -266,8 +402,71 @@ const componentDef = {
266
402
  },
267
403
  },
268
404
  mounted: function () {
269
- this.initValues();
405
+ var self = this;
406
+ if (this.props["fields_for"] != undefined) {
407
+ this.isNestedForm = true;
408
+
409
+ this.data = { "_destroy": false };
410
+
411
+ //initialize nestedForm data in parent form if required
412
+ if(this.$parent.data[this.props["fields_for"]] == undefined){
413
+ this.$parent.data[this.props["fields_for"]] = [];
414
+ }
415
+ if(this.$parent.nestedForms[this.props["fields_for"]] == undefined){
416
+ this.$parent.nestedForms[this.props["fields_for"]] = [];
417
+ }
418
+ if(this.$parent.deletedNestedForms[this.props["fields_for"]] == undefined){
419
+ this.$parent.deletedNestedForms[this.props["fields_for"]] = []
420
+ }
421
+
422
+ var id = parseInt(self.$el.id.replace(this.props["fields_for"]+"_child_", ""));
423
+
424
+ //setup data binding for serverside rendered nested forms
425
+ if (isNaN(id)){
426
+ id = this.$parent.nestedForms[this.props["fields_for"]].length
427
+ this.nestedFormRuntimeId = "_"+this.props["fields_for"]+"_child_"+id
428
+ this.$el.id = this.props["fields_for"]+"_child_"+id
429
+ this.initValues()
430
+ this.$parent.data[this.props["fields_for"]].push(this.data);
431
+ this.$parent.nestedForms[this.props["fields_for"]].push(this);
432
+ }
433
+
434
+ //setup data binding for runtime nested forms (dynamic add via v-runtime-template)
435
+ if (!isNaN(id)){
436
+ this.nestedFormRuntimeId = "_"+this.props["fields_for"]+"_child_"+id
437
+ if(this.$parent.data[this.props["fields_for"]][id] == undefined){
438
+ //new runtime form
439
+ this.initValues()
440
+ this.$parent.data[this.props["fields_for"]].push(this.data);
441
+ this.$parent.nestedForms[this.props["fields_for"]].push(this);
442
+ }else{
443
+ //retreive state for existing runtime form (after remount for example)
444
+ this.data = this.$parent.data[this.props["fields_for"]][id]
445
+ if (this.data["_destroy"] == true){
446
+ this.hideNestedForm = true;
447
+ }
448
+ this.$parent.nestedForms[this.props["fields_for"]][id] = this;
449
+ Object.keys(this.$parent.errors).forEach(function(errorKey){
450
+ if (errorKey.includes(".")){
451
+ let childErrorKey = errorKey.split(".")[1]
452
+ let childModelName = errorKey.split(".")[0].split("[")[0]
453
+ let childModelIndex = errorKey.split(".")[0].split("[")[1].split("]")[0]
454
+ let mappedChildModelIndex = self.$parent.mapToNestedForms(parseInt(childModelIndex), childModelName+"_attributes")
455
+ if(childModelName+"_attributes" == self.props["fields_for"] && mappedChildModelIndex == id){
456
+ self.setNestedFormServerErrorIndex(parseInt(childModelIndex))
457
+ self.setErrorKey(childErrorKey, self.$parent.errors[errorKey])
458
+ }
459
+ }
460
+ })
461
+ }
462
+ }
463
+ } else {
464
+ this.initValues();
465
+ }
270
466
  },
467
+ components: {
468
+ VRuntimeTemplate: VRuntimeTemplate
469
+ }
271
470
  };
272
471
 
273
472
  let component = Vue.component("matestack-ui-core-form", componentDef);
@@ -7,6 +7,9 @@ module Matestack
7
7
  vue_name 'matestack-ui-core-form'
8
8
 
9
9
  optional :for, :path, :success, :failure, :multipart, :emit, :delay, :errors
10
+ optional :fields_for, :reject_blank
11
+
12
+ attr_accessor :prototype_template
10
13
 
11
14
  # setup form context to allow child components like inputs to access the form configuration
12
15
  def initialize(html_tag = nil, text = nil, options = {}, &block)
@@ -16,9 +19,21 @@ module Matestack
16
19
  Matestack::Ui::VueJs::Components::Form::Context.form_context = previous_form_context
17
20
  end
18
21
 
22
+ def component_id
23
+ "matestack-form-fields-for-#{context.fields_for}-#{SecureRandom.hex}" if context.fields_for
24
+ end
25
+
19
26
  def response
20
- form attributes do
21
- yield
27
+ if context.fields_for
28
+ div class: "matestack-form-fields-for", "v-show": "hideNestedForm != true", id: options[:id] do
29
+ form_input key: context.for&.class&.primary_key, type: :hidden # required for existing model mapping
30
+ form_input key: :_destroy, type: :hidden, init: true if context.reject_blank == true
31
+ yield
32
+ end
33
+ else
34
+ form attributes do
35
+ yield
36
+ end
22
37
  end
23
38
  end
24
39
 
@@ -40,6 +55,8 @@ module Matestack
40
55
  multipart: !!ctx.multipart,
41
56
  emit: ctx.emit,
42
57
  delay: ctx.delay,
58
+ fields_for: ctx.fields_for,
59
+ primary_key: for_object_primary_key
43
60
  }
44
61
  end
45
62
 
@@ -52,13 +69,16 @@ module Matestack
52
69
  @for_option ||= ctx.for
53
70
  end
54
71
 
72
+ def for_object_primary_key
73
+ context.for&.class&.primary_key rescue nil
74
+ end
75
+
55
76
  def form_method
56
77
  @form_method ||= options.delete(:method)
57
78
  end
58
-
59
79
  end
60
80
  end
61
81
  end
62
82
  end
63
83
  end
64
- end
84
+ end
@@ -8,7 +8,7 @@ module Matestack
8
8
 
9
9
  def response
10
10
  div class: 'matestack-ui-core-form-input' do
11
- label input_label, for: id if input_label
11
+ label input_label, ":for": id if input_label
12
12
  input input_attributes
13
13
  render_errors
14
14
  end
@@ -22,6 +22,11 @@ module Matestack
22
22
  attributes
23
23
  end
24
24
 
25
+ def init_value
26
+ return nil if ctx.type.to_s == "file"
27
+ super
28
+ end
29
+
25
30
  def vue_props
26
31
  {
27
32
  init_value: init_value,
@@ -34,4 +39,4 @@ module Matestack
34
39
  end
35
40
  end
36
41
  end
37
- end
42
+ end
@@ -7,22 +7,17 @@ const formInputMixin = {
7
7
  for (let key in this.$refs) {
8
8
  let initValue = this.$refs[key]["attributes"]["init-value"];
9
9
 
10
+ self.$set(self.$parent.data, key.replace("input.", ""), null)
11
+
10
12
  if (initValue) {
11
- data[key.replace("input.", "")] = initValue["value"];
12
- Object.assign(self.$parent.data, data);
13
+ self.setValue(initValue["value"])
13
14
  self.afterInitialize(initValue["value"])
14
15
  } else {
15
- data[key.replace("input.", "")] = null;
16
- Object.assign(self.$parent.data, data);
16
+ self.setValue(null)
17
17
  self.afterInitialize(null)
18
18
  }
19
19
  }
20
20
 
21
- //without the timeout it's somehow not working
22
- setTimeout(function () {
23
- self.$parent.$forceUpdate();
24
- self.$forceUpdate();
25
- }, 1);
26
21
  },
27
22
  filesAdded: function (key) {
28
23
  const dataTransfer = event.dataTransfer || event.target;
@@ -39,17 +34,17 @@ const formInputMixin = {
39
34
  }
40
35
  },
41
36
  inputChanged: function (key) {
37
+ if (this.$parent.isNestedForm){
38
+ this.$parent.data["_destroy"] = false;
39
+ }
42
40
  this.$parent.resetErrors(key);
43
- this.$parent.$forceUpdate();
44
- this.$forceUpdate();
41
+
45
42
  },
46
43
  afterInitialize: function(value){
47
44
  // can be used in the main component for further initialization steps
48
45
  },
49
46
  setValue: function (value){
50
- this.$parent.data[this.props["key"]] = value
51
- this.$parent.$forceUpdate();
52
- this.$forceUpdate();
47
+ this.$parent.data[this.props["key"]] = value;
53
48
  }
54
49
  }
55
50
 
@@ -16,7 +16,7 @@ module Matestack
16
16
  def render_options
17
17
  radio_options.to_a.each do |item|
18
18
  input radio_attributes(item)
19
- label item_label(item), for: item_id(item)
19
+ label item_label(item), ":for": item_id(item)
20
20
  end
21
21
  end
22
22
 
@@ -33,7 +33,7 @@ module Matestack
33
33
 
34
34
  def radio_attributes(item)
35
35
  attributes.merge({
36
- id: item_id(item),
36
+ ":id": item_id(item),
37
37
  name: item_name(item),
38
38
  value: item_value(item),
39
39
  type: :radio,
@@ -51,13 +51,13 @@ module Matestack
51
51
  def item_value(item)
52
52
  item.is_a?(Array) ? item.last : item
53
53
  end
54
-
54
+
55
55
  def item_label(item)
56
56
  item.is_a?(Array) ? item.first : item
57
57
  end
58
58
 
59
59
  def item_id(item)
60
- "#{id || key}_#{item_value(item)}"
60
+ "#{id || key}+'_#{item_value(item)}'"
61
61
  end
62
62
 
63
63
  def item_name(item)
@@ -73,4 +73,4 @@ module Matestack
73
73
  end
74
74
  end
75
75
  end
76
- end
76
+ end
@@ -10,53 +10,43 @@ const formRadioMixin = {
10
10
 
11
11
  if (key.startsWith("select.")) {
12
12
  if (key.startsWith("select.multiple.")) {
13
+ self.$set(self.$parent.data, key.replace("select.multiple.", ""), null)
13
14
  if (initValue) {
14
- data[key.replace("select.multiple.", "")] = JSON.parse(initValue["value"]);
15
- Object.assign(self.$parent.data, data);
15
+ self.setValue(JSON.parse(initValue["value"]));
16
16
  self.afterInitialize(JSON.parse(initValue["value"]))
17
17
  } else {
18
- data[key.replace("select.multiple.", "")] = [];
19
- Object.assign(self.$parent.data, data);
20
- self.afterInitialize([])
18
+ self.setValue([]);
19
+ self.afterInitialize([]);
21
20
  }
22
21
  } else {
22
+ self.$set(self.$parent.data, key.replace("select.", ""), null)
23
23
  if (initValue) {
24
24
  if (valueType && valueType["value"] == "Integer") {
25
- data[key.replace("select.", "")] = parseInt(initValue["value"]);
26
- Object.assign(self.$parent.data, data);
25
+ self.setValue(parseInt(initValue["value"]));
27
26
  self.afterInitialize(parseInt(initValue["value"]))
28
27
  } else {
29
- data[key.replace("select.", "")] = initValue["value"];
30
- Object.assign(self.$parent.data, data);
28
+ self.setValue(initValue["value"]);
31
29
  self.afterInitialize(initValue["value"])
32
30
  }
33
31
  } else {
34
- data[key.replace("select.", "")] = null;
35
- Object.assign(self.$parent.data, data);
32
+ self.setValue(null);
36
33
  self.afterInitialize(null)
37
34
  }
38
35
  }
39
36
  }
40
37
  }
41
-
42
- //without the timeout it's somehow not working
43
- setTimeout(function () {
44
- self.$parent.$forceUpdate();
45
- self.$forceUpdate();
46
- }, 1);
47
38
  },
48
39
  inputChanged: function (key) {
40
+ if (this.$parent.isNestedForm){
41
+ this.$parent.data["_destroy"] = false;
42
+ }
49
43
  this.$parent.resetErrors(key);
50
- this.$parent.$forceUpdate();
51
- this.$forceUpdate();
52
44
  },
53
45
  afterInitialize: function(value){
54
46
  // can be used in the main component for further initialization steps
55
47
  },
56
48
  setValue: function (value){
57
49
  this.$parent.data[this.props["key"]] = value
58
- this.$parent.$forceUpdate();
59
- this.$forceUpdate();
60
50
  }
61
51
  }
62
52
 
@@ -8,7 +8,7 @@ module Matestack
8
8
 
9
9
  def response
10
10
  div class: 'matestack-ui-core-form-select' do
11
- label input_label, for: id if input_label
11
+ label input_label, ":for": id if input_label
12
12
  select select_attributes do
13
13
  render_options
14
14
  end
@@ -39,7 +39,7 @@ module Matestack
39
39
  def select_attributes
40
40
  attributes.merge({
41
41
  multiple: multiple,
42
- id: id,
42
+ ":id": id,
43
43
  ref: "select#{'.multiple' if multiple}.#{key}",
44
44
  'value-type': value_type(select_options.first),
45
45
  'init-value': init_value,
@@ -57,7 +57,7 @@ module Matestack
57
57
  def item_value(item)
58
58
  item.is_a?(Array) ? item.last : item
59
59
  end
60
-
60
+
61
61
  def item_label(item)
62
62
  item.is_a?(Array) ? item.first : item
63
63
  end
@@ -85,4 +85,4 @@ module Matestack
85
85
  end
86
86
  end
87
87
  end
88
- end
88
+ end
@@ -10,49 +10,43 @@ const formSelectMixin = {
10
10
 
11
11
  if (key.startsWith("select.")) {
12
12
  if (key.startsWith("select.multiple.")) {
13
+ self.$set(self.$parent.data, key.replace("select.multiple.", ""), null)
13
14
  if (initValue) {
14
- data[key.replace("select.multiple.", "")] = JSON.parse(initValue["value"]);
15
+ self.setValue(JSON.parse(initValue["value"]));
15
16
  self.afterInitialize(JSON.parse(initValue["value"]))
16
17
  } else {
17
- data[key.replace("select.multiple.", "")] = [];
18
- self.afterInitialize([])
18
+ self.setValue([]);
19
+ self.afterInitialize([]);
19
20
  }
20
21
  } else {
22
+ self.$set(self.$parent.data, key.replace("select.", ""), null)
21
23
  if (initValue) {
22
24
  if (valueType && valueType["value"] == "Integer") {
23
- data[key.replace("select.", "")] = parseInt(initValue["value"]);
25
+ self.setValue(parseInt(initValue["value"]));
24
26
  self.afterInitialize(parseInt(initValue["value"]))
25
27
  } else {
26
- data[key.replace("select.", "")] = initValue["value"];
28
+ self.setValue(initValue["value"]);
27
29
  self.afterInitialize(initValue["value"])
28
30
  }
29
31
  } else {
30
- data[key.replace("select.", "")] = null;
32
+ self.setValue(null);
31
33
  self.afterInitialize(null)
32
34
  }
33
35
  }
34
36
  }
35
37
  }
36
-
37
- //without the timeout it's somehow not working
38
- setTimeout(function () {
39
- Object.assign(self.$parent.data, data);
40
- self.$parent.$forceUpdate();
41
- self.$forceUpdate();
42
- }, 1);
43
38
  },
44
39
  inputChanged: function (key) {
40
+ if (this.$parent.isNestedForm){
41
+ this.$parent.data["_destroy"] = false;
42
+ }
45
43
  this.$parent.resetErrors(key);
46
- this.$parent.$forceUpdate();
47
- this.$forceUpdate();
48
44
  },
49
45
  afterInitialize: function(value){
50
46
  // can be used in the main component for further initialization steps
51
47
  },
52
48
  setValue: function (value){
53
49
  this.$parent.data[this.props["key"]] = value
54
- this.$parent.$forceUpdate();
55
- this.$forceUpdate();
56
50
  }
57
51
  }
58
52
 
@@ -8,7 +8,7 @@ module Matestack
8
8
 
9
9
  def response
10
10
  div class: 'matestack-ui-core-form-textarea' do
11
- label input_label, for: id if input_label
11
+ label input_label, ":for": id if input_label
12
12
  textarea textarea_attributes
13
13
  render_errors
14
14
  end
@@ -34,4 +34,4 @@ module Matestack
34
34
  end
35
35
  end
36
36
  end
37
- end
37
+ end
@@ -7,35 +7,28 @@ const formTextareaMixin = {
7
7
  for (let key in this.$refs) {
8
8
  let initValue = this.$refs[key]["attributes"]["init-value"];
9
9
 
10
+ self.$set(self.$parent.data, key.replace("input.", ""), null)
11
+
10
12
  if (initValue) {
11
- data[key.replace("input.", "")] = initValue["value"];
12
- Object.assign(self.$parent.data, data);
13
+ self.setValue(initValue["value"])
13
14
  self.afterInitialize(initValue["value"])
14
15
  } else {
15
- data[key.replace("input.", "")] = null;
16
- Object.assign(self.$parent.data, data);
16
+ self.setValue(null)
17
17
  self.afterInitialize(null)
18
18
  }
19
19
  }
20
-
21
- //without the timeout it's somehow not working
22
- setTimeout(function () {
23
- self.$parent.$forceUpdate();
24
- self.$forceUpdate();
25
- }, 1);
26
20
  },
27
21
  inputChanged: function (key) {
22
+ if (this.$parent.isNestedForm){
23
+ this.$parent.data["_destroy"] = false;
24
+ }
28
25
  this.$parent.resetErrors(key);
29
- this.$parent.$forceUpdate();
30
- this.$forceUpdate();
31
26
  },
32
27
  afterInitialize: function(value){
33
28
  // can be used in the main component for further initialization steps
34
29
  },
35
30
  setValue: function (value){
36
31
  this.$parent.data[this.props["key"]] = value
37
- this.$parent.$forceUpdate();
38
- this.$forceUpdate();
39
32
  }
40
33
  }
41
34
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: matestack-ui-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonas Jabari
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-04-12 00:00:00.000000000 Z
12
+ date: 2021-06-29 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -82,6 +82,8 @@ files:
82
82
  - lib/matestack/ui/vue_js/components/form/checkbox.rb
83
83
  - lib/matestack/ui/vue_js/components/form/checkbox_mixin.js
84
84
  - lib/matestack/ui/vue_js/components/form/context.rb
85
+ - lib/matestack/ui/vue_js/components/form/fields_for_add_item.rb
86
+ - lib/matestack/ui/vue_js/components/form/fields_for_remove_item.rb
85
87
  - lib/matestack/ui/vue_js/components/form/form.js
86
88
  - lib/matestack/ui/vue_js/components/form/form.rb
87
89
  - lib/matestack/ui/vue_js/components/form/input.js