@bgroup/wise-form 1.0.6 → 1.0.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bgroup/wise-form",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Wise Form - A reactive form library",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -1,208 +1,161 @@
1
1
  import type { FormulaManager } from '..';
2
2
  import { conditionsTypes } from '../helpers/condition-types';
3
3
  import { EvaluationsManager } from '../helpers/evaluations';
4
- import {
5
- EvaluatedFormula,
6
- FormulaObserver,
7
- IComplexCondition,
8
- IConditionalField,
9
- } from '../types/formulas';
4
+ import { EvaluatedFormula, FormulaObserver, IComplexCondition, IConditionalField } from '../types/formulas';
10
5
  import { parse } from 'mathjs';
11
6
 
12
7
  export class FormulaConditional {
13
- #plugin: any;
14
- #specs: FormulaObserver;
15
- #emptyValue: undefined;
16
- get formula() {
17
- return this.#specs.formula;
18
- }
19
- get base() {
20
- const formula = <IComplexCondition>this.#specs.formula;
21
- return formula.base;
22
- }
23
- #value: string | number | undefined | 0;
24
- get value() {
25
- return this.#value;
26
- }
27
- get name() {
28
- return this.#specs.name;
29
- }
30
- /**
31
- * Represents the fields defined in the plugin settings
32
- */
33
- get fields() {
34
- const formula = <IComplexCondition>this.formula;
35
- return typeof formula?.fields === 'string'
36
- ? [formula?.fields]
37
- : formula?.fields;
38
- }
8
+ #plugin: any;
9
+ #specs: FormulaObserver;
10
+ #emptyValue: undefined;
11
+ get formula() {
12
+ return this.#specs.formula;
13
+ }
14
+ get base() {
15
+ const formula = <IComplexCondition>this.#specs.formula;
16
+ return formula.base;
17
+ }
18
+ #value: string | number | undefined | 0;
19
+ get value() {
20
+ return this.#value;
21
+ }
22
+ get name() {
23
+ return this.#specs.name;
24
+ }
25
+ /**
26
+ * Represents the fields defined in the plugin settings
27
+ */
28
+ get fields() {
29
+ const formula = <IComplexCondition>this.formula;
30
+ return typeof formula?.fields === 'string' ? [formula?.fields] : formula?.fields;
31
+ }
32
+
33
+ get conditions() {
34
+ if (typeof this.#specs.formula === 'string') return;
35
+ const formula = this.#specs.formula as IComplexCondition;
36
+ return formula.conditions;
37
+ }
38
+
39
+ /**
40
+ * FormField type
41
+ */
42
+ #fields: any;
43
+
44
+ #parent: FormulaManager;
45
+ #ceil: boolean;
46
+ #round: boolean;
47
+ #isNotListenToChanges = false;
48
+ constructor(parent, plugin, specs) {
49
+ this.#parent = parent;
50
+ this.#plugin = plugin;
51
+ this.#specs = specs;
52
+ this.#round = specs.round;
53
+ this.#ceil = specs.ceil;
54
+ this.#emptyValue = specs.emptyValue;
55
+
56
+ if (specs.isNotListenToChanges) this.#isNotListenToChanges = specs.isNotListenToChanges;
57
+ }
58
+
59
+ initialize() {
60
+ try {
61
+ const { form } = this.#plugin;
62
+
63
+ if (!this.fields) {
64
+ throw new Error(`Fields not found in formula ${this.name}`);
65
+ }
66
+ const fields = this.fields.map(name => {
67
+ const formula = this.#plugin.formulas.get(name);
68
+ if (formula) return formula;
69
+ const field = form.getField(name);
70
+ return field;
71
+ });
72
+ this.#fields = fields;
73
+ if (this.name === 'costoTotalGrafico') {
74
+ console.log('fields', fields);
75
+ }
76
+ if (!this.#isNotListenToChanges)
77
+ fields.forEach(field => {
78
+ if (!field) {
79
+ throw new Error(`Field ${this.name} not found in form ${form.name}`);
80
+ }
81
+ field.on('change', this.calculate.bind(this));
82
+ });
83
+ } catch (e) {}
84
+ }
85
+
86
+ evaluate() {
87
+ const formula = <IComplexCondition>this.#specs.formula;
88
+ let evaluatedFormula: any = { formula: formula.base }; // Use the base formula by default
89
+ if (formula.conditions) {
90
+ const conditionsArray = Array.isArray(formula.conditions) ? formula.conditions : [formula.conditions];
91
+ for (const condition of conditionsArray) {
92
+ let conditionMet = false;
93
+ if (condition.conditions) {
94
+ // If there are nested conditions, all must be met
95
+ conditionMet = condition.conditions.every(subCondition => {
96
+ const fieldValues = subCondition.fields.map(fieldName => {
97
+ const field = this.#fields.find(f => f.name === fieldName);
98
+ return field ? field.value : this.#emptyValue;
99
+ });
100
+ return EvaluationsManager.validateAll(subCondition.condition, fieldValues, subCondition.value);
101
+ });
102
+ } else {
103
+ const fieldValues = condition.fields.map(fieldName => {
104
+ const field = this.#fields.find(f => f.name === fieldName);
105
+ return field ? field.value : this.#emptyValue;
106
+ });
107
+ const conditionType = !!condition.type && conditionsTypes[condition.type] ? conditionsTypes[condition.type] : conditionsTypes.some;
108
+ // Check if any of the specified fields meet the condition
109
+ conditionMet = EvaluationsManager[conditionType](condition.condition, fieldValues, condition.value);
110
+ }
39
111
 
40
- get conditions() {
41
- if (typeof this.#specs.formula === 'string') return;
42
- const formula = this.#specs.formula as IComplexCondition;
43
- return formula.conditions;
112
+ if (conditionMet) {
113
+ evaluatedFormula.formula = condition.formula;
114
+ evaluatedFormula.fi = condition;
115
+ break;
116
+ }
117
+ }
44
118
  }
119
+ return evaluatedFormula;
120
+ }
45
121
 
122
+ async calculate() {
46
123
  /**
47
- * FormField type
124
+ * the formula is taken from the evaluate method since the conditions are evaluated there and
125
+ * can change the formula to be applied
48
126
  */
49
- #fields: any;
50
-
51
- #parent: FormulaManager;
52
- #ceil: boolean;
53
- #round: boolean;
54
- #isNotListenToChanges = false;
55
- constructor(parent, plugin, specs) {
56
- this.#parent = parent;
57
- this.#plugin = plugin;
58
- this.#specs = specs;
59
- this.#round = specs.round;
60
- this.#ceil = specs.ceil;
61
- this.#emptyValue = specs.emptyValue;
62
-
63
- if (specs.isNotListenToChanges)
64
- this.#isNotListenToChanges = specs.isNotListenToChanges;
127
+ const model = this.#plugin.form.getField(this.name);
128
+ const formula = this.evaluate();
129
+
130
+ if (!formula.formula) {
131
+ this.#value = this.#emptyValue !== undefined ? this.#emptyValue : '';
132
+ model && model.set({ value: this.#value });
133
+ this.#parent.trigger('change');
134
+ return this.#value;
65
135
  }
136
+ // todo: Review if this section can be replaced by formulaManager.variables property.
137
+ const { tokens } = this.#parent.getParser(formula);
138
+ const variables = tokens.filter(token => token.type === 'variable').map(item => item.value);
66
139
 
67
- initialize() {
68
- try {
69
- const { form } = this.#plugin;
70
-
71
- if (!this.fields) {
72
- throw new Error(`Fields not found in formula ${this.name}`);
73
- }
74
- const fields = this.fields.map((name) => {
75
- const formula = this.#plugin.formulas.get(name);
76
- if (formula) return formula;
77
- const field = form.getField(name);
78
- return field;
79
- });
80
- this.#fields = fields;
81
- if (this.name === 'costoTotalGrafico') {
82
- console.log('fields', fields);
83
- }
84
- if (!this.#isNotListenToChanges)
85
- fields.forEach((field) => {
86
- if (!field) {
87
- throw new Error(
88
- `Field ${this.name} not found in form ${form.name}`
89
- );
90
- }
91
- field.on('change', this.calculate.bind(this));
92
- });
93
- } catch (e) {}
94
- }
140
+ const params = await this.#parent.getParams(variables);
95
141
 
96
- evaluate() {
97
- const formula = <IComplexCondition>this.#specs.formula;
98
- let evaluatedFormula: any = { formula: formula.base }; // Use the base formula by default
99
- if (formula.conditions) {
100
- const conditionsArray = Array.isArray(formula.conditions) ? formula.conditions : [formula.conditions];
101
- for (const condition of conditionsArray) {
102
- let conditionMet = false;
103
- if (condition.conditions) {
104
- // If there are nested conditions, all must be met
105
- conditionMet = condition.conditions.every(
106
- (subCondition) => {
107
- const fieldValues = subCondition.fields.map(
108
- (fieldName) => {
109
- const field = this.#fields.find(
110
- (f) => f.name === fieldName
111
- );
112
- return field
113
- ? field.value
114
- : this.#emptyValue;
115
- }
116
- );
117
- return EvaluationsManager.validateAll(
118
- subCondition.condition,
119
- fieldValues,
120
- subCondition.value
121
- );
122
- }
123
- );
124
- } else {
125
- const fieldValues = condition.fields.map((fieldName) => {
126
- const field = this.#fields.find(
127
- (f) => f.name === fieldName
128
- );
129
- return field ? field.value : this.#emptyValue;
130
- });
131
- const conditionType =
132
- !!condition.type && conditionsTypes[condition.type]
133
- ? conditionsTypes[condition.type]
134
- : conditionsTypes.some;
135
- // Check if any of the specified fields meet the condition
136
- conditionMet = EvaluationsManager[conditionType](
137
- condition.condition,
138
- fieldValues,
139
- condition.value
140
- );
141
- }
142
-
143
- if (conditionMet) {
144
- evaluatedFormula.formula = condition.formula;
145
- evaluatedFormula.fi = condition;
146
- break;
147
- }
148
- }
149
- }
150
- return evaluatedFormula;
151
- }
142
+ try {
143
+ const keys = Object.keys(params);
144
+ let result = keys.length === 1 ? params[keys[0]] : parse(formula.formula as string).evaluate(params);
152
145
 
153
- async calculate() {
154
- /**
155
- * the formula is taken from the evaluate method since the conditions are evaluated there and
156
- * can change the formula to be applied
157
- */
158
- const model = this.#plugin.form.getField(this.name);
159
- const formula = this.evaluate();
160
-
161
- if (!formula.formula) {
162
- this.#value =
163
- this.#emptyValue !== undefined ? this.#emptyValue : '';
164
- model && model.set({ value: this.#value });
165
- this.#parent.trigger('change');
166
- return this.#value;
167
- }
168
- // todo: Review if this section can be replaced by formulaManager.variables property.
169
- const { tokens } = this.#parent.getParser(formula);
170
- const variables = tokens
171
- .filter((token) => token.type === 'variable')
172
- .map((item) => item.value);
173
-
174
- const params = await this.#parent.getParams(variables);
175
-
176
- try {
177
- const keys = Object.keys(params);
178
- let result =
179
- keys.length === 1
180
- ? params[keys[0]]
181
- : parse(formula.formula as string).evaluate(params);
182
-
183
- const isInvalidResult = [
184
- -Infinity,
185
- Infinity,
186
- undefined,
187
- null,
188
- NaN,
189
- ].includes(result);
190
- if (this.#round && !isInvalidResult) result = Math.round(result);
191
- if (this.#ceil && !isInvalidResult) result = Math.ceil(result);
192
- this.#value =
193
- isInvalidResult || typeof result === 'object'
194
- ? this.#emptyValue
195
- : Number(result.toFixed(2));
196
-
197
- this.#parent.trigger('change');
198
-
199
- model && model.set({ value: this.#value });
200
- return this.#value;
201
- } catch (e) {
202
- console.log('formula', this.name, formula.formula, params);
203
- console.error(e);
204
- throw new Error('Error calculating the formula');
205
- }
146
+ const isInvalidResult = [-Infinity, Infinity, undefined, null, NaN].includes(result);
147
+ if (this.#round && !isInvalidResult) result = Math.round(result);
148
+ if (this.#ceil && !isInvalidResult) result = Math.ceil(result);
149
+ this.#value = isInvalidResult || typeof result === 'object' ? this.#emptyValue : Number(Number(result).toFixed(2));
150
+
151
+ this.#parent.trigger('change');
152
+
153
+ model && model.set({ value: this.#value });
154
+ return this.#value;
155
+ } catch (e) {
156
+ console.log('formula', this.name, formula.formula, params);
157
+ console.error(e);
158
+ throw new Error('Error calculating the formula');
206
159
  }
160
+ }
207
161
  }
208
-
@@ -33,7 +33,7 @@ export class CallbackManager {
33
33
 
34
34
  initialize() {
35
35
  const instance = this.#field;
36
- const checkField = async settings => {
36
+ const checkField = async (settings, index) => {
37
37
  const dependency = this.#model.getField(this.#model.getFieldName(settings.field));
38
38
  await dependency.isReady;
39
39
  const required = ['field', 'callback'];
@@ -47,6 +47,12 @@ export class CallbackManager {
47
47
  throw new Error(`${settings.callback} is not a registered callback ${settings.name}`);
48
48
  }
49
49
 
50
+ // Asignar un ID único a cada configuración de callback para tracking individual
51
+ if (!settings.__callbackId) {
52
+ const instanceName = (instance as any).name || 'unknown';
53
+ settings.__callbackId = `${instanceName}_${settings.callback}_${settings.field}_${index}_${Date.now()}`;
54
+ }
55
+
50
56
  // saved in listener array to be able to remove the listener if is required.
51
57
  const event = settings.event || 'value.change';
52
58
  const caller = () => this.executeCallback(settings);
@@ -61,12 +67,18 @@ export class CallbackManager {
61
67
 
62
68
  /**
63
69
  * Generates a unique execution key for tracking callback recursion.
64
- * Format: "callbackName:fieldName:dependencyName"
70
+ * Uses the unique __callbackId from settings if available, otherwise falls back to name-based key.
65
71
  */
66
- #getExecutionKey(callbackName: string, fieldName: string, dependencyName?: string): string {
72
+ #getExecutionKey(settings: any, fieldName: string): string {
73
+ // Si el settings tiene un __callbackId único, usarlo para permitir múltiples callbacks del mismo tipo
74
+ if (settings?.__callbackId) {
75
+ return settings.__callbackId;
76
+ }
77
+ // Fallback para compatibilidad con código legacy
78
+ const callbackName = settings?.callback || 'unknown';
67
79
  const fieldNameStr = fieldName || 'unknown';
68
- const dependencyNameStr = dependencyName || 'unknown';
69
- return `${callbackName}:${fieldNameStr}:${dependencyNameStr}`;
80
+ const dependencyFieldName = typeof settings?.field === 'string' ? settings.field : settings?.field?.field || 'unknown';
81
+ return `${callbackName}:${fieldNameStr}:${dependencyFieldName}`;
70
82
  }
71
83
 
72
84
  executeCallback = async settings => {
@@ -84,7 +96,7 @@ export class CallbackManager {
84
96
  const fieldName = (this.#field as { name?: string })?.name || 'unknown';
85
97
  const dependencyFieldName = typeof settings.field === 'string' ? settings.field : settings.field?.field || 'unknown';
86
98
 
87
- const executionKey = this.#getExecutionKey(callbackName, fieldName, dependencyFieldName);
99
+ const executionKey = this.#getExecutionKey(settings, fieldName);
88
100
 
89
101
  // Check if already executing (prevent concurrent executions)
90
102
  if (currentlyExecutingCallbacks.has(executionKey)) {
@@ -105,6 +117,7 @@ export class CallbackManager {
105
117
  `\n Dependency: ${dependencyFieldName}`,
106
118
  `\n Current depth: ${currentDepth + 1}`,
107
119
  `\n Execution key: ${executionKey}`,
120
+ `\n Callback ID: ${settings?.__callbackId || 'N/A'}`,
108
121
  `\n This indicates a circular dependency in callbacks. Please review the form configuration.`
109
122
  );
110
123
  return;