alchemy-form 0.2.8 → 0.2.10

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## 0.2.10 (2024-01-15)
2
+
3
+ * Fix `al-table` throwing an error when receiving null data
4
+ * Add `al-virtual-scroll` element
5
+ * Make field `constraints` work by using the `Document` instance instead of plain data object
6
+ * Clear old errors when validating a form
7
+
8
+ ## 0.2.9 (2023-11-27)
9
+
10
+ * Add support for new local Date/Time fields & Decimal fields
11
+ * Add "hidden" field templates
12
+ * Fix `al-toggle` not firing a change event
13
+ * Fixed `al-field` rendering to update content for non-value setter elements when a new value is assigned
14
+ * Use `constraints` data from sibling fields when loading associated field data
15
+ * Automatically update computed field values when a field it depends on changes
16
+
1
17
  ## 0.2.8 (2023-10-15)
2
18
 
3
19
  * Add basic `al-apex-chart` element
@@ -0,0 +1,19 @@
1
+ al-virtual-scroll {
2
+ display: block;
3
+ min-height: 20rem;
4
+ min-width: 2rem;
5
+ overflow: auto;
6
+ border: 1px solid red;
7
+
8
+ .top-trigger,
9
+ .bottom-trigger {
10
+ width: 1px;
11
+ height: 1px;
12
+ }
13
+
14
+ [data-loaded-entry-index] {
15
+ border: 1px solid orange;
16
+ min-height: 2rem;
17
+ padding: 1rem;
18
+ }
19
+ }
@@ -13,4 +13,5 @@
13
13
  @import "_toggle.scss";
14
14
  @import "_tabs.scss";
15
15
  @import "_pathway.scss";
16
- @import "_apex_charts.scss";
16
+ @import "_apex_charts.scss";
17
+ @import "_virtual_scroll.scss";
package/config/routes.js CHANGED
@@ -3,6 +3,8 @@ Router.add({
3
3
  methods : 'post',
4
4
  paths : '/api/form/data/related',
5
5
  policy : 'logged_in',
6
+ cache : true,
7
+ is_system_route : true,
6
8
  });
7
9
 
