This commit is contained in:
Lorenz Hilpert
2025-07-28 19:11:36 +02:00
8 changed files with 165 additions and 37 deletions

View File

@@ -2,7 +2,6 @@ import {
patchState,
signalStore,
withComputed,
withHooks,
withMethods,
withProps,
withState,
@@ -99,9 +98,12 @@ export const RemissionStore = signalStore(
* @see {@link https://angular.dev/guide/signals/resource} Angular Resource API documentation
*/
_receiptResource: resource({
loader: async ({ abortSignal }) => {
const receiptId = store.receiptId();
const returnId = store.returnId();
params: () => ({
returnId: store.returnId(),
receiptId: store.receiptId(),
}),
loader: async ({ params, abortSignal }) => {
const { receiptId, returnId } = params;
if (!receiptId || !returnId) {
return undefined;

View File

@@ -53,7 +53,7 @@ export class MyComponent {
The library supports several filter input types:
- **Text Filters** (`InputType.Text`): For search queries and text-based filtering
- **Checkbox Filters** (`InputType.Checkbox`): For multi-select options
- **Checkbox Filters** (`InputType.Checkbox`): For multi-select options with optional selection limits
- **Date Range Filters** (`InputType.DateRange`): For time-based filtering
### Query Settings
@@ -73,9 +73,12 @@ const settings: QuerySettings = {
label: 'Category',
type: InputType.Checkbox,
options: {
max: 3, // Optional: Limit selection to 3 items
values: [
{ label: 'Electronics', value: 'electronics' },
{ label: 'Clothing', value: 'clothing' },
{ label: 'Books', value: 'books' },
{ label: 'Sports', value: 'sports' },
],
},
},
@@ -114,7 +117,7 @@ The `OrderByToolbarComponent` allows users to sort data based on different crite
The library includes specialized components for different input types:
- `CheckboxInputComponent`: For multi-select options
- `CheckboxInputComponent`: For multi-select options with optional selection limits
- `DatepickerRangeInputComponent`: For date range selection
- `SearchBarInputComponent`: For text-based search
@@ -146,9 +149,11 @@ const settings: QuerySettings = {
label: 'Status',
type: InputType.Checkbox,
options: {
max: 2, // Allow up to 2 status selections
values: [
{ label: 'Active', value: 'active' },
{ label: 'Inactive', value: 'inactive' },
{ label: 'Pending', value: 'pending' },
],
},
},
@@ -193,6 +198,52 @@ export class ProductListComponent {
onApplyFilters() {
// Trigger data refresh or update
this.filterService.commit();
}
}
```
### Maximum Options for Checkbox Filters
Checkbox filters support an optional `max` property to limit the number of selections users can make:
```typescript
// Configuration with maximum options
{
key: 'tags',
label: 'Tags',
type: InputType.Checkbox,
options: {
max: 5, // Users can select up to 5 tags
values: [
{ label: 'JavaScript', value: 'js' },
{ label: 'TypeScript', value: 'ts' },
{ label: 'Angular', value: 'angular' },
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Node.js', value: 'node' },
{ label: 'Python', value: 'python' },
],
},
}
```
**Behavior with Maximum Options:**
- **FIFO Strategy**: When the limit is exceeded, the oldest selections are automatically removed
- **UI Enhancement**: The "Select All" control is hidden when `max` is configured to prevent user confusion
- **Automatic Enforcement**: The filter service handles limit enforcement transparently
**Example with FIFO Behavior:**
```typescript
// Starting state: [] (empty)
// User selects: ['js']
// User selects: ['js', 'ts']
// User selects: ['js', 'ts', 'angular']
// User selects: ['js', 'ts', 'angular', 'react']
// User selects: ['js', 'ts', 'angular', 'react', 'vue']
// User selects 'node' -> ['ts', 'angular', 'react', 'vue', 'node'] (oldest 'js' removed)
```
}
}
```
@@ -228,7 +279,7 @@ The Filter library is organized into several key modules:
### Inputs Module
- **CheckboxInput**: Multi-select options component
- **CheckboxInput**: Multi-select options component with configurable selection limits
- **DatepickerRangeInput**: Date range selection component
- **SearchBarInput**: Text-based search component
- **InputRenderer**: Dynamic component renderer

View File

@@ -113,10 +113,29 @@ export class FilterService {
/**
* Sets the selected values for a checkbox input with the specified key.
*
* This method directly sets the selected values for a checkbox input. Unlike
* `setInputCheckboxOptionSelected`, this method does not handle hierarchical
* parent-child relationships or enforce maximum options limits. It directly
* replaces the entire selection array with the provided values.
*
* @param key - The key of the checkbox input to update
* @param selected - Array of selected values
* @param selected - Array of selected values to set (replaces current selection entirely)
* @param options - Optional parameters
* @param options.commit - If true, commits the changes immediately
*
* @example
* ```typescript
* // Set specific selected values
* filterService.setInputCheckboxValue('categories', ['electronics', 'books'], { commit: true });
*
* // Clear all selections
* filterService.setInputCheckboxValue('categories', []);
* ```
*
* @remarks
* This method bypasses the maximum options enforcement and hierarchical logic.
* Use `setInputCheckboxOptionSelected` for individual option management with
* proper limit enforcement and parent-child relationship handling.
*/
setInputCheckboxValue(
key: string,
@@ -147,12 +166,13 @@ export class FilterService {
* Sets the selection state of a specific checkbox option within a hierarchical structure.
*
* This method handles the selection/deselection of checkbox options with automatic
* parent-child relationship management:
* parent-child relationship management and maximum options enforcement:
* - When selecting a parent option, all child options are implicitly selected
* - When deselecting a parent option, all child options are also deselected
* - Child options can be individually selected/deselected
* - If `maxOptions` is configured and selection would exceed the limit, oldest selections are removed (FIFO)
*
* @param path - Array representing the hierarchical path to the option [group, groupKey, ...optionKeys]
* @param checkboxOption - The checkbox option object containing the hierarchical path and metadata
* @param selected - Whether to select (true) or deselect (false) the option
* @param options - Optional parameters
* @param options.commit - If true, commits the changes immediately
@@ -161,17 +181,26 @@ export class FilterService {
* ```typescript
* // Select a specific option
* filterService.setInputCheckboxOptionSelected(
* ['category', 'products', 'electronics', 'phones'],
* checkboxOption,
* true
* );
*
* // Deselect a parent option (also deselects all children)
* filterService.setInputCheckboxOptionSelected(
* ['category', 'products', 'electronics'],
* parentCheckboxOption,
* false,
* { commit: true }
* );
*
* // When maxOptions is set to 3 and selecting would exceed the limit:
* // If currently selected: ['option1', 'option2', 'option3']
* // Selecting 'option4' results in: ['option2', 'option3', 'option4']
* // (oldest selection 'option1' is automatically removed)
* ```
*
* @remarks
* The maximum options enforcement only applies when selecting options. Deselection is not affected by the limit.
* The FIFO (First In, First Out) strategy ensures the most recently selected options are preserved when the limit is exceeded.
*/
setInputCheckboxOptionSelected(
checkboxOption: CheckboxFilterInputOption,
@@ -210,7 +239,14 @@ export class FilterService {
}
if (selected) {
const newSelected = [...input.selected, ...keys];
const maxOptions = input.maxOptions;
let newSelected = [...input.selected, ...keys];
if (maxOptions && newSelected.length > maxOptions) {
const excessCount = newSelected.length - maxOptions;
newSelected = newSelected.slice(excessCount);
}
return {
...input,

View File

@@ -6,15 +6,16 @@ import { checkboxOptionMapping } from './checkbox-option.mapping';
* Maps an Input object to a CheckboxFilterInput object with support for hierarchical options.
*
* This function transforms a generic Input object into a strongly-typed CheckboxFilterInput,
* handling nested checkbox options and tracking selection states. The mapping process:
* handling nested checkbox options, selection limits, and tracking selection states. The mapping process:
* - Validates the input against the CheckboxFilterInputSchema
* - Maps the maximum options limit from `input.options?.max` to `maxOptions` property
* - Recursively maps nested options with proper path tracking
* - Extracts selected values from the option tree
*
* @param group - The group identifier that this input belongs to
* @param input - The source input object containing checkbox configuration
* @returns A validated CheckboxFilterInput object with hierarchical options
*
* @returns A validated CheckboxFilterInput object with hierarchical options and optional selection limits
*
* @example
* ```typescript
* const checkboxInput = checkboxFilterInputMapping('filters', {
@@ -22,6 +23,7 @@ import { checkboxOptionMapping } from './checkbox-option.mapping';
* label: 'Product Category',
* type: 'checkbox',
* options: {
* max: 3, // Maps to maxOptions: 3
* values: [
* { value: 'electronics', label: 'Electronics', selected: true },
* { value: 'clothing', label: 'Clothing' }

View File

@@ -8,9 +8,25 @@ import { CheckboxFilterInputOptionSchema } from './checkbox-filter-input-option.
* Extends the BaseFilterInputSchema with checkbox-specific properties.
*
* @property type - Must be InputType.Checkbox
* @property maxOptions - Optional limit on how many options can be selected
* @property maxOptions - Optional limit on how many options can be selected simultaneously.
* When set, the filter service enforces this limit using a FIFO (First In, First Out) strategy,
* automatically removing the oldest selections when new selections would exceed the limit.
* When configured, the "Select All" control is hidden to prevent user confusion.
* @property options - Array of selectable checkbox options
* @property selected - Array of string values representing the currently selected options
*
* @example
* ```typescript
* // Checkbox input with maximum of 3 selections
* const checkboxInput: CheckboxFilterInput = {
* type: InputType.Checkbox,
* key: 'categories',
* label: 'Categories',
* maxOptions: 3, // Users can select up to 3 categories
* options: [...],
* selected: ['electronics', 'books']
* };
* ```
*/
export const CheckboxFilterInputSchema = BaseFilterInputSchema.extend({
type: z
@@ -22,7 +38,9 @@ export const CheckboxFilterInputSchema = BaseFilterInputSchema.extend({
.number()
.optional()
.describe(
'Optional maximum number of options that can be selected simultaneously. If not provided, all options can be selected.',
'Optional maximum number of options that can be selected simultaneously. ' +
'When exceeded, the oldest selections are automatically removed (FIFO strategy). ' +
'Setting this value also hides the "Select All" control to prevent user confusion.',
),
options: z
.array(CheckboxFilterInputOptionSchema)

View File

@@ -24,18 +24,20 @@
}
}
</div>
<label
class="w-full flex items-center gap-4 cursor-pointer isa-text-body-2-bold"
>
<ui-checkbox>
<input
(click)="toggleSelection()"
[checked]="allChecked()"
type="checkbox"
/>
</ui-checkbox>
<span> Alle aus/abwählen</span>
</label>
@if (!hasMaxOptions()) {
<label
class="w-full flex items-center gap-4 cursor-pointer isa-text-body-2-bold"
>
<ui-checkbox>
<input
(click)="toggleSelection()"
[checked]="allChecked()"
type="checkbox"
/>
</ui-checkbox>
<span> Alle aus/abwählen</span>
</label>
}
@for (option of options(); track option.key; let i = $index) {
<filter-checkbox-input-control

View File

@@ -54,6 +54,29 @@ export class CheckboxInputComponent {
) as CheckboxFilterInput;
});
/**
* Computed property that determines if the checkbox input has a maximum options limit configured.
*
* This property is used to conditionally show/hide the "Select All" control in the template.
* When a maximum limit is set, the "Select All" functionality is disabled to prevent
* users from selecting more items than allowed, which would result in automatic deselection
* due to the FIFO enforcement in the filter service.
*
* @returns True if the input has a maximum options limit greater than 0, false otherwise
*
* @example
* ```typescript
* // In template:
* // @if (!hasMaxOptions()) {
* // <label>Select All</label>
* // }
* ```
*/
hasMaxOptions = computed(() => {
const input = this.input();
return input.maxOptions && input.maxOptions > 0;
});
options = computed(() => {
const input = this.input();
const options = input?.options || [];