@telcomdev/ui 0.1.16 → 0.1.17

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/README.md CHANGED
@@ -76,8 +76,64 @@ el borde outline permanecen visibles en todos los estados.
76
76
  />
77
77
  ```
78
78
 
79
- Ambos controles usan `TdSelectOption<T>`, son compatibles con formularios
80
- Angular y muestran su panel mediante Angular CDK Overlay.
79
+ Ambos controles son compatibles con formularios Angular y muestran su panel
80
+ mediante Angular CDK Overlay.
81
+
82
+ `td-autocomplete-select` soporta dos formas de trabajo:
83
+
84
+ ### Modo estándar
85
+
86
+ Usa `TdSelectOption<T>` cuando ya tienes opciones normalizadas:
87
+
88
+ ```ts
89
+ readonly proveedores = [
90
+ { value: 1, label: 'AMERICA MOVIL PERU S.A.C.', description: '20467534026' },
91
+ ];
92
+
93
+ proveedorId: number | null = 1;
94
+ ```
95
+
96
+ ```html
97
+ <td-autocomplete-select
98
+ label="Proveedor"
99
+ [options]="proveedores"
100
+ [(ngModel)]="proveedorId"
101
+ />
102
+ ```
103
+
104
+ ### Modo mapeado
105
+
106
+ Usa `valueKey`, `labelKey` y, opcionalmente, `descriptionKey`, `iconKey` o
107
+ `disabledKey` cuando quieres consumir DTOs reales sin mapearlos manualmente:
108
+
109
+ ```ts
110
+ readonly proveedores = [
111
+ {
112
+ proveedorId: 1,
113
+ tipoDocumentoIdentidadId: 4,
114
+ numeroDocumento: '20467534026',
115
+ nombre: 'AMERICA MOVIL PERU S.A.C.',
116
+ },
117
+ ];
118
+
119
+ proveedorId: number | null = 1;
120
+ ```
121
+
122
+ ```html
123
+ <td-autocomplete-select
124
+ label="Proveedor"
125
+ [options]="proveedores"
126
+ valueKey="proveedorId"
127
+ labelKey="nombre"
128
+ descriptionKey="numeroDocumento"
129
+ [(ngModel)]="proveedorId"
130
+ />
131
+ ```
132
+
133
+ Si el valor llega antes que las opciones, o las opciones llegan antes que el
134
+ valor, el texto visible se sincroniza automáticamente. Esta sincronización no
135
+ emite `selectionChange`; ese evento se reserva para selección o limpieza
136
+ realizada por el usuario.
81
137
 
82
138
  El comportamiento predeterminado durante el scroll es `reposition`, por lo que
83
139
  el panel permanece unido al control. Puede cambiarse con
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { ChangeDetectionStrategy, Component, Injectable, inject, HostBinding, Input, EventEmitter, booleanAttribute, Output, ViewChild, Injector, ChangeDetectorRef, signal, input, model, output, computed, forwardRef, InjectionToken, HostListener, Directive, EnvironmentInjector, DestroyRef, TemplateRef, ViewChildren, ContentChildren, effect } from '@angular/core';
2
+ import { ChangeDetectionStrategy, Component, Injectable, inject, HostBinding, Input, EventEmitter, booleanAttribute, Output, ViewChild, Injector, ChangeDetectorRef, signal, input, model, output, computed, forwardRef, InjectionToken, HostListener, Directive, EnvironmentInjector, DestroyRef, effect, TemplateRef, ViewChildren, ContentChildren } from '@angular/core';
3
3
  import * as i1 from '@angular/forms';
4
4
  import { NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
5
5
  import { FORM_FIELD } from '@angular/forms/signals';
@@ -4157,14 +4157,28 @@ class TdAutocompleteSelect {
4157
4157
  destroyRef = inject(DestroyRef);
4158
4158
  scrollStrategies = inject(ScrollStrategyOptions);
4159
4159
  generatedId = `td-autocomplete-${++autocompleteSequence}`;
4160
+ rawOptions = [];
4161
+ normalizedOptions = [];
4160
4162
  searchInput;
4161
4163
  connectedOverlay;
4162
4164
  panel;
4163
4165
  label = 'Buscar y seleccionar';
4164
4166
  placeholder = 'Escribe para buscar...';
4165
- options = [];
4167
+ set options(value) {
4168
+ this.rawOptions = value ?? [];
4169
+ this.normalizedOptions = this.normalizeOptions(this.rawOptions);
4170
+ this.syncSearchWithValue();
4171
+ }
4172
+ get options() {
4173
+ return this.normalizedOptions;
4174
+ }
4166
4175
  hint = '';
4167
4176
  hideLabel = false;
4177
+ valueKey = null;
4178
+ labelKey = null;
4179
+ descriptionKey = null;
4180
+ iconKey = null;
4181
+ disabledKey = null;
4168
4182
  name = input('', /* @ts-ignore */
4169
4183
  ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
4170
4184
  disabled = input(false, /* @ts-ignore */
@@ -4277,11 +4291,31 @@ class TdAutocompleteSelect {
4277
4291
  };
4278
4292
  this.document.addEventListener('scroll', scrollListener, true);
4279
4293
  this.destroyRef.onDestroy(() => this.document.removeEventListener('scroll', scrollListener, true));
4294
+ effect(() => {
4295
+ this.value();
4296
+ this.syncSearchWithValue();
4297
+ });
4298
+ }
4299
+ ngOnChanges(changes) {
4300
+ if (changes['valueKey'] ||
4301
+ changes['labelKey'] ||
4302
+ changes['descriptionKey'] ||
4303
+ changes['iconKey'] ||
4304
+ changes['disabledKey']) {
4305
+ this.normalizedOptions = this.normalizeOptions(this.rawOptions);
4306
+ }
4307
+ if (changes['valueKey'] ||
4308
+ changes['labelKey'] ||
4309
+ changes['descriptionKey'] ||
4310
+ changes['iconKey'] ||
4311
+ changes['disabledKey'] ||
4312
+ changes['compareWith']) {
4313
+ this.syncSearchWithValue();
4314
+ }
4280
4315
  }
4281
4316
  writeValue(value) {
4282
4317
  this.value.set(value ?? null);
4283
- this.search =
4284
- this.options.find((option) => this.compare(option.value, this.value()))?.label ?? '';
4318
+ this.syncSearchWithValue();
4285
4319
  this.cdr.markForCheck();
4286
4320
  }
4287
4321
  registerOnChange(fn) {
@@ -4327,9 +4361,7 @@ class TdAutocompleteSelect {
4327
4361
  if (!this.open)
4328
4362
  return;
4329
4363
  this.open = false;
4330
- const selected = this.options.find((option) => this.compare(option.value, this.value()));
4331
- if (selected)
4332
- this.search = selected.label;
4364
+ this.syncSearchWithValue();
4333
4365
  this.formBridge.markTouched(this.touched, this.touch);
4334
4366
  this.cdr.markForCheck();
4335
4367
  }
@@ -4417,6 +4449,82 @@ class TdAutocompleteSelect {
4417
4449
  firstEnabledIndex() {
4418
4450
  return this.filteredOptions.findIndex((option) => !option.disabled);
4419
4451
  }
4452
+ syncSearchWithValue() {
4453
+ const currentValue = this.value();
4454
+ if (currentValue === null) {
4455
+ if (!this.focused() && !this.open) {
4456
+ this.search = '';
4457
+ }
4458
+ this.cdr.markForCheck();
4459
+ return;
4460
+ }
4461
+ const selected = this.options.find((option) => this.compare(option.value, currentValue));
4462
+ if (selected) {
4463
+ this.search = selected.label;
4464
+ }
4465
+ this.cdr.markForCheck();
4466
+ }
4467
+ normalizeOptions(options) {
4468
+ return options.map((option) => this.normalizeOption(option));
4469
+ }
4470
+ normalizeOption(option) {
4471
+ if (this.isSelectOption(option) && !this.valueKey && !this.labelKey) {
4472
+ return option;
4473
+ }
4474
+ const value = this.valueKey ? this.readOptionValue(option, this.valueKey) : this.readOptionValue(option, 'value');
4475
+ const label = this.labelKey ? this.readOptionText(option, this.labelKey) : this.readOptionText(option, 'label');
4476
+ const description = this.descriptionKey
4477
+ ? this.readOptionText(option, this.descriptionKey)
4478
+ : this.readOptionText(option, 'description');
4479
+ const icon = this.iconKey ? this.readOptionText(option, this.iconKey) : this.readOptionText(option, 'icon');
4480
+ const disabled = this.disabledKey
4481
+ ? !!this.readOptionUnknown(option, this.disabledKey)
4482
+ : !!this.readOptionUnknown(option, 'disabled');
4483
+ return {
4484
+ value,
4485
+ label: label || this.fallbackOptionLabel(option, value),
4486
+ ...(description ? { description } : {}),
4487
+ ...(icon ? { icon } : {}),
4488
+ ...(disabled ? { disabled } : {}),
4489
+ };
4490
+ }
4491
+ isSelectOption(option) {
4492
+ if (!option || typeof option !== 'object')
4493
+ return false;
4494
+ return 'value' in option && 'label' in option;
4495
+ }
4496
+ readOptionValue(option, accessor) {
4497
+ if (typeof accessor === 'function') {
4498
+ return accessor(option);
4499
+ }
4500
+ return this.readPath(option, accessor);
4501
+ }
4502
+ readOptionText(option, accessor) {
4503
+ const value = typeof accessor === 'function' ? accessor(option) : this.readPath(option, accessor);
4504
+ return value === null || value === undefined ? '' : String(value);
4505
+ }
4506
+ readOptionUnknown(option, accessor) {
4507
+ return typeof accessor === 'function' ? accessor(option) : this.readPath(option, accessor);
4508
+ }
4509
+ readPath(source, path) {
4510
+ if (!path.trim())
4511
+ return undefined;
4512
+ return path.split('.').reduce((current, key) => {
4513
+ if (current === null || current === undefined || typeof current !== 'object') {
4514
+ return undefined;
4515
+ }
4516
+ return current[key];
4517
+ }, source);
4518
+ }
4519
+ fallbackOptionLabel(option, value) {
4520
+ if (typeof option === 'string' || typeof option === 'number' || typeof option === 'boolean') {
4521
+ return String(option);
4522
+ }
4523
+ if (value !== null && value !== undefined) {
4524
+ return String(value);
4525
+ }
4526
+ return '';
4527
+ }
4420
4528
  scrollActiveOptionIntoView() {
4421
4529
  const panel = this.panel?.nativeElement;
4422
4530
  if (!panel || this.activeIndex < 0)
@@ -4473,13 +4581,13 @@ class TdAutocompleteSelect {
4473
4581
  });
4474
4582
  }
4475
4583
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: TdAutocompleteSelect, deps: [], target: i0.ɵɵFactoryTarget.Component });
4476
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.4", type: TdAutocompleteSelect, isStandalone: true, selector: "td-autocomplete-select", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: false, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: false, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: false, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: false, isRequired: false, transformFunction: null }, hideLabel: { classPropertyName: "hideLabel", publicName: "hideLabel", isSignal: false, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, disabledReasons: { classPropertyName: "disabledReasons", publicName: "disabledReasons", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, pending: { classPropertyName: "pending", publicName: "pending", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, errorDisplay: { classPropertyName: "errorDisplay", publicName: "errorDisplay", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: false, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: false, isRequired: false, transformFunction: null }, dark: { classPropertyName: "dark", publicName: "dark", isSignal: false, isRequired: false, transformFunction: null }, scrollBehavior: { classPropertyName: "scrollBehavior", publicName: "scrollBehavior", isSignal: false, isRequired: false, transformFunction: null }, overflowText: { classPropertyName: "overflowText", publicName: "overflowText", isSignal: false, isRequired: false, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: false, isRequired: false, transformFunction: null }, minimumCharacters: { classPropertyName: "minimumCharacters", publicName: "minimumCharacters", isSignal: false, isRequired: false, transformFunction: null }, filterWith: { classPropertyName: "filterWith", publicName: "filterWith", isSignal: false, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: false, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { touched: "touchedChange", touch: "touch", selectionChange: "selectionChange", searchChange: "searchChange", value: "valueChange" }, host: { properties: { "hidden": "hidden()", "attr.aria-busy": "pending() ? \"true\" : null" } }, providers: [
4584
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.4", type: TdAutocompleteSelect, isStandalone: true, selector: "td-autocomplete-select", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: false, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: false, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: false, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: false, isRequired: false, transformFunction: null }, hideLabel: { classPropertyName: "hideLabel", publicName: "hideLabel", isSignal: false, isRequired: false, transformFunction: null }, valueKey: { classPropertyName: "valueKey", publicName: "valueKey", isSignal: false, isRequired: false, transformFunction: null }, labelKey: { classPropertyName: "labelKey", publicName: "labelKey", isSignal: false, isRequired: false, transformFunction: null }, descriptionKey: { classPropertyName: "descriptionKey", publicName: "descriptionKey", isSignal: false, isRequired: false, transformFunction: null }, iconKey: { classPropertyName: "iconKey", publicName: "iconKey", isSignal: false, isRequired: false, transformFunction: null }, disabledKey: { classPropertyName: "disabledKey", publicName: "disabledKey", isSignal: false, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, disabledReasons: { classPropertyName: "disabledReasons", publicName: "disabledReasons", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, pending: { classPropertyName: "pending", publicName: "pending", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, errorDisplay: { classPropertyName: "errorDisplay", publicName: "errorDisplay", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: false, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: false, isRequired: false, transformFunction: null }, dark: { classPropertyName: "dark", publicName: "dark", isSignal: false, isRequired: false, transformFunction: null }, scrollBehavior: { classPropertyName: "scrollBehavior", publicName: "scrollBehavior", isSignal: false, isRequired: false, transformFunction: null }, overflowText: { classPropertyName: "overflowText", publicName: "overflowText", isSignal: false, isRequired: false, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: false, isRequired: false, transformFunction: null }, minimumCharacters: { classPropertyName: "minimumCharacters", publicName: "minimumCharacters", isSignal: false, isRequired: false, transformFunction: null }, filterWith: { classPropertyName: "filterWith", publicName: "filterWith", isSignal: false, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: false, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { touched: "touchedChange", touch: "touch", selectionChange: "selectionChange", searchChange: "searchChange", value: "valueChange" }, host: { properties: { "hidden": "hidden()", "attr.aria-busy": "pending() ? \"true\" : null" } }, providers: [
4477
4585
  {
4478
4586
  provide: NG_VALUE_ACCESSOR,
4479
4587
  useExisting: forwardRef(() => TdAutocompleteSelect),
4480
4588
  multi: true,
4481
4589
  },
4482
- ], viewQueries: [{ propertyName: "searchInput", first: true, predicate: ["searchInput"], descendants: true }, { propertyName: "connectedOverlay", first: true, predicate: CdkConnectedOverlay, descendants: true }, { propertyName: "panel", first: true, predicate: ["panel"], descendants: true }], ngImport: i0, template: `
4590
+ ], viewQueries: [{ propertyName: "searchInput", first: true, predicate: ["searchInput"], descendants: true }, { propertyName: "connectedOverlay", first: true, predicate: CdkConnectedOverlay, descendants: true }, { propertyName: "panel", first: true, predicate: ["panel"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
4483
4591
  <div
4484
4592
  cdkOverlayOrigin
4485
4593
  #origin="cdkOverlayOrigin"
@@ -4734,6 +4842,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
4734
4842
  type: Input
4735
4843
  }], hideLabel: [{
4736
4844
  type: Input
4845
+ }], valueKey: [{
4846
+ type: Input
4847
+ }], labelKey: [{
4848
+ type: Input
4849
+ }], descriptionKey: [{
4850
+ type: Input
4851
+ }], iconKey: [{
4852
+ type: Input
4853
+ }], disabledKey: [{
4854
+ type: Input
4737
4855
  }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], disabledReasons: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabledReasons", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], hidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "hidden", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], pending: [{ type: i0.Input, args: [{ isSignal: true, alias: "pending", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], touch: [{ type: i0.Output, args: ["touch"] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], errorDisplay: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorDisplay", required: false }] }], emptyText: [{
4738
4856
  type: Input
4739
4857
  }], clearable: [{