8
10
  Router.add({
@@ -10,4 +12,15 @@ Router.add({
10
12
  methods : 'post',
11
13
  paths : '/api/form/data/qbdata',
12
14
  policy : 'logged_in',
15
+ cache : true,
16
+ is_system_route : true,
17
+ });
18
+
19
+ Router.add({
20
+ name : 'FormApi#recompute',
21
+ methods : 'post',
22
+ paths : '/api/form/data/recompute/{model_name}/{field}',
23
+ policy : 'logged_in',
24
+ is_system_route : true,
25
+ permission : 'model.{model_name}.recompute.{field}',
13
26
  });
@@ -17,7 +17,7 @@ const FormApi = Function.inherits('Alchemy.Controller', 'FormApi');
17
17
  *
18
18
  * @author Jelle De Loecker <jelle@elevenways.be>
19
19
  * @since 0.1.0
20
- * @version 0.2.3
20
+ * @version 0.2.9
21
21
  *
22
22
  * @param {Conduit} conduit
23
23
  */
@@ -31,6 +31,12 @@ FormApi.setAction(async function related(conduit) {
31
31
  crit.limit(50);
32
32
  crit.setOption('scenario', 'related_data');
33
33
 
34
+ if (body.constraints) {
35
+ for (let key in body.constraints) {
36
+ crit.where(key).equals(body.constraints[key]);
37
+ }
38
+ }
39
+
34
40
  if (config.value) {
35
41
  crit.where(model.primary_key).equals(config.value);
36
42
  } else if (config.search) {
@@ -84,6 +90,36 @@ FormApi.setAction(async function related(conduit) {
84
90
  conduit.end(result);
85
91
  });
86
92
 
93
+ /**
94
+ * The action to recompute field values
95
+ *
96
+ * @author Jelle De Loecker <jelle@elevenways.be>
97
+ * @since 0.2.9
98
+ * @version 0.2.9
99
+ *
100
+ * @param {Conduit} conduit
101
+ * @param {String} model_name
102
+ * @param {String} field
103
+ */
104
+ FormApi.setAction(async function recompute(conduit, model_name, field) {
105
+
106
+ const body = conduit.body || {};
107
+
108
+ const model = this.getModel(model_name);
109
+
110
+ if (!model) {
111
+ return conduit.error(new Error('Model "' + model_name + '" not found'));
112
+ }
113
+
114
+ let doc = model.createDocument(body);
115
+
116
+ await doc.recomputeFieldIfNecessary(field, true);
117
+
118
+ conduit.end({
119
+ result : doc[field],
120
+ });
121
+ });
122
+
87
123
  /**
88
124
  * The related action
89
125
  *
@@ -199,7 +199,7 @@ WithDataprovider.setMethod(function getWantedPage() {
199
199
  *
200
200
  * @author Jelle De Loecker <jelle@elevenways.be>
201
201
  * @since 0.2.0
202
- * @version 0.2.0
202
+ * @version 0.2.9
203
203
  *
204
204
  * @param {Object} fetch_config
205
205
  */
@@ -207,9 +207,9 @@ WithDataprovider.setMethod(function loadRemoteData(fetch_config) {
207
207
 
208
208
  let pledge = new Classes.Pledge();
209
209
 
210
- let config = this.getRemoteFetchConfig();
210
+ let config = this.getRemoteFetchConfig(fetch_config);
211
211
 
212
- if (fetch_config) {
212
+ if (fetch_config && fetch_config != config) {
213
213
  Object.assign(config, fetch_config);
214
214
  }
215
215
 
@@ -1,3 +1,5 @@
1
+ const LAST_SET_VALUE = Symbol('last_set_value');
2
+
1
3
  /**
2
4
  * The al-field element
3
5
  *
@@ -583,7 +585,7 @@ Field.setProperty(function value_element() {
583
585
  *
584
586
  * @author Jelle De Loecker <jelle@elevenways.be>
585
587
  * @since 0.1.0
586
- * @version 0.1.8
588
+ * @version 0.2.9
587
589
  */
588
590
  Field.setProperty(function value() {
589
591
 
@@ -591,19 +593,47 @@ Field.setProperty(function value() {
591
593
 
592
594
  if (element) {
593
595
  return element.value;
594
- } else {
595
- return this.original_value;
596
596
  }
597
597
 
598
+ return this.value_to_render;
599
+
598
600
  }, function setValue(value) {
599
601
 
600
- let element = this.value_element;
602
+ let has_changed = !Object.alike(this[LAST_SET_VALUE], value);
601
603
 
602
- if (element) {
603
- element.value = value;
604
- } else if (this.original_value == null) {
604
+ if (this.original_value == null) {
605
605
  this.original_value = value;
606
606
  }
607
+
608
+ this[LAST_SET_VALUE] = value;
609
+
610
+ if (!this.valueElementHasValuePropertySetter()) {
611
+ // @TODO: Rerendering during a render causes a deadlock
612
+ if (has_changed) {
613
+ this.rerender();
614
+ }
615
+ } else {
616
+ let element = this.value_element;
617
+
618
+ if (element) {
619
+ element.value = value;
620
+ }
621
+ }
622
+ });
623
+
624
+ /**
625
+ * Get value to use for rendering
626
+ *
627
+ * @author Jelle De Loecker <jelle@elevenways.be>
628
+ * @since 0.2.9
629
+ * @version 0.2.9
630
+ */
631
+ Field.setProperty(function value_to_render() {
632
+ if (this[LAST_SET_VALUE] != null) {
633
+ return this[LAST_SET_VALUE];
634
+ } else {
635
+ return this.original_value;
636
+ }
607
637
  });
608
638
 
609
639
  /**
@@ -827,7 +857,122 @@ Field.setMethod(function retained() {
827
857
  label.setAttribute('for', v_id);
828
858
  this.value_element.setAttribute('id', v_id);
829
859
  }
860
+ });
861
+
862
+ /**
863
+ * Is this field in the given list somehow?
864
+ *
865
+ * @author Jelle De Loecker <jelle@elevenways.be>
866
+ * @since 0.2.9
867
+ * @version 0.2.9
868
+ *
869
+ * @param {string[]|Field[]} list
870
+ *
871
+ * @return boolean
872
+ */
873
+ Field.setMethod(function isInList(list) {
874
+
875
+ if (!list?.length) {
876
+ return false;
877
+ }
878
+
879
+ for (let entry of list) {
880
+
881
+ if (typeof entry == 'string') {
882
+ if (entry == this.field_name) {
883
+ return true;
884
+ }
885
+
886
+ continue;
887
+ }
888
+
889
+ if (entry == this || entry == this.config) {
890
+ return true;
891
+ }
892
+
893
+ // @TODO
894
+ }
895
+
896
+ return false;
897
+ });
898
+
899
+ /**
900
+ * The element has been introduced to the DOM for the first time
901
+ *
902
+ * @author Jelle De Loecker <jelle@elevenways.be>
903
+ * @since 0.2.9
904
+ * @version 0.2.9
905
+ */
906
+ Field.setMethod(function introduced() {
907
+
908
+ let dependencies = this.getDependencyFields();
909
+
910
+ if (!dependencies.length) {
911
+ return;
912
+ }
913
+
914
+ let form = this.alchemy_form;
915
+
916
+ if (!form) {
917
+ return;
918
+ }
919
+
920
+ form.addEventListener('change', e => {
921
+
922
+ let changed_field = e.target.closest('al-field');
923
+
924
+ console.log('Field change', changed_field, e, this)
925
+
926
+ // Rerender the entire field when a field we depend on changes
927
+ // (@TODO: use something less drastic than a rerender)
928
+ if (changed_field?.field_name && changed_field.isInList(dependencies)) {
929
+ this.handleDependentFieldValueChange(changed_field);
930
+ }
931
+ });
932
+ });
933
+
934
+ /**
935
+ * Refresh the value of this field somehow
936
+ *
937
+ * @author Jelle De Loecker <jelle@elevenways.be>
938
+ * @since 0.2.9
939
+ * @version 0.2.9
940
+ *
941
+ * @param {Alchemy.Element.Form.Field} changed_field
942
+ */
943
+ Field.decorateMethod(
944
+ Blast.Decorators.throttle({
945
+ minimum_wait: 150,
946
+ reset_on_call: true,
947
+ }),
948
+ async function handleDependentFieldValueChange(changed_field) {
949
+
950
+ const field = this.config;
951
+
952
+ // Unset the value
953
+ this.value = null;
954
+
955
+ if (field?.is_computed) {
956
+ let doc = this.alchemy_form.getUpdatedDocument();
957
+
958
+ if (doc) {
959
+ let value = await doc.recomputeFieldIfNecessary(field, true);
960
+ this.value = value;
961
+ }
962
+ }
963
+ });
830
964
 
965
+ /**
966
+ * Get any other field names this field depends on
967
+ *
968
+ * @author Jelle De Loecker <jelle@elevenways.be>
969
+ * @since 0.2.9
970
+ * @version 0.2.9
971
+ *
972
+ * @return {string[]}
973
+ */
974
+ Field.setMethod(function getDependencyFields() {
975
+ return this.config?.dependency_fields || [];
831
976
  });
832
977
 
833
978
  /**
@@ -835,7 +980,7 @@ Field.setMethod(function retained() {
835
980
  *
836
981
  * @author Jelle De Loecker <jelle@elevenways.be>
837
982
  * @since 0.1.0
838
- * @version 0.1.9
983
+ * @version 0.2.9
839
984
  *
840
985
  * @param {Object} config
841
986
  * @param {HTMLElement} element
@@ -862,18 +1007,27 @@ Field.setMethod(async function loadData(config, element) {
862
1007
  }
863
1008
  }
864
1009
 
1010
+ const field_options = field.options || {},
1011
+ assoc_options = field_options.options;
1012
+
865
1013
  let model = field.parent_schema?.model_name,
866
- assoc_model = field.options?.model_name || field.options?.modelName;
1014
+ assoc_model = field_options.model_name || field_options.modelName;
1015
+
1016
+ let body = {
1017
+ field : field.name,
1018
+ model : model,
1019
+ assoc_model : assoc_model,
1020
+ config : config,
1021
+ };
1022
+
1023
+ if (assoc_options?.constraints) {
1024
+ body.constraints = this.resolveConstraintInstruction(assoc_options.constraints);
1025
+ }
867
1026
 
868
1027
  let resource_options = {
869
1028
  name : 'FormApi#related',
870
1029
  post : true,
871
- body : {
872
- field : field.name,
873
- model : model,
874
- assoc_model : assoc_model,
875
- config : config,
876
- }
1030
+ body : body,
877
1031
  };
878
1032
 
879
1033
  if (this.data_src) {
@@ -882,5 +1036,109 @@ Field.setMethod(async function loadData(config, element) {
882
1036
 
883
1037
  return this.hawkejs_helpers.Alchemy.getResource(resource_options);
884
1038
  }
1039
+ });
1040
+
1041
+ /**
1042
+ * Resolve a constraint instruction
1043
+ *
1044
+ * @author Jelle De Loecker <jelle@elevenways.be>
1045
+ * @since 0.2.9
1046
+ * @version 0.2.10
1047
+ *
1048
+ * @param {Object} constraints
1049
+ */
1050
+ Field.setMethod(function resolveConstraintInstruction(constraints) {
1051
+
1052
+ let context,
1053
+ result = {},
1054
+ value,
1055
+ key;
1056
+
1057
+ for (key in constraints) {
1058
+ value = constraints[key];
1059
+
1060
+ if (value && typeof value == 'object') {
1061
+ if (value instanceof Classes.Alchemy.PathEvaluator) {
1062
+ if (!context && this.alchemy_form) {
1063
+ context = this.alchemy_form.getValueAsDocument();
1064
+ }
1065
+
1066
+ result[key] = value.getValue({$0: context}) ?? null;
1067
+ }
1068
+ } else {
1069
+ result[key] = value;
1070
+ }
1071
+ }
1072
+
1073
+ return result;
1074
+ });
1075
+
1076
+ /**
1077
+ * Does this field require a re-render when a related field changes?
1078
+ *
1079
+ * @author Jelle De Loecker <jelle@elevenways.be>
1080
+ * @since 0.2.9
1081
+ * @version 0.2.9
1082
+ */
1083
+ Field.setMethod(function requiresRerenderOnRelatedFieldChange() {
1084
+
1085
+ if (!this.valueElementHasValuePropertySetter()) {
1086
+ return true;
1087
+ }
1088
+
1089
+ return false;
1090
+ });
1091
+
1092
+ /**
1093
+ * Can the value element of this field be updated by its value property?
1094
+ *
1095
+ * @author Jelle De Loecker <jelle@elevenways.be>
1096
+ * @since 0.2.9
1097
+ * @version 0.2.9
1098
+ */
1099
+ Field.setMethod(function valueElementHasValuePropertySetter() {
1100
+
1101
+ let element = this.value_element;
1102
+
1103
+ if (!element) {
1104
+ return false;
1105
+ }
1106
+
1107
+ let descriptor = Object.getPropertyDescriptor(element, 'value');
1108
+
1109
+ if (typeof descriptor?.set == 'function') {
1110
+ return true;
1111
+ }
1112
+
1113
+ return false;
1114
+ });
1115
+
1116
+ /**
1117
+ * Get a certain field option.
1118
+ * This might be defined in the Field instance itself, or on this element.
1119
+ *
1120
+ * @author Jelle De Loecker <jelle@elevenways.be>
1121
+ * @since 0.2.9
1122
+ * @version 0.2.9
1123
+ */
1124
+ Field.setMethod(function getFieldOption(name) {
1125
+
1126
+ if (!name) {
1127
+ return;
1128
+ }
1129
+
1130
+ let dasherized = name.dasherize();
1131
+ let attribute_name = 'data-' + dasherized;
1132
+
1133
+ if (this.hasAttribute(attribute_name)) {
1134
+ let result = this.getAttribute(attribute_name);
1135
+
1136
+ if (result) {
1137
+ return result;
1138
+ }
1139
+ }
1140
+
1141
+ let underscored = name.underscore();
885
1142
 
1143
+ return this.config?.options?.[underscored];
886
1144
  });
@@ -21,7 +21,7 @@ FieldSchema.setTemplateFile('form/elements/alchemy_field_schema');
21
21
  *
22
22
  * @author Jelle De Loecker <jelle@elevenways.be>
23
23
  * @since 0.1.0
24
- * @version 0.2.6
24
+ * @version 0.2.9
25
25
  */
26
26
  FieldSchema.setProperty(function schema() {
27
27
 
@@ -46,12 +46,7 @@ FieldSchema.setProperty(function schema() {
46
46
 
47
47
  const form = field_element.alchemy_form;
48
48
 
49
- let record_value = form.value;
50
-
51
- if (form.model) {
52
- record_value = record_value[form.model];
53
- }
54
-
49
+ let record_value = form.getMainValue();
55
50
  parent_schema_value = record_value;
56
51
  }
57
52