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 +1 -1
- package/src/lib/components/filter/filter-complete/complete-filter.util.ts +1 -1
- package/src/lib/components/toggle-radio/shared/toggle-options.types.ts +1 -0
- package/src/lib/components/toggle-radio/shared/toggle-options.util.ts +113 -14
- package/src/lib/components/toggle-radio/toggle-radio/toggle-radio.component.ts +66 -5
- package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.component.ts +66 -5
package/package.json
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
label
|
|
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 {
|
|
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
|
-
|
|
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 = (
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
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 = (
|
|
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(
|
|
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 {
|