ui-core-abv 0.10.4 → 0.10.6

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.
@@ -4175,6 +4175,24 @@ class UicFormStateService {
4175
4175
  this.formChangesSub = null;
4176
4176
  });
4177
4177
  }
4178
+ /**
4179
+ * Opciones de subcampos de repeater resueltas por ítem (optionsSource con dependsOn).
4180
+ * Cada FormGroup de ítem guarda las suyas; el registro se reemplaza (inmutable) en cada cambio
4181
+ * para que los consumidores puedan memoizar por referencia.
4182
+ */
4183
+ rowFieldOptions = new WeakMap();
4184
+ rowFieldOptionsVersion = signal(0);
4185
+ setRowFieldOptions(row, fieldName, state) {
4186
+ const current = this.rowFieldOptions.get(row) ?? {};
4187
+ this.rowFieldOptions.set(row, {
4188
+ ...current,
4189
+ [fieldName]: { ...current[fieldName], ...state }
4190
+ });
4191
+ this.rowFieldOptionsVersion.update(version => version + 1);
4192
+ }
4193
+ getRowFieldOptions(row) {
4194
+ return this.rowFieldOptions.get(row);
4195
+ }
4178
4196
  getFieldValue(fieldName) {
4179
4197
  if (!this.currentForm)
4180
4198
  return undefined;
@@ -4189,6 +4207,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImpo
4189
4207
 
4190
4208
  class UicRepeaterComponent {
4191
4209
  fb = inject(FormBuilder);
4210
+ formState = inject(UicFormStateService, { optional: true });
4211
+ itemFieldsCache = new WeakMap();
4192
4212
  formArray;
4193
4213
  field;
4194
4214
  cols = 2;
@@ -4210,6 +4230,31 @@ class UicRepeaterComponent {
4210
4230
  }
4211
4231
  }
4212
4232
  }
4233
+ /**
4234
+ * Subcampos del ítem con las opciones resueltas para ese ítem (optionsSource con dependsOn).
4235
+ * Se memoiza por referencia para no recrear los FormField en cada ciclo de detección.
4236
+ */
4237
+ getItemFields(control) {
4238
+ const base = this.field.repeaterFields ?? [];
4239
+ this.formState?.rowFieldOptionsVersion();
4240
+ const row = control;
4241
+ const state = this.formState?.getRowFieldOptions(row);
4242
+ const cached = this.itemFieldsCache.get(row);
4243
+ if (cached && cached.base === base && cached.state === state)
4244
+ return cached.fields;
4245
+ const fields = !state ? base : base.map(subField => {
4246
+ const rowState = state[subField.name];
4247
+ if (!rowState)
4248
+ return subField;
4249
+ return {
4250
+ ...subField,
4251
+ ...(rowState.options !== undefined ? { options: rowState.options } : {}),
4252
+ ...(rowState.loading !== undefined ? { loading: rowState.loading } : {})
4253
+ };
4254
+ });
4255
+ this.itemFieldsCache.set(row, { base, state, fields });
4256
+ return fields;
4257
+ }
4213
4258
  getGroup(index) {
4214
4259
  return this.formArray.at(index);
4215
4260
  }
@@ -4279,7 +4324,7 @@ class UicRepeaterComponent {
4279
4324
  }
4280
4325
  }
4281
4326
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: UicRepeaterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4282
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: UicRepeaterComponent, isStandalone: true, selector: "ui-repeater", inputs: { formArray: "formArray", field: "field", cols: "cols", error: "error", fileUidResolverFn: "fileUidResolverFn", selectOptionsResolver: "selectOptionsResolver", parentForm: "parentForm" }, ngImport: i0, template: "<div class=\"repeater-label\"> {{field.label}} </div>\r\n<div class=\"uic-repeater\" [class.disabled]=\"formArray.disabled\">\r\n @for (group of formArray.controls; track $index) {\r\n <div class=\"uic-repeater-item\">\r\n <div class=\"uic-repeater-item-header\">\r\n <div class=\"uic-repeater-item-title\">\r\n {{ field.repeaterItemTitle || field.label || 'Item' }} {{ $index + 1 }}\r\n </div>\r\n @if (canRemove && !formArray.disabled) {\r\n <ui-button \r\n [icon]=\"'ri-delete-bin-line'\" \r\n [type]=\"'bordered'\" \r\n [color]=\"'red'\" \r\n size=\"s\" \r\n (click)=\"removeGroup($index)\"\r\n [text]=\"field.repeaterRemoveLabel || 'Eliminar'\">\r\n </ui-button>\r\n }\r\n </div>\r\n <div class=\"uic-repeater-item-body\">\r\n <ui-dynamic-form \r\n [form]=\"getGroup($index)\" \r\n [fields]=\"field.repeaterFields || []\" \r\n [disabled]=\"formArray.disabled\"\r\n [cols]=\"cols\"\r\n [fileUidResolverFn]=\"fileUidResolverFn\"\r\n [selectOptionsResolver]=\"selectOptionsResolver\"\r\n [parentFormValues]=\"getParentFormValues()\"\r\n [isSubForm]=\"true\">\r\n </ui-dynamic-form>\r\n </div>\r\n </div>\r\n }\r\n \r\n @if (canAdd && !formArray.disabled) {\r\n <div class=\"uic-repeater-add-action\">\r\n <ui-button \r\n [icon]=\"'ri-add-line'\" \r\n [type]=\"'bordered'\" \r\n [color]=\"'primary'\" \r\n (click)=\"addGroup()\"\r\n [text]=\"field.repeaterAddLabel || 'A\u00F1adir'\">\r\n </ui-button>\r\n </div>\r\n }\r\n @if (error) {\r\n <p class=\"uic-repeater-error\">{{ error }}</p>\r\n }\r\n</div>\r\n", styles: [".uic-repeater{display:flex;flex-direction:column;gap:16px;width:100%}.uic-repeater.disabled{opacity:.7}.uic-repeater .uic-repeater-item{border:1px solid var(--primary-400, #e0e0e0);border-radius:8px;background-color:var(--uic-bg-color, #fff)}.uic-repeater .uic-repeater-item .uic-repeater-item-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:5px;border-bottom:1px solid var(--primary-400);padding:5px 10px;min-height:34px}.uic-repeater .uic-repeater-item .uic-repeater-item-header .uic-repeater-item-title{margin:0;font-size:14px;font-weight:600;color:var(--primary-700)}.uic-repeater .uic-repeater-item .uic-repeater-item-body{width:100%;padding:0 12px}.uic-repeater .uic-repeater-add-action{margin-top:1px}.uic-repeater .uic-repeater-error{margin:4px 0 0;font-size:12px;color:var(--uic-red, #e53935)}.repeater-label{font-size:max(var(--form-ref) + 4px,14px);line-height:1.125rem;min-height:1.125rem;font-weight:400;color:var(--grey-950);margin-bottom:var(--input-label-space)}\n"], dependencies: [{ kind: "ngmodule", type: i0.forwardRef(() => CommonModule) }, { kind: "ngmodule", type: i0.forwardRef(() => ReactiveFormsModule) }, { kind: "component", type: i0.forwardRef(() => UicButtonComponent), selector: "ui-button", inputs: ["text", "icon", "rightIcon", "iconOnly", "disabled", "loading", "size", "type", "color"] }, { kind: "component", type: i0.forwardRef(() => UicDynamicFormComponent), selector: "ui-dynamic-form", inputs: ["fields", "isSubForm", "form", "disabled", "externalValidationStates", "voiceToTextSilenceMs", "cols", "iconMode", "fileUidResolverFn", "selectOptionsResolver", "parentFormValues"], outputs: ["fieldBlur"] }] });
4327
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: UicRepeaterComponent, isStandalone: true, selector: "ui-repeater", inputs: { formArray: "formArray", field: "field", cols: "cols", error: "error", fileUidResolverFn: "fileUidResolverFn", selectOptionsResolver: "selectOptionsResolver", parentForm: "parentForm" }, ngImport: i0, template: "<div class=\"repeater-label\"> {{field.label}} </div>\r\n<div class=\"uic-repeater\" [class.disabled]=\"formArray.disabled\">\r\n @for (group of formArray.controls; track $index) {\r\n <div class=\"uic-repeater-item\">\r\n <div class=\"uic-repeater-item-header\">\r\n <div class=\"uic-repeater-item-title\">\r\n {{ field.repeaterItemTitle || field.label || 'Item' }} {{ $index + 1 }}\r\n </div>\r\n @if (canRemove && !formArray.disabled) {\r\n <ui-button \r\n [icon]=\"'ri-delete-bin-line'\" \r\n [type]=\"'bordered'\" \r\n [color]=\"'red'\" \r\n size=\"s\" \r\n (click)=\"removeGroup($index)\"\r\n [text]=\"field.repeaterRemoveLabel || 'Eliminar'\">\r\n </ui-button>\r\n }\r\n </div>\r\n <div class=\"uic-repeater-item-body\">\r\n <ui-dynamic-form \r\n [form]=\"getGroup($index)\" \r\n [fields]=\"getItemFields(group)\" \r\n [disabled]=\"formArray.disabled\"\r\n [cols]=\"cols\"\r\n [fileUidResolverFn]=\"fileUidResolverFn\"\r\n [selectOptionsResolver]=\"selectOptionsResolver\"\r\n [parentFormValues]=\"getParentFormValues()\"\r\n [isSubForm]=\"true\">\r\n </ui-dynamic-form>\r\n </div>\r\n </div>\r\n }\r\n \r\n @if (canAdd && !formArray.disabled) {\r\n <div class=\"uic-repeater-add-action\">\r\n <ui-button \r\n [icon]=\"'ri-add-line'\" \r\n [type]=\"'bordered'\" \r\n [color]=\"'primary'\" \r\n (click)=\"addGroup()\"\r\n [text]=\"field.repeaterAddLabel || 'A\u00F1adir'\">\r\n </ui-button>\r\n </div>\r\n }\r\n @if (error) {\r\n <p class=\"uic-repeater-error\">{{ error }}</p>\r\n }\r\n</div>\r\n", styles: [".uic-repeater{display:flex;flex-direction:column;gap:16px;width:100%}.uic-repeater.disabled{opacity:.7}.uic-repeater .uic-repeater-item{border:1px solid var(--primary-400, #e0e0e0);border-radius:8px;background-color:var(--uic-bg-color, #fff)}.uic-repeater .uic-repeater-item .uic-repeater-item-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:5px;border-bottom:1px solid var(--primary-400);padding:5px 10px;min-height:34px}.uic-repeater .uic-repeater-item .uic-repeater-item-header .uic-repeater-item-title{margin:0;font-size:14px;font-weight:600;color:var(--primary-700)}.uic-repeater .uic-repeater-item .uic-repeater-item-body{width:100%;padding:0 12px}.uic-repeater .uic-repeater-add-action{margin-top:1px}.uic-repeater .uic-repeater-error{margin:4px 0 0;font-size:12px;color:var(--uic-red, #e53935)}.repeater-label{font-size:max(var(--form-ref) + 4px,14px);line-height:1.125rem;min-height:1.125rem;font-weight:400;color:var(--grey-950);margin-bottom:var(--input-label-space)}\n"], dependencies: [{ kind: "ngmodule", type: i0.forwardRef(() => CommonModule) }, { kind: "ngmodule", type: i0.forwardRef(() => ReactiveFormsModule) }, { kind: "component", type: i0.forwardRef(() => UicButtonComponent), selector: "ui-button", inputs: ["text", "icon", "rightIcon", "iconOnly", "disabled", "loading", "size", "type", "color"] }, { kind: "component", type: i0.forwardRef(() => UicDynamicFormComponent), selector: "ui-dynamic-form", inputs: ["fields", "isSubForm", "form", "disabled", "externalValidationStates", "voiceToTextSilenceMs", "cols", "iconMode", "fileUidResolverFn", "selectOptionsResolver", "parentFormValues"], outputs: ["fieldBlur"] }] });
4283
4328
  }
4284
4329
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: UicRepeaterComponent, decorators: [{
4285
4330
  type: Component,
@@ -4288,7 +4333,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImpo
4288
4333
  ReactiveFormsModule,
4289
4334
  UicButtonComponent,
4290
4335
  forwardRef(() => UicDynamicFormComponent)
4291
- ], template: "<div class=\"repeater-label\"> {{field.label}} </div>\r\n<div class=\"uic-repeater\" [class.disabled]=\"formArray.disabled\">\r\n @for (group of formArray.controls; track $index) {\r\n <div class=\"uic-repeater-item\">\r\n <div class=\"uic-repeater-item-header\">\r\n <div class=\"uic-repeater-item-title\">\r\n {{ field.repeaterItemTitle || field.label || 'Item' }} {{ $index + 1 }}\r\n </div>\r\n @if (canRemove && !formArray.disabled) {\r\n <ui-button \r\n [icon]=\"'ri-delete-bin-line'\" \r\n [type]=\"'bordered'\" \r\n [color]=\"'red'\" \r\n size=\"s\" \r\n (click)=\"removeGroup($index)\"\r\n [text]=\"field.repeaterRemoveLabel || 'Eliminar'\">\r\n </ui-button>\r\n }\r\n </div>\r\n <div class=\"uic-repeater-item-body\">\r\n <ui-dynamic-form \r\n [form]=\"getGroup($index)\" \r\n [fields]=\"field.repeaterFields || []\" \r\n [disabled]=\"formArray.disabled\"\r\n [cols]=\"cols\"\r\n [fileUidResolverFn]=\"fileUidResolverFn\"\r\n [selectOptionsResolver]=\"selectOptionsResolver\"\r\n [parentFormValues]=\"getParentFormValues()\"\r\n [isSubForm]=\"true\">\r\n </ui-dynamic-form>\r\n </div>\r\n </div>\r\n }\r\n \r\n @if (canAdd && !formArray.disabled) {\r\n <div class=\"uic-repeater-add-action\">\r\n <ui-button \r\n [icon]=\"'ri-add-line'\" \r\n [type]=\"'bordered'\" \r\n [color]=\"'primary'\" \r\n (click)=\"addGroup()\"\r\n [text]=\"field.repeaterAddLabel || 'A\u00F1adir'\">\r\n </ui-button>\r\n </div>\r\n }\r\n @if (error) {\r\n <p class=\"uic-repeater-error\">{{ error }}</p>\r\n }\r\n</div>\r\n", styles: [".uic-repeater{display:flex;flex-direction:column;gap:16px;width:100%}.uic-repeater.disabled{opacity:.7}.uic-repeater .uic-repeater-item{border:1px solid var(--primary-400, #e0e0e0);border-radius:8px;background-color:var(--uic-bg-color, #fff)}.uic-repeater .uic-repeater-item .uic-repeater-item-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:5px;border-bottom:1px solid var(--primary-400);padding:5px 10px;min-height:34px}.uic-repeater .uic-repeater-item .uic-repeater-item-header .uic-repeater-item-title{margin:0;font-size:14px;font-weight:600;color:var(--primary-700)}.uic-repeater .uic-repeater-item .uic-repeater-item-body{width:100%;padding:0 12px}.uic-repeater .uic-repeater-add-action{margin-top:1px}.uic-repeater .uic-repeater-error{margin:4px 0 0;font-size:12px;color:var(--uic-red, #e53935)}.repeater-label{font-size:max(var(--form-ref) + 4px,14px);line-height:1.125rem;min-height:1.125rem;font-weight:400;color:var(--grey-950);margin-bottom:var(--input-label-space)}\n"] }]
4336
+ ], template: "<div class=\"repeater-label\"> {{field.label}} </div>\r\n<div class=\"uic-repeater\" [class.disabled]=\"formArray.disabled\">\r\n @for (group of formArray.controls; track $index) {\r\n <div class=\"uic-repeater-item\">\r\n <div class=\"uic-repeater-item-header\">\r\n <div class=\"uic-repeater-item-title\">\r\n {{ field.repeaterItemTitle || field.label || 'Item' }} {{ $index + 1 }}\r\n </div>\r\n @if (canRemove && !formArray.disabled) {\r\n <ui-button \r\n [icon]=\"'ri-delete-bin-line'\" \r\n [type]=\"'bordered'\" \r\n [color]=\"'red'\" \r\n size=\"s\" \r\n (click)=\"removeGroup($index)\"\r\n [text]=\"field.repeaterRemoveLabel || 'Eliminar'\">\r\n </ui-button>\r\n }\r\n </div>\r\n <div class=\"uic-repeater-item-body\">\r\n <ui-dynamic-form \r\n [form]=\"getGroup($index)\" \r\n [fields]=\"getItemFields(group)\" \r\n [disabled]=\"formArray.disabled\"\r\n [cols]=\"cols\"\r\n [fileUidResolverFn]=\"fileUidResolverFn\"\r\n [selectOptionsResolver]=\"selectOptionsResolver\"\r\n [parentFormValues]=\"getParentFormValues()\"\r\n [isSubForm]=\"true\">\r\n </ui-dynamic-form>\r\n </div>\r\n </div>\r\n }\r\n \r\n @if (canAdd && !formArray.disabled) {\r\n <div class=\"uic-repeater-add-action\">\r\n <ui-button \r\n [icon]=\"'ri-add-line'\" \r\n [type]=\"'bordered'\" \r\n [color]=\"'primary'\" \r\n (click)=\"addGroup()\"\r\n [text]=\"field.repeaterAddLabel || 'A\u00F1adir'\">\r\n </ui-button>\r\n </div>\r\n }\r\n @if (error) {\r\n <p class=\"uic-repeater-error\">{{ error }}</p>\r\n }\r\n</div>\r\n", styles: [".uic-repeater{display:flex;flex-direction:column;gap:16px;width:100%}.uic-repeater.disabled{opacity:.7}.uic-repeater .uic-repeater-item{border:1px solid var(--primary-400, #e0e0e0);border-radius:8px;background-color:var(--uic-bg-color, #fff)}.uic-repeater .uic-repeater-item .uic-repeater-item-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:5px;border-bottom:1px solid var(--primary-400);padding:5px 10px;min-height:34px}.uic-repeater .uic-repeater-item .uic-repeater-item-header .uic-repeater-item-title{margin:0;font-size:14px;font-weight:600;color:var(--primary-700)}.uic-repeater .uic-repeater-item .uic-repeater-item-body{width:100%;padding:0 12px}.uic-repeater .uic-repeater-add-action{margin-top:1px}.uic-repeater .uic-repeater-error{margin:4px 0 0;font-size:12px;color:var(--uic-red, #e53935)}.repeater-label{font-size:max(var(--form-ref) + 4px,14px);line-height:1.125rem;min-height:1.125rem;font-weight:400;color:var(--grey-950);margin-bottom:var(--input-label-space)}\n"] }]
4292
4337
  }], propDecorators: { formArray: [{
4293
4338
  type: Input,
4294
4339
  args: [{ required: true }]
@@ -5055,6 +5100,9 @@ class UicFormWrapperComponent {
5055
5100
  formValueSub = null;
5056
5101
  optionSourceSubs = new Map();
5057
5102
  optionSourceDependencyValueByField = new Map();
5103
+ rowOptionSourceSubs = new Map();
5104
+ rowOptionSourceDependencyKeys = new WeakMap();
5105
+ formState = inject(UicFormStateService);
5058
5106
  computedFieldSubs = new Map();
5059
5107
  computedFieldDependencySignatureByField = new Map();
5060
5108
  computedExternalValidationLastValue = new Map();
@@ -5366,12 +5414,106 @@ class UicFormWrapperComponent {
5366
5414
  if (field.type !== 'repeater' || !field.repeaterFields?.length)
5367
5415
  continue;
5368
5416
  field.repeaterFields.forEach(subField => {
5369
- if (this.hasUsableOptionsSource(subField)) {
5417
+ // Con dependsOn se resuelve por ítem (ver updateRowScopedSubFieldOptionsSources).
5418
+ if (this.hasUsableOptionsSource(subField) && !subField.optionsSource?.dependsOn) {
5370
5419
  this.resolveRepeaterSubFieldOptionsSource(field, subField, force);
5371
5420
  }
5372
5421
  });
5373
5422
  }
5374
5423
  }
5424
+ this.updateRowScopedSubFieldOptionsSources(force);
5425
+ }
5426
+ /**
5427
+ * Subcampos con optionsSource.dependsOn: cada ítem resuelve sus propias opciones.
5428
+ * La dependencia sigue la regla de ámbito "gana el hermano": primero el ítem, luego el raíz
5429
+ * (las relaciones van de menor a mayor o igual ámbito, nunca raíz -> ítem).
5430
+ */
5431
+ updateRowScopedSubFieldOptionsSources(force = false) {
5432
+ const activeRows = new Set();
5433
+ for (const block of this.effectiveSchema?.blocks ?? []) {
5434
+ for (const field of block.fields) {
5435
+ if (field.type !== 'repeater' || !field.repeaterFields?.length)
5436
+ continue;
5437
+ const dependentSubFields = field.repeaterFields.filter(subField => this.hasUsableOptionsSource(subField) && !!subField.optionsSource?.dependsOn);
5438
+ if (!dependentSubFields.length)
5439
+ continue;
5440
+ const formArray = this.form?.get(field.name);
5441
+ if (!formArray)
5442
+ continue;
5443
+ const rootValues = this.toNestedValues(this.form.getRawValue());
5444
+ formArray.controls.forEach(control => {
5445
+ const row = control;
5446
+ activeRows.add(row);
5447
+ dependentSubFields.forEach(subField => this.resolveRowSubFieldOptionsSource(row, subField, rootValues, force));
5448
+ });
5449
+ }
5450
+ }
5451
+ for (const [row, subs] of this.rowOptionSourceSubs) {
5452
+ if (activeRows.has(row))
5453
+ continue;
5454
+ subs.forEach(sub => sub.unsubscribe());
5455
+ this.rowOptionSourceSubs.delete(row);
5456
+ }
5457
+ }
5458
+ resolveRowSubFieldOptionsSource(row, subField, rootValues, force) {
5459
+ const source = subField.optionsSource;
5460
+ const dependsOn = source.dependsOn;
5461
+ if (!this.selectOptionsResolver) {
5462
+ this.formState.setRowFieldOptions(row, subField.name, { options: subField.options ?? [], loading: false });
5463
+ return;
5464
+ }
5465
+ const rowValues = this.toNestedValues(row.getRawValue());
5466
+ const dependencyValue = row.contains(dependsOn)
5467
+ ? row.get([dependsOn])?.value
5468
+ : this.getValueByPath(rootValues, dependsOn);
5469
+ const dependencyKey = `${dependsOn}:${this.safeStringify(dependencyValue ?? null)}`;
5470
+ let dependencyKeys = this.rowOptionSourceDependencyKeys.get(row);
5471
+ if (!dependencyKeys) {
5472
+ dependencyKeys = new Map();
5473
+ this.rowOptionSourceDependencyKeys.set(row, dependencyKeys);
5474
+ }
5475
+ if (!force && dependencyKeys.get(subField.name) === dependencyKey)
5476
+ return;
5477
+ dependencyKeys.set(subField.name, dependencyKey);
5478
+ let subs = this.rowOptionSourceSubs.get(row);
5479
+ if (!subs) {
5480
+ subs = new Map();
5481
+ this.rowOptionSourceSubs.set(row, subs);
5482
+ }
5483
+ subs.get(subField.name)?.unsubscribe();
5484
+ subs.delete(subField.name);
5485
+ const control = row.get([subField.name]);
5486
+ if (dependencyValue === null || dependencyValue === undefined || dependencyValue === '') {
5487
+ if (control)
5488
+ this.clearControlValue(control, subField);
5489
+ this.formState.setRowFieldOptions(row, subField.name, { options: [], loading: false });
5490
+ return;
5491
+ }
5492
+ this.formState.setRowFieldOptions(row, subField.name, { loading: true });
5493
+ const values = { ...rootValues, ...rowValues };
5494
+ let optionsResult;
5495
+ try {
5496
+ optionsResult = this.selectOptionsResolver(source, { field: subField, values, dependencyValue });
5497
+ }
5498
+ catch (error) {
5499
+ this.handleRowOptionsSourceError(row, subField, source, error);
5500
+ return;
5501
+ }
5502
+ const sub = this.toObservable(optionsResult).subscribe({
5503
+ next: items => {
5504
+ const options = this.mapOptionsSourceItems(items, source);
5505
+ this.formState.setRowFieldOptions(row, subField.name, { options, loading: false });
5506
+ if (control)
5507
+ this.clearInvalidControlValue(control, options);
5508
+ },
5509
+ error: (error) => this.handleRowOptionsSourceError(row, subField, source, error),
5510
+ complete: () => this.formState.setRowFieldOptions(row, subField.name, { loading: false })
5511
+ });
5512
+ subs.set(subField.name, sub);
5513
+ }
5514
+ handleRowOptionsSourceError(row, subField, source, error) {
5515
+ this.formState.setRowFieldOptions(row, subField.name, { options: [], loading: false });
5516
+ this.optionsSourceError.emit({ field: subField, source, error });
5375
5517
  }
5376
5518
  resolveRepeaterSubFieldOptionsSource(parentField, subField, force = false) {
5377
5519
  const source = subField.optionsSource;
@@ -5443,17 +5585,8 @@ class UicFormWrapperComponent {
5443
5585
  this.updateDependentRepeaterSubFieldOptionsSources();
5444
5586
  }
5445
5587
  updateDependentRepeaterSubFieldOptionsSources() {
5446
- for (const block of this.effectiveSchema?.blocks ?? []) {
5447
- for (const field of block.fields) {
5448
- if (field.type !== 'repeater' || !field.repeaterFields?.length)
5449
- continue;
5450
- field.repeaterFields.forEach(subField => {
5451
- if (this.hasUsableOptionsSource(subField) && subField.optionsSource?.dependsOn) {
5452
- this.resolveRepeaterSubFieldOptionsSource(field, subField, true);
5453
- }
5454
- });
5455
- }
5456
- }
5588
+ // Sin force: cada ítem solo vuelve a resolver si cambió el valor de su dependencia.
5589
+ this.updateRowScopedSubFieldOptionsSources();
5457
5590
  }
5458
5591
  resolveOptionsSourceField(field, force = false) {
5459
5592
  const source = field.optionsSource;
@@ -5578,6 +5711,9 @@ class UicFormWrapperComponent {
5578
5711
  const control = this.getFieldControl(fieldName);
5579
5712
  if (!control)
5580
5713
  return;
5714
+ this.clearInvalidControlValue(control, options);
5715
+ }
5716
+ clearInvalidControlValue(control, options) {
5581
5717
  const currentValue = control.value;
5582
5718
  if (currentValue === null || currentValue === undefined || currentValue === '')
5583
5719
  return;
@@ -5599,6 +5735,9 @@ class UicFormWrapperComponent {
5599
5735
  const control = this.getFieldControl(field.name);
5600
5736
  if (!control)
5601
5737
  return;
5738
+ this.clearControlValue(control, field);
5739
+ }
5740
+ clearControlValue(control, field) {
5602
5741
  const blankValue = this.blankValue(field);
5603
5742
  if (control.value !== blankValue) {
5604
5743
  control.setValue(blankValue, { emitEvent: false });
@@ -5631,6 +5770,8 @@ class UicFormWrapperComponent {
5631
5770
  this.optionSourceSubs.forEach(sub => sub.unsubscribe());
5632
5771
  this.optionSourceSubs.clear();
5633
5772
  this.optionSourceDependencyValueByField.clear();
5773
+ this.rowOptionSourceSubs.forEach(subs => subs.forEach(sub => sub.unsubscribe()));
5774
+ this.rowOptionSourceSubs.clear();
5634
5775
  }
5635
5776
  clearComputedFieldSubs() {
5636
5777
  this.computedFieldSubs.forEach(sub => sub.unsubscribe());
@@ -7544,13 +7685,12 @@ function helperShowFormFromBuilder(blocks, cols, editable = false, schemaOptions
7544
7685
  };
7545
7686
  }
7546
7687
  function mapEditableBlocksToBlocks(blocks, editable) {
7688
+ // Solo campos raíz: un campo raíz nunca depende de un subcampo (raíz -> ítem no existe),
7689
+ // y los códigos sf_N se repiten entre repeaters, así que no pueden ir en un mapa global.
7547
7690
  const fieldCodeToName = new Map();
7548
7691
  blocks.forEach(block => {
7549
7692
  block.fields.forEach(field => {
7550
7693
  fieldCodeToName.set(field.code, field.fieldData.name);
7551
- field.repeaterSubFields?.forEach(sf => {
7552
- fieldCodeToName.set(sf.code, sf.fieldData.name);
7553
- });
7554
7694
  });
7555
7695
  });
7556
7696
  return blocks.map(block => ({
@@ -7569,9 +7709,9 @@ function mapEditableBlocksToBlocks(blocks, editable) {
7569
7709
  }
7570
7710
  : field.fieldData.optionsSource;
7571
7711
  const isComputedField = field.fieldData.type === 'computed';
7572
- // Los códigos sf_N se repiten entre repeaters: las reglas de un subcampo
7573
- // se resuelven primero contra los hermanos de su propio repeater.
7712
+ // Referencias de un subcampo: gana el hermano del mismo repeater, luego el raíz.
7574
7713
  const siblingCodeToName = new Map((field.repeaterSubFields ?? []).map(sf => [sf.code, sf.fieldData.name]));
7714
+ const resolveSubFieldRef = (code) => siblingCodeToName.get(code) ?? fieldCodeToName.get(code) ?? code;
7575
7715
  return {
7576
7716
  ...field.fieldData,
7577
7717
  type: isComputedField ? 'text' : field.fieldData.type,
@@ -7590,13 +7730,12 @@ function mapEditableBlocksToBlocks(blocks, editable) {
7590
7730
  const sfIsComputed = sf.fieldData.type === 'computed';
7591
7731
  const sfVisibilityRules = sf.fieldData.visibilityRules?.map(rule => ({
7592
7732
  ...rule,
7593
- fieldName: siblingCodeToName.get(rule.fieldName) ?? fieldCodeToName.get(rule.fieldName) ?? rule.fieldName
7733
+ fieldName: resolveSubFieldRef(rule.fieldName)
7594
7734
  }));
7595
7735
  const sfOptionsSource = sf.fieldData.optionsSource?.dependsOn
7596
7736
  ? {
7597
7737
  ...sf.fieldData.optionsSource,
7598
- dependsOn: fieldCodeToName.get(sf.fieldData.optionsSource.dependsOn)
7599
- ?? sf.fieldData.optionsSource.dependsOn
7738
+ dependsOn: resolveSubFieldRef(sf.fieldData.optionsSource.dependsOn)
7600
7739
  }
7601
7740
  : sf.fieldData.optionsSource;
7602
7741
  return {
@@ -7609,7 +7748,7 @@ function mapEditableBlocksToBlocks(blocks, editable) {
7609
7748
  ? {
7610
7749
  computedSource: {
7611
7750
  key: sf.fieldData.computedSource.key,
7612
- dependsOn: (sf.fieldData.computedSource.dependsOn ?? []).map(code => fieldCodeToName.get(code) ?? code)
7751
+ dependsOn: (sf.fieldData.computedSource.dependsOn ?? []).map(resolveSubFieldRef)
7613
7752
  }
7614
7753
  }
7615
7754
  : {})