@bgroup/wise-form 1.0.6 → 1.0.9

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.9",
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;
@@ -33,20 +33,78 @@ export class FormField extends ReactiveModel<IFormField> {
33
33
  get disabled() {
34
34
  if (typeof this.#disabled !== 'object' || !this.#disabled?.fields) return this.#disabled;
35
35
 
36
- const validate = field => {
37
- if (typeof field !== 'object') return !this.#parent.form.getField(field).value;
38
- const { name, value } = field;
39
- const fieldInstance = this.#parent.getField(name);
36
+ const { action, operator = 'or' } = this.#disabled;
37
+ const isEnableAction = action === 'enable';
38
+
39
+ const validate = fieldSettings => {
40
+ // 1. Get the target field instance
41
+ const name = typeof fieldSettings === 'string' ? fieldSettings : fieldSettings.name;
42
+ const fieldInstance = this.#parent.form.getField(name);
43
+
44
+ // If field dependency is missing, we can't evaluate.
40
45
  if (!fieldInstance) return false;
41
- if (field.hasOwnProperty('condition')) {
42
- const compare = this.evaluations[field.condition](fieldInstance.value, field.value);
43
- return compare;
46
+
47
+ // 2. Determine the value to check from that field
48
+ let currentValue;
49
+ if (typeof fieldSettings === 'object' && fieldSettings.property) {
50
+ // Access specific property (e.g. 'entries', 'length', 'value')
51
+ const props = fieldInstance.getProperties();
52
+ const propPath = fieldSettings.property.split('.');
53
+
54
+ let val: any = props;
55
+ // If property is 'value', start from fieldInstance.value
56
+ if (propPath[0] === 'value') {
57
+ val = fieldInstance.value;
58
+ propPath.shift(); // consume 'value'
59
+ }
60
+
61
+ // Traverse path safely
62
+ for (const p of propPath) {
63
+ if (val === undefined || val === null) break;
64
+ val = val[p];
65
+ }
66
+ currentValue = val;
67
+ } else {
68
+ // Default: use the field's main value
69
+ currentValue = fieldInstance.value;
70
+ }
71
+
72
+ // 3. Simple boolean check if settings is just a string
73
+ if (typeof fieldSettings !== 'object') {
74
+ return !currentValue;
75
+ }
76
+
77
+ // 4. Determine comparison value
78
+ let comparisonValue = fieldSettings.value;
79
+ if (fieldSettings.valueFromField) {
80
+ const compareField = this.#parent.form.getField(fieldSettings.valueFromField);
81
+ comparisonValue = compareField ? compareField.value : undefined;
44
82
  }
45
- const { value: fieldValue } = fieldInstance;
46
- return value !== fieldValue;
83
+
84
+ // 5. Evaluate condition
85
+ const condition = fieldSettings.condition;
86
+ if (condition && this.evaluations[condition]) {
87
+ return this.evaluations[condition](currentValue, comparisonValue);
88
+ }
89
+
90
+ // Default behavior
91
+ return currentValue !== comparisonValue;
47
92
  };
48
93
 
49
- return this.#disabled.fields.some(validate);
94
+ const results = this.#disabled.fields.map(validate);
95
+
96
+ let conditionsMet = false;
97
+ if (operator === 'and') {
98
+ conditionsMet = results.every(r => r === true);
99
+ } else {
100
+ conditionsMet = results.some(r => r === true);
101
+ }
102
+
103
+ if (isEnableAction) {
104
+ return !conditionsMet;
105
+ }
106
+
107
+ return conditionsMet;
50
108
  }
51
109
 
52
110
  set disabled(value) {
@@ -184,34 +242,34 @@ export class FormField extends ReactiveModel<IFormField> {
184
242
  if (typeof props.disabled !== 'object') {
185
243
  throw new Error(`The disabled property of the field ${props.name} must be a boolean or an object`);
186
244
  }
187
- if (!props.disabled.fields && !props.disabled.mode) {
188
- throw new Error(`The disabled property of the field ${props.name} must have a fields property or a mode defined`);
189
- }
190
-
191
245
  if (props.disabled.mode) {
192
246
  // posible modes : create, update;
193
247
  this.#disabled = this.#parent.form.mode === props.disabled.mode;
194
248
  return;
195
249
  }
196
250
 
197
- let allValid;
198
- props.disabled.fields.forEach(item => {
199
- const name = typeof item === 'string' ? item : item.name;
200
-
201
- const instance = this.#parent.form.getField(name);
202
- allValid = instance;
203
- if (!allValid) return;
204
- instance.on('change', this.#listenSiblings);
205
- instance.on('value.change', this.#listenSiblings);
206
- this.#listeningItems.set(name, {
207
- item: instance,
208
- listener: this.#listenSiblings,
209
- });
210
- });
251
+ if (props.disabled.fields && Array.isArray(props.disabled.fields)) {
252
+ props.disabled.fields.forEach(item => {
253
+ const namesToListen: string[] = [];
254
+ if (typeof item === 'string') {
255
+ namesToListen.push(item);
256
+ } else {
257
+ if (item.name) namesToListen.push(item.name);
258
+ if (item.valueFromField) namesToListen.push(item.valueFromField);
259
+ }
211
260
 
212
- if (!allValid) {
213
- const fieldName = this.getProperties().name || 'unknown';
214
- throw new Error(`the field ${allValid} does not exist in the form ${(this.#parent as any).name}, field passed in invalid settings of field "${fieldName}"`);
261
+ namesToListen.forEach(name => {
262
+ const instance = this.#parent.form.getField(name);
263
+ if (!instance) return;
264
+
265
+ instance.on('change', this.#listenSiblings);
266
+ instance.on('value.change', this.#listenSiblings);
267
+ this.#listeningItems.set(name, {
268
+ item: instance,
269
+ listener: this.#listenSiblings,
270
+ });
271
+ });
272
+ });
215
273
  }
216
274
  this.#disabled = props.disabled;
217
275
  }
@@ -1,8 +1,15 @@
1
1
  export type TDisabledSettings = {
2
2
  name: string;
3
3
  value: any;
4
+ condition?: string;
5
+ property?: string;
6
+ operator?: 'and' | 'or';
7
+ valueFromField?: string;
4
8
  };
9
+
5
10
  export interface IDisabled {
6
11
  fields: string[] | TDisabledSettings[];
12
+ action?: 'enable' | 'disable';
13
+ operator?: 'and' | 'or'; // Operator for the top-level list of fields/conditions
7
14
  }
8
15