Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/app/examples/filter-side-panel/advanced-filter-example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<div class="has-navbar-fixed-top si-layout-fixed-height h-100">
<si-application-header>
<si-header-brand>
<a siHeaderLogo routerLink="/" class="d-none d-md-flex"></a>
<span class="application-name">Application name</span>
</si-header-brand>
</si-application-header>
<si-side-panel mode="scroll" [collapsed]="!showFilters()" (collapsedChange)="toggleFilter()">
<div class="si-layout-fixed-height mb-6 si-layout-main-padding" siResponsiveContainer>
<div class="si-layout-header">
<h2 class="si-layout-title si-layout-top-element">
Title describing what page content to expect
</h2>
<p class="si-layout-subtitle">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
</p>
</div>
<div class="d-flex flex-column gap-4">
<div class="d-flex gap-6">
<si-search-bar class="flex-grow-1" placeholder="Search..." [showIcon]="true" />
<button
type="button"
class="btn btn-secondary"
aria-label="Filters"
(click)="toggleFilter()"
>
<si-icon class="icon" [icon]="icons.elementFilter" />
Filters
</button>
</div>
@if (filters().length > 0) {
<si-filter-bar [filters]="filters()" (filtersChange)="deleteFilter($event)" />
}
</div>
<div class="si-layout-fixed-height align-items-stretch justify-content-center">
<div class="d-flex align-items-center justify-content-center">Content</div>
</div>
</div>
<si-side-panel-content heading="Select filters">
<app-filter-side-panel [showFilters]="showFilters()" (showFiltersChange)="toggleFilter()" />
</si-side-panel-content>
</si-side-panel>
</div>
88 changes: 88 additions & 0 deletions src/app/examples/filter-side-panel/advanced-filter-example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* Copyright (c) Siemens 2016 - 2025
* SPDX-License-Identifier: MIT
*/
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
import {
SiApplicationHeaderComponent,
SiHeaderBrandDirective
} from '@siemens/element-ng/application-header';
import { Filter, SiFilterBarComponent } from '@siemens/element-ng/filter-bar';
import { addIcons, SiIconComponent } from '@siemens/element-ng/icon';
import { SiResponsiveContainerDirective } from '@siemens/element-ng/resize-observer';
import { SiSearchBarComponent } from '@siemens/element-ng/search-bar';
import { SiSidePanelComponent, SiSidePanelContentComponent } from '@siemens/element-ng/side-panel';
import { LOG_EVENT } from '@siemens/live-preview';
import { elementFilter } from '@simpl/element-icons/ionic';

import { BackendService } from './backend.service';
import { FilterSidePanelComponent } from './components/filter-side-panel';
import { FilterModel } from './filter.model';

@Component({
selector: 'app-sample',
imports: [
FilterSidePanelComponent,
SiApplicationHeaderComponent,
SiFilterBarComponent,
SiHeaderBrandDirective,
SiIconComponent,
SiResponsiveContainerDirective,
SiSearchBarComponent,
SiSidePanelComponent,
SiSidePanelContentComponent
],
templateUrl: './advanced-filter-example.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SampleComponent {
protected readonly logEvent = inject(LOG_EVENT);
protected readonly service = inject(BackendService);
protected readonly showFilters = signal(false);
protected readonly icons = addIcons({ elementFilter });
/** Synchronize filters between main view and side panel */
protected readonly filters = computed<Filter[]>(() => {
const model = this.service.filter();
const filters: Filter[] = [];
if (model.states.length) {
filters.push({
filterName: 'states',
title: 'Status',
description: this.filterText(
'states',
model.states.map(s => s.title)
)
});
}
if (model.versions.length) {
filters.push({
filterName: 'versions',
title: 'Version',
description: this.filterText('versions', model.versions)
});
}
return filters;
});

protected toggleFilter(): void {
this.showFilters.update(v => !v);
}

protected filterText(suffix: string, items: string[]): string {
return items.length === 1 ? items[0] : `${items.length} ${suffix}`;
}

protected deleteFilter(filter: Filter[]): void {
this.service.filter.update(model => {
for (const key of Object.keys(model)) {
const k = key as keyof FilterModel;
if (!filter.find(f => f.filterName === key)) {
if (Array.isArray(model[k])) {
model[k] = [];
}
}
}
return { ...model };
});
}
}
36 changes: 36 additions & 0 deletions src/app/examples/filter-side-panel/backend.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Copyright (c) Siemens 2016 - 2025
* SPDX-License-Identifier: MIT
*/
import { inject, Injectable, LOCALE_ID, signal } from '@angular/core';
import { delay, Observable, of } from 'rxjs';

import { countryList, statusList, versionList } from './data-utils';
import { Country, FilterModel, Result, Status, Version } from './filter.model';

@Injectable({
providedIn: 'root'
})
export class BackendService {
private readonly locale = inject(LOCALE_ID);

countries(): Observable<Country[]> {
return of(countryList(this.locale)).pipe(delay(1000));
}

states(): Observable<Status[]> {
return of(statusList()).pipe(delay(500));
}

versions(count: number): Observable<Result<Version>> {
const list = versionList();
return of({
items: list.splice(0, count),
complete: count >= list.length,
count: list.length
}).pipe(delay(2000));
}

/** Current filter state */
readonly filter = signal<FilterModel>({ states: [], versions: [], countries: [] });
}
136 changes: 136 additions & 0 deletions src/app/examples/filter-side-panel/components/filter-side-panel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/**
* Copyright (c) Siemens 2016 - 2025
* SPDX-License-Identifier: MIT
*/
import {
ChangeDetectionStrategy,
Component,
effect,
inject,
model,
signal,
untracked,
viewChildren
} from '@angular/core';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { TreeItem } from '@siemens/element-ng/tree-view';
import { BehaviorSubject, switchMap } from 'rxjs';

import { BackendService } from '../backend.service';
import { FilterModel } from '../filter.model';
import { TreeFilterComponent, TreeFilterSelection } from './tree-filter.component';

const toTreeItem = (label: string, customData: any): TreeItem => ({
label,
selectable: true,
state: 'leaf',
checked: 'unchecked',
customData
});

@Component({
selector: 'app-filter-side-panel',
imports: [TreeFilterComponent],
template: `
<div class="d-flex flex-column h-100">
<div class="flex-grow-1">
<app-tree-filter
name="states"
label="Status"
[items]="stateItems()"
(selectionChange)="treeModelChange('states', $event)"
/>
<app-tree-filter
name="versions"
label="Version"
[filter]="filterVersion"
[items]="versionItems()"
[loadMore]="versionsComplete()"
[loading]="versionsPending()"
(selectionChange)="treeModelChange('versions', $event)"
(loadMoreItems)="loadMoreVersions()"
/>
</div>
<div class="d-flex gap-6 ms-auto p-6">
<button type="button" class="btn btn-secondary" (click)="cancelFilters()">Cancel</button>
<button type="button" class="btn btn-primary" (click)="applyFilters()">Apply</button>
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FilterSidePanelComponent {
protected service = inject(BackendService);
protected readonly filterComponents = viewChildren(TreeFilterComponent);

readonly showFilters = model(true);

protected readonly statesResult = toSignal(this.service.states());
protected readonly stateItems = signal<TreeItem[] | undefined>(undefined);

protected readonly versionsRequested$ = new BehaviorSubject(5);
protected readonly versionsComplete = signal(false);
protected readonly versionsPending = signal(true);
protected readonly versionResult = toSignal(
this.versionsRequested$.pipe(
takeUntilDestroyed(),
switchMap(count => this.service.versions(count))
)
);
protected readonly versionItems = signal<TreeItem[] | undefined>(undefined);

protected filterVersion(item: TreeItem, searchString: string): boolean {
return item.label?.toLocaleLowerCase().includes(searchString.toLowerCase()) ?? false;
}

readonly filter = signal<FilterModel>({ states: [], versions: [], countries: [] });

constructor() {
effect(() => {
const result = this.versionResult();
if (result) {
this.versionsComplete.set(!result?.complete);
this.versionItems.set(result.items.map(i => toTreeItem(i, i)));
this.versionsPending.set(false);
}
});
effect(() => {
const result = this.statesResult();
if (result) {
this.stateItems.set(result.map(item => toTreeItem(item.title, item)));
}
});
effect(() => {
const filterModel = this.service.filter();
untracked(() => {
for (const [key, value] of Object.entries(filterModel)) {
if (Array.isArray(value) && value.length === 0) {
this.filterComponents()
.find(c => c.name() === key)
?.reset();
}
}
});
});
}

protected treeModelChange(name: string, event: TreeFilterSelection): void {
this.filter.update(f => {
return { ...f, [name]: event.selected.map(i => i.customData!) };
});
}

protected loadMoreVersions(): void {
this.versionsPending.set(true);
this.versionsRequested$.next(this.versionsRequested$.value + 5);
}

protected cancelFilters(): void {
this.showFilters.set(false);
}

protected applyFilters(): void {
this.showFilters.set(false);
this.service.filter.set({ ...this.filter() });
}
}
Loading
Loading