ui-core-abv 0.10.4 → 0.10.5

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());