Skip to content

Commit 7274ffd

Browse files
committed
docs: provide advanced filter example
1 parent b34955c commit 7274ffd

File tree

7 files changed

+592
-0
lines changed

7 files changed

+592
-0
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<div class="has-navbar-fixed-top si-layout-fixed-height h-100">
2+
<si-application-header>
3+
<si-header-brand>
4+
<a siHeaderLogo routerLink="/" class="d-none d-md-flex"></a>
5+
<span class="application-name">Application name</span>
6+
</si-header-brand>
7+
</si-application-header>
8+
<si-side-panel mode="scroll" [collapsed]="!showFilters()" (collapsedChange)="toggleFilter()">
9+
<div class="si-layout-fixed-height mb-6 si-layout-main-padding" siResponsiveContainer>
10+
<div class="si-layout-header">
11+
<h2 class="si-layout-title si-layout-top-element">
12+
Title describing what page content to expect
13+
</h2>
14+
<p class="si-layout-subtitle">
15+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
16+
</p>
17+
</div>
18+
<div class="d-flex flex-column gap-4">
19+
<div class="d-flex gap-6">
20+
<si-search-bar class="flex-grow-1" placeholder="Search..." [showIcon]="true" />
21+
<button
22+
type="button"
23+
class="btn btn-secondary"
24+
aria-label="Filters"
25+
(click)="toggleFilter()"
26+
>
27+
<si-icon class="icon" [icon]="icons.elementFilter" />
28+
Filters
29+
</button>
30+
</div>
31+
@if (filters().length > 0) {
32+
<si-filter-bar [filters]="filters()" (filtersChange)="deleteFilter($event)" />
33+
}
34+
</div>
35+
<div class="si-layout-fixed-height align-items-stretch justify-content-center">
36+
<div class="d-flex align-items-center justify-content-center">Content</div>
37+
</div>
38+
</div>
39+
<si-side-panel-content heading="Select filters">
40+
<app-filter-side-panel [showFilters]="showFilters()" (showFiltersChange)="toggleFilter()" />
41+
</si-side-panel-content>
42+
</si-side-panel>
43+
</div>
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* Copyright (c) Siemens 2016 - 2025
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
6+
import {
7+
SiApplicationHeaderComponent,
8+
SiHeaderBrandDirective
9+
} from '@siemens/element-ng/application-header';
10+
import { Filter, SiFilterBarComponent } from '@siemens/element-ng/filter-bar';
11+
import { addIcons, SiIconComponent } from '@siemens/element-ng/icon';
12+
import { SiResponsiveContainerDirective } from '@siemens/element-ng/resize-observer';
13+
import { SiSearchBarComponent } from '@siemens/element-ng/search-bar';
14+
import { SiSidePanelComponent, SiSidePanelContentComponent } from '@siemens/element-ng/side-panel';
15+
import { LOG_EVENT } from '@siemens/live-preview';
16+
import { elementFilter } from '@simpl/element-icons/ionic';
17+
18+
import { BackendService } from './backend.service';
19+
import { FilterSidePanelComponent } from './components/filter-side-panel';
20+
import { FilterModel } from './filter.model';
21+
22+
@Component({
23+
selector: 'app-sample',
24+
imports: [
25+
FilterSidePanelComponent,
26+
SiApplicationHeaderComponent,
27+
SiFilterBarComponent,
28+
SiHeaderBrandDirective,
29+
SiIconComponent,
30+
SiResponsiveContainerDirective,
31+
SiSearchBarComponent,
32+
SiSidePanelComponent,
33+
SiSidePanelContentComponent
34+
],
35+
templateUrl: './advanced-filter-example.html',
36+
changeDetection: ChangeDetectionStrategy.OnPush
37+
})
38+
export class SampleComponent {
39+
protected readonly logEvent = inject(LOG_EVENT);
40+
protected readonly service = inject(BackendService);
41+
protected readonly showFilters = signal(false);
42+
protected readonly icons = addIcons({ elementFilter });
43+
/** Synchronize filters between main view and side panel */
44+
protected readonly filters = computed<Filter[]>(() => {
45+
const model = this.service.filter();
46+
const filters: Filter[] = [];
47+
if (model.states.length) {
48+
filters.push({
49+
filterName: 'states',
50+
title: 'Status',
51+
description: this.filterText(
52+
'states',
53+
model.states.map(s => s.title)
54+
)
55+
});
56+
}
57+
if (model.versions.length) {
58+
filters.push({
59+
filterName: 'versions',
60+
title: 'Version',
61+
description: this.filterText('versions', model.versions)
62+
});
63+
}
64+
return filters;
65+
});
66+
67+
protected toggleFilter(): void {
68+
this.showFilters.update(v => !v);
69+
}
70+
71+
protected filterText(suffix: string, items: string[]): string {
72+
return items.length === 1 ? items[0] : `${items.length} ${suffix}`;
73+
}
74+
75+
protected deleteFilter(filter: Filter[]): void {
76+
this.service.filter.update(model => {
77+
for (const key of Object.keys(model)) {
78+
const k = key as keyof FilterModel;
79+
if (!filter.find(f => f.filterName === key)) {
80+
if (Array.isArray(model[k])) {
81+
model[k] = [];
82+
}
83+
}
84+
}
85+
return { ...model };
86+
});
87+
}
88+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Copyright (c) Siemens 2016 - 2025
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
import { inject, Injectable, LOCALE_ID, signal } from '@angular/core';
6+
import { delay, Observable, of } from 'rxjs';
7+
8+
import { countryList, statusList, versionList } from './data-utils';
9+
import { Country, FilterModel, Result, Status, Version } from './filter.model';
10+
11+
@Injectable({
12+
providedIn: 'root'
13+
})
14+
export class BackendService {
15+
private readonly locale = inject(LOCALE_ID);
16+
17+
countries(): Observable<Country[]> {
18+
return of(countryList(this.locale)).pipe(delay(1000));
19+
}
20+
21+
states(): Observable<Status[]> {
22+
return of(statusList()).pipe(delay(500));
23+
}
24+
25+
versions(count: number): Observable<Result<Version>> {
26+
const list = versionList();
27+
return of({
28+
items: list.splice(0, count),
29+
complete: count >= list.length,
30+
count: list.length
31+
}).pipe(delay(2000));
32+
}
33+
34+
/** Current filter state */
35+
readonly filter = signal<FilterModel>({ states: [], versions: [], countries: [] });
36+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/**
2+
* Copyright (c) Siemens 2016 - 2025
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
import {
6+
ChangeDetectionStrategy,
7+
Component,
8+
effect,
9+
inject,
10+
model,
11+
signal,
12+
untracked,
13+
viewChildren
14+
} from '@angular/core';
15+
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
16+
import { TreeItem } from '@siemens/element-ng/tree-view';
17+
import { BehaviorSubject, switchMap } from 'rxjs';
18+
19+
import { BackendService } from '../backend.service';
20+
import { FilterModel } from '../filter.model';
21+
import { TreeFilterComponent, TreeFilterSelection } from './tree-filter.component';
22+
23+
const toTreeItem = (label: string, customData: any): TreeItem => ({
24+
label,
25+
selectable: true,
26+
state: 'leaf',
27+
checked: 'unchecked',
28+
customData
29+
});
30+
31+
@Component({
32+
selector: 'app-filter-side-panel',
33+
imports: [TreeFilterComponent],
34+
template: `
35+
<div class="d-flex flex-column h-100">
36+
<div class="flex-grow-1">
37+
<app-tree-filter
38+
name="states"
39+
label="Status"
40+
[items]="stateItems()"
41+
(selectionChange)="treeModelChange('states', $event)"
42+
/>
43+
<app-tree-filter
44+
name="versions"
45+
label="Version"
46+
[filter]="filterVersion"
47+
[items]="versionItems()"
48+
[loadMore]="versionsComplete()"
49+
[loading]="versionsPending()"
50+
(selectionChange)="treeModelChange('versions', $event)"
51+
(loadMoreItems)="loadMoreVersions()"
52+
/>
53+
</div>
54+
<div class="d-flex gap-6 ms-auto p-6">
55+
<button type="button" class="btn btn-secondary" (click)="cancelFilters()">Cancel</button>
56+
<button type="button" class="btn btn-primary" (click)="applyFilters()">Apply</button>
57+
</div>
58+
</div>
59+
`,
60+
changeDetection: ChangeDetectionStrategy.OnPush
61+
})
62+
export class FilterSidePanelComponent {
63+
protected service = inject(BackendService);
64+
protected readonly filterComponents = viewChildren(TreeFilterComponent);
65+
66+
readonly showFilters = model(true);
67+
68+
protected readonly statesResult = toSignal(this.service.states());
69+
protected readonly stateItems = signal<TreeItem[] | undefined>(undefined);
70+
71+
protected readonly versionsRequested$ = new BehaviorSubject(5);
72+
protected readonly versionsComplete = signal(false);
73+
protected readonly versionsPending = signal(true);
74+
protected readonly versionResult = toSignal(
75+
this.versionsRequested$.pipe(
76+
takeUntilDestroyed(),
77+
switchMap(count => this.service.versions(count))
78+
)
79+
);
80+
protected readonly versionItems = signal<TreeItem[] | undefined>(undefined);
81+
82+
protected filterVersion(item: TreeItem, searchString: string): boolean {
83+
return item.label?.toLocaleLowerCase().includes(searchString.toLowerCase()) ?? false;
84+
}
85+
86+
readonly filter = signal<FilterModel>({ states: [], versions: [], countries: [] });
87+
88+
constructor() {
89+
effect(() => {
90+
const result = this.versionResult();
91+
if (result) {
92+
this.versionsComplete.set(!result?.complete);
93+
this.versionItems.set(result.items.map(i => toTreeItem(i, i)));
94+
this.versionsPending.set(false);
95+
}
96+
});
97+
effect(() => {
98+
const result = this.statesResult();
99+
if (result) {
100+
this.stateItems.set(result.map(item => toTreeItem(item.title, item)));
101+
}
102+
});
103+
effect(() => {
104+
const filterModel = this.service.filter();
105+
untracked(() => {
106+
for (const [key, value] of Object.entries(filterModel)) {
107+
if (Array.isArray(value) && value.length === 0) {
108+
this.filterComponents()
109+
.find(c => c.name() === key)
110+
?.reset();
111+
}
112+
}
113+
});
114+
});
115+
}
116+
117+
protected treeModelChange(name: string, event: TreeFilterSelection): void {
118+
this.filter.update(f => {
119+
return { ...f, [name]: event.selected.map(i => i.customData!) };
120+
});
121+
}
122+
123+
protected loadMoreVersions(): void {
124+
this.versionsPending.set(true);
125+
this.versionsRequested$.next(this.versionsRequested$.value + 5);
126+
}
127+
128+
protected cancelFilters(): void {
129+
this.showFilters.set(false);
130+
}
131+
132+
protected applyFilters(): void {
133+
this.showFilters.set(false);
134+
this.service.filter.set({ ...this.filter() });
135+
}
136+
}

0 commit comments

Comments
 (0)