matestack-ui-core 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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