tailjng 0.1.12 → 0.1.13

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": "tailjng",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "peerDependencies": {
5
5
  "@angular/common": "^19.2.0",
6
6
  "@angular/core": "^19.2.0",
@@ -1,4 +1,4 @@
1
- import type { FilterSelect } from 'tailjng';
1
+ import type { FilterSelect } from '../../../interfaces/crud/filter.interface';
2
2
 
3
3
  /** Whether search text or any filter select differs from its initial value. */
4
4
  export function hasActiveFilters(
@@ -1,6 +1,7 @@
1
1
  export type ToggleOption<T = unknown> = {
2
2
  value: T;
3
3
  label: string;
4
+ original?: unknown;
4
5
  };
5
6
 
6
7
  export type ToggleSortOrder = 'ASC' | 'DESC';
@@ -18,22 +18,105 @@ export function buildToggleLoadParams(
18
18
  return params;
19
19
  }
20
20
 
21
+ /** Reads a nested property using dot notation (`a.b.c`). */
22
+ export function getNestedToggleRawValue(obj: unknown, path: string): unknown {
23
+ if (!path?.trim()) {
24
+ return '';
25
+ }
26
+
27
+ let value: unknown = obj;
28
+
29
+ for (const part of path.split('.').filter(Boolean)) {
30
+ if (value == null || typeof value !== 'object') {
31
+ return '';
32
+ }
33
+
34
+ value = (value as Record<string, unknown>)[part];
35
+ }
36
+
37
+ return value ?? '';
38
+ }
39
+
21
40
  /**
22
- * Reads a nested property using dot notation.
41
+ * Reads a nested property using dot notation and returns a display string.
23
42
  */
24
43
  export function getNestedToggleValue(obj: unknown, path: string): string {
25
- if (!obj || typeof obj !== 'object') {
26
- return '';
44
+ const value = getNestedToggleRawValue(obj, path);
45
+ return value == null || value === '' ? '' : String(value);
46
+ }
47
+
48
+ /**
49
+ * Resolves the options array from `response.data`.
50
+ *
51
+ * Use `dataPath` with dot notation for nested shapes, e.g.:
52
+ * - `users` → `data.users`
53
+ * - `typeAccess.options` → `data.typeAccess.options`
54
+ *
55
+ * When `dataPath` / `responseKey` are omitted, falls back to legacy behavior:
56
+ * `data[firstEndpointSegment]` if it is an array (e.g. endpoint `role/list` → `data.role`).
57
+ */
58
+ export function extractToggleOptionsFromResponse(
59
+ responseData: Record<string, unknown> | undefined | null,
60
+ endpoint = '',
61
+ responseKey?: string,
62
+ dataPath?: string,
63
+ ): unknown[] {
64
+ if (!responseData || typeof responseData !== 'object') {
65
+ return [];
27
66
  }
28
67
 
29
- const value = path.split('.').reduce<unknown>((acc, part) => {
30
- if (acc && typeof acc === 'object' && part in (acc as Record<string, unknown>)) {
31
- return (acc as Record<string, unknown>)[part];
68
+ const explicitPath = dataPath?.trim() || responseKey?.trim() || '';
69
+
70
+ if (explicitPath) {
71
+ const node = getNestedToggleRawValue(responseData, explicitPath);
72
+ return Array.isArray(node) ? node : [];
73
+ }
74
+
75
+ const legacyKey = endpoint.split('/').filter(Boolean)[0];
76
+ if (legacyKey) {
77
+ const legacyNode = responseData[legacyKey];
78
+ if (Array.isArray(legacyNode)) {
79
+ return legacyNode;
80
+ }
81
+ }
82
+
83
+ return Array.isArray(responseData) ? (responseData as unknown[]) : [];
84
+ }
85
+
86
+ /** Builds the display label from a template like `Acceso {label} (id {value})`. */
87
+ export function resolveToggleOptionLabelFromTemplate(
88
+ option: Record<string, unknown>,
89
+ template: string,
90
+ ): string {
91
+ return template.replace(/\{([^}]+)\}/g, (_, path: string) => {
92
+ const value = getNestedToggleRawValue(option, path.trim());
93
+ if (value == null || value === '') {
94
+ return '';
32
95
  }
33
- return undefined;
34
- }, obj);
35
96
 
36
- return value == null ? '' : String(value);
97
+ return String(value);
98
+ });
99
+ }
100
+
101
+ /** Builds the display label for one option object. */
102
+ export function resolveToggleOptionLabel(
103
+ option: Record<string, unknown>,
104
+ optionLabel: string | string[],
105
+ labelSeparator: string,
106
+ optionLabelTemplate?: string,
107
+ ): string {
108
+ if (optionLabelTemplate?.trim()) {
109
+ return resolveToggleOptionLabelFromTemplate(option, optionLabelTemplate);
110
+ }
111
+
112
+ if (Array.isArray(optionLabel)) {
113
+ return optionLabel
114
+ .map((key) => getNestedToggleValue(option, key))
115
+ .filter(Boolean)
116
+ .join(labelSeparator);
117
+ }
118
+
119
+ return getNestedToggleValue(option, optionLabel);
37
120
  }
38
121
 
39
122
  /**
@@ -41,18 +124,34 @@ export function getNestedToggleValue(obj: unknown, path: string): string {
41
124
  */
42
125
  export function normalizeToggleOptions(
43
126
  options: unknown[],
44
- optionLabel: string,
127
+ optionLabel: string | string[],
45
128
  optionValue: string,
129
+ labelSeparator = ' ',
130
+ optionLabelTemplate?: string,
131
+ optionLabelFn?: (option: Record<string, unknown>) => string,
46
132
  ): ToggleOption[] {
47
133
  if (!options.length) {
48
134
  return [];
49
135
  }
50
136
 
51
137
  if (typeof options[0] === 'object' && options[0] !== null) {
52
- return options.map((opt) => ({
53
- value: (opt as Record<string, unknown>)[optionValue],
54
- label: getNestedToggleValue(opt, optionLabel),
55
- }));
138
+ return options.map((opt) => {
139
+ const record = opt as Record<string, unknown>;
140
+ const label = optionLabelFn
141
+ ? optionLabelFn(record)
142
+ : resolveToggleOptionLabel(
143
+ record,
144
+ optionLabel,
145
+ labelSeparator,
146
+ optionLabelTemplate,
147
+ );
148
+
149
+ return {
150
+ value: getNestedToggleRawValue(record, optionValue),
151
+ label,
152
+ original: record,
153
+ };
154
+ });
56
155
  }
57
156
 
58
157
  return options.map((opt) => ({
@@ -14,7 +14,11 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
14
14
  import { JGenericCrudService } from 'tailjng';
15
15
  import { Icons } from '../../.config/icons/icons.lucide';
16
16
  import { JIconComponent } from '../../icon/icon.component';
17
- import { normalizeToggleOptions, buildToggleLoadParams } from '../shared/toggle-options.util';
17
+ import {
18
+ buildToggleLoadParams,
19
+ extractToggleOptionsFromResponse,
20
+ normalizeToggleOptions,
21
+ } from '../shared/toggle-options.util';
18
22
  import { ToggleOption, ToggleRadioLayout, ToggleSortOrder } from '../shared/toggle-options.types';
19
23
 
20
24
  export type { ToggleOption, ToggleRadioLayout, ToggleSortOrder } from './toggle-radio.types';
@@ -25,6 +29,18 @@ let radioGroupCounter = 0;
25
29
  * Classic radio group with circular indicators and labels.
26
30
  *
27
31
  * Install: `npx tailjng add toggle-radio`
32
+ *
33
+ * ```html
34
+ * <JToggleRadio
35
+ * endpoint="enum/typeAccess"
36
+ * dataPath="typeAccess.options"
37
+ * optionLabel="label"
38
+ * optionValue="value"
39
+ * optionLabelTemplate="Acceso {label} (id {value})"
40
+ * [loadOnInit]="true"
41
+ * [(ngModel)]="selected"
42
+ * />
43
+ * ```
28
44
  */
29
45
  @Component({
30
46
  selector: 'JToggleRadio',
@@ -45,8 +61,32 @@ export class JToggleRadioComponent implements OnInit, OnChanges, ControlValueAcc
45
61
  @Input() name = `j-toggle-radio-${++radioGroupCounter}`;
46
62
  @Input() endpoint = '';
47
63
  @Input() options: unknown[] = [];
48
- @Input() optionLabel = 'label';
64
+ @Input() optionLabel: string | string[] = 'label';
49
65
  @Input() optionValue = 'value';
66
+
67
+ /** Separator when `optionLabel` is an array of keys. */
68
+ @Input() labelSeparator = ' ';
69
+
70
+ /**
71
+ * Custom label template per option. Use `{field}` placeholders with dot paths.
72
+ * Example: `Acceso {label} (id {value})`.
73
+ */
74
+ @Input() optionLabelTemplate = '';
75
+
76
+ /**
77
+ * Optional formatter for full control over the displayed label.
78
+ * Takes precedence over `optionLabelTemplate` when provided.
79
+ */
80
+ @Input() optionLabelFn?: (option: Record<string, unknown>) => string;
81
+
82
+ /** @deprecated Alias of `dataPath` for a single top-level key in `response.data`. */
83
+ @Input() responseKey = '';
84
+
85
+ /**
86
+ * Dot path from `response.data` to the options array.
87
+ * Examples: `users`, `typeAccess.options`, `catalog.items`.
88
+ */
89
+ @Input() dataPath = '';
50
90
  @Input() sort: ToggleSortOrder = 'ASC';
51
91
  @Input() defaultFilters: Record<string, unknown> = {};
52
92
  @Input() loadOnInit = false;
@@ -99,7 +139,16 @@ export class JToggleRadioComponent implements OnInit, OnChanges, ControlValueAcc
99
139
  }
100
140
 
101
141
  ngOnChanges(changes: SimpleChanges): void {
102
- if (changes['options'] && !this.endpoint) {
142
+ const optionInputs = [
143
+ 'options',
144
+ 'optionLabel',
145
+ 'optionValue',
146
+ 'labelSeparator',
147
+ 'optionLabelTemplate',
148
+ 'optionLabelFn',
149
+ ];
150
+
151
+ if (optionInputs.some((key) => changes[key]) && !this.endpoint) {
103
152
  this.processOptions();
104
153
  }
105
154
  }
@@ -119,7 +168,12 @@ export class JToggleRadioComponent implements OnInit, OnChanges, ControlValueAcc
119
168
 
120
169
  this.genericService.findAll<unknown>({ endpoint: this.endpoint, params }).subscribe({
121
170
  next: (res) => {
122
- const data = (res.data as Record<string, unknown[]>)[this.endpoint] ?? [];
171
+ const data = extractToggleOptionsFromResponse(
172
+ res.data as Record<string, unknown>,
173
+ this.endpoint,
174
+ this.responseKey || undefined,
175
+ this.dataPath || undefined,
176
+ );
123
177
  this.options = data;
124
178
  this.processOptions();
125
179
  this.isLoading = false;
@@ -135,7 +189,14 @@ export class JToggleRadioComponent implements OnInit, OnChanges, ControlValueAcc
135
189
  }
136
190
 
137
191
  processOptions(): void {
138
- this.internalOptions = normalizeToggleOptions(this.options, this.optionLabel, this.optionValue);
192
+ this.internalOptions = normalizeToggleOptions(
193
+ this.options,
194
+ this.optionLabel,
195
+ this.optionValue,
196
+ this.labelSeparator,
197
+ this.optionLabelFn ? undefined : this.optionLabelTemplate || undefined,
198
+ this.optionLabelFn,
199
+ );
139
200
  }
140
201
 
141
202
  select(value: unknown): void {
@@ -14,7 +14,11 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
14
14
  import { JGenericCrudService } from 'tailjng';
15
15
  import { Icons } from '../../.config/icons/icons.lucide';
16
16
  import { JIconComponent } from '../../icon/icon.component';
17
- import { normalizeToggleOptions, buildToggleLoadParams } from '../shared/toggle-options.util';
17
+ import {
18
+ buildToggleLoadParams,
19
+ extractToggleOptionsFromResponse,
20
+ normalizeToggleOptions,
21
+ } from '../shared/toggle-options.util';
18
22
  import { ToggleOption, ToggleSortOrder } from '../shared/toggle-options.types';
19
23
 
20
24
  export type { ToggleOption, ToggleSortOrder } from './segment-toggle.types';
@@ -23,6 +27,18 @@ export type { ToggleOption, ToggleSortOrder } from './segment-toggle.types';
23
27
  * Segmented single-select control (pill/tab style).
24
28
  *
25
29
  * Install: `npx tailjng add toggle-segment`
30
+ *
31
+ * ```html
32
+ * <JToggleSegment
33
+ * endpoint="enum/typeAccess"
34
+ * dataPath="typeAccess.options"
35
+ * optionLabel="label"
36
+ * optionValue="value"
37
+ * optionLabelTemplate="Acceso {label} (id {value})"
38
+ * [loadOnInit]="true"
39
+ * [(ngModel)]="selected"
40
+ * />
41
+ * ```
26
42
  */
27
43
  @Component({
28
44
  selector: 'JToggleSegment',
@@ -42,8 +58,32 @@ export class JToggleSegmentComponent implements OnInit, OnChanges, ControlValueA
42
58
 
43
59
  @Input() endpoint = '';
44
60
  @Input() options: unknown[] = [];
45
- @Input() optionLabel = 'label';
61
+ @Input() optionLabel: string | string[] = 'label';
46
62
  @Input() optionValue = 'value';
63
+
64
+ /** Separator when `optionLabel` is an array of keys. */
65
+ @Input() labelSeparator = ' ';
66
+
67
+ /**
68
+ * Custom label template per option. Use `{field}` placeholders with dot paths.
69
+ * Example: `Acceso {label} (id {value})`.
70
+ */
71
+ @Input() optionLabelTemplate = '';
72
+
73
+ /**
74
+ * Optional formatter for full control over the displayed label.
75
+ * Takes precedence over `optionLabelTemplate` when provided.
76
+ */
77
+ @Input() optionLabelFn?: (option: Record<string, unknown>) => string;
78
+
79
+ /** @deprecated Alias of `dataPath` for a single top-level key in `response.data`. */
80
+ @Input() responseKey = '';
81
+
82
+ /**
83
+ * Dot path from `response.data` to the options array.
84
+ * Examples: `users`, `typeAccess.options`, `catalog.items`.
85
+ */
86
+ @Input() dataPath = '';
47
87
  @Input() sort: ToggleSortOrder = 'ASC';
48
88
  @Input() defaultFilters: Record<string, unknown> = {};
49
89
  @Input() loadOnInit = false;
@@ -95,7 +135,16 @@ export class JToggleSegmentComponent implements OnInit, OnChanges, ControlValueA
95
135
  }
96
136
 
97
137
  ngOnChanges(changes: SimpleChanges): void {
98
- if (changes['options'] && !this.endpoint) {
138
+ const optionInputs = [
139
+ 'options',
140
+ 'optionLabel',
141
+ 'optionValue',
142
+ 'labelSeparator',
143
+ 'optionLabelTemplate',
144
+ 'optionLabelFn',
145
+ ];
146
+
147
+ if (optionInputs.some((key) => changes[key]) && !this.endpoint) {
99
148
  this.processOptions();
100
149
  }
101
150
  }
@@ -115,7 +164,12 @@ export class JToggleSegmentComponent implements OnInit, OnChanges, ControlValueA
115
164
 
116
165
  this.genericService.findAll<unknown>({ endpoint: this.endpoint, params }).subscribe({
117
166
  next: (res) => {
118
- const data = (res.data as Record<string, unknown[]>)[this.endpoint] ?? [];
167
+ const data = extractToggleOptionsFromResponse(
168
+ res.data as Record<string, unknown>,
169
+ this.endpoint,
170
+ this.responseKey || undefined,
171
+ this.dataPath || undefined,
172
+ );
119
173
  this.options = data;
120
174
  this.processOptions();
121
175
  this.isLoading = false;
@@ -131,7 +185,14 @@ export class JToggleSegmentComponent implements OnInit, OnChanges, ControlValueA
131
185
  }
132
186
 
133
187
  processOptions(): void {
134
- this.internalOptions = normalizeToggleOptions(this.options, this.optionLabel, this.optionValue);
188
+ this.internalOptions = normalizeToggleOptions(
189
+ this.options,
190
+ this.optionLabel,
191
+ this.optionValue,
192
+ this.labelSeparator,
193
+ this.optionLabelFn ? undefined : this.optionLabelTemplate || undefined,
194
+ this.optionLabelFn,
195
+ );
135
196
  }
136
197
 
137
198
  select(value: unknown): void {