song list filter

This commit is contained in:
2020-04-24 15:07:35 +02:00
committed by smuddy
parent fdc1b299fa
commit 888d1e74b1
18 changed files with 229 additions and 23 deletions

View File

@@ -0,0 +1,6 @@
export interface FilterValues {
q: string;
type: string;
legalType: string;
flag: string;
}

View File

@@ -0,0 +1,35 @@
<div [formGroup]="filterFormGroup">
<mat-form-field appearance="outline">
<mat-label>Titel oder Text</mat-label>
<input formControlName="q" matInput>
</mat-form-field>
<div class="third">
<mat-form-field appearance="outline">
<mat-label>Typ</mat-label>
<mat-select formControlName="type">
<mat-option [value]="null">- kein Filter -</mat-option>
<mat-option *ngFor="let type of types" [value]="type">{{type | songType}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Rechtlicher Status</mat-label>
<mat-select formControlName="legalType">
<mat-option [value]="null">- kein Filter -</mat-option>
<mat-option *ngFor="let key of legalType" [value]="key">{{key|legalType}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Attribute</mat-label>
<mat-select formControlName="flag">
<mat-option [value]="null">- kein Filter -</mat-option>
<mat-option *ngFor="let flag of getFlags()" [value]="flag">{{flag}}</mat-option>
</mat-select>
</mat-form-field>
</div>
<i>Anzahl der Suchergebnisse: {{songs.length}}</i>
</div>

View File

@@ -0,0 +1,5 @@
.third {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
column-gap: 20px;
}

View File

@@ -0,0 +1,25 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {FilterComponent} from './filter.component';
describe('FilterComponent', () => {
let component: FilterComponent;
let fixture: ComponentFixture<FilterComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [FilterComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FilterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,63 @@
import {Component, Input, OnInit} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {FormBuilder, FormGroup} from '@angular/forms';
import {SongService} from '../../services/song.service';
import {FilterValues} from './filter-values';
import {Song} from '../../services/song';
@Component({
selector: 'app-filter',
templateUrl: './filter.component.html',
styleUrls: ['./filter.component.less']
})
export class FilterComponent implements OnInit {
public filterFormGroup: FormGroup;
@Input() route: string;
@Input() songs: Song[];
public types = SongService.TYPES;
public legalType = SongService.LEGAL_TYPE;
constructor(private router: Router, activatedRoute: ActivatedRoute, fb: FormBuilder) {
this.filterFormGroup = fb.group({
q: '',
type: '',
legalType: '',
flag: '',
});
activatedRoute.queryParams.subscribe((filterValues: FilterValues) => {
if (filterValues.q) this.filterFormGroup.controls.q.setValue(filterValues.q);
if (filterValues.type) this.filterFormGroup.controls.type.setValue(filterValues.type);
if (filterValues.legalType) this.filterFormGroup.controls.legalType.setValue(filterValues.legalType);
if (filterValues.flag) this.filterFormGroup.controls.flag.setValue(filterValues.flag);
})
this.filterFormGroup.controls.q.valueChanges.subscribe(_ => this.filerValueChanged('q', _));
this.filterFormGroup.controls.type.valueChanges.subscribe(_ => this.filerValueChanged('type', _));
this.filterFormGroup.controls.legalType.valueChanges.subscribe(_ => this.filerValueChanged('legalType', _));
this.filterFormGroup.controls.flag.valueChanges.subscribe(_ => this.filerValueChanged('flag', _));
}
ngOnInit(): void {
}
public getFlags(): string[] {
const flags = this.songs
.map(_ => _.flags)
.filter(_ => !!_)
.map(_ => _.split(';'))
.reduce((pn, u) => [...pn, ...u], [])
.filter(_ => !!_);
const uqFlags = flags.filter((n, i) => flags.indexOf(n) === i);
return uqFlags;
}
private async filerValueChanged(key: string, value: string): Promise<void> {
const route = this.router.createUrlTree([this.route], {queryParams: {[key]: value}, queryParamsHandling: 'merge'});
await this.router.navigateByUrl(route);
}
}

View File

@@ -1,6 +1,8 @@
<div class="list-item"> <div class="list-item">
<div class="number">{{song.number}}</div> <div class="number">{{song.number}}</div>
<div>{{song.title}}</div> <div>
<span *ngIf="song.legalType==='open'" class="warning"><fa-icon [icon]="faLegal"></fa-icon> &nbsp;</span>
{{song.title}}</div>
<div>{{song.key}}</div> <div>{{song.key}}</div>
<div>{{song.legalType | legalType}}</div>
</div> </div>

View File

@@ -3,7 +3,7 @@
.list-item { .list-item {
padding: 5px 20px; padding: 5px 20px;
display: grid; display: grid;
grid-template-columns: 50px auto 30px 100px; grid-template-columns: 50px auto 30px;
& > div { & > div {
display: flex; display: flex;
@@ -15,6 +15,10 @@
&:hover { &:hover {
background: @primary-color; background: @primary-color;
color: #fff; color: #fff;
.warning {
color: #fff;
}
} }
} }
@@ -23,3 +27,7 @@
font-weight: bold; font-weight: bold;
text-align: right; text-align: right;
} }
.warning {
color: #ba3500;
}

View File

@@ -1,5 +1,6 @@
import {Component, Input, OnInit} from '@angular/core'; import {Component, Input, OnInit} from '@angular/core';
import {Song} from '../../services/song'; import {Song} from '../../services/song';
import {faBalanceScaleRight} from '@fortawesome/free-solid-svg-icons/faBalanceScaleRight';
@Component({ @Component({
selector: 'app-list-item', selector: 'app-list-item',
@@ -8,6 +9,7 @@ import {Song} from '../../services/song';
}) })
export class ListItemComponent implements OnInit { export class ListItemComponent implements OnInit {
@Input() public song: Song; @Input() public song: Song;
public faLegal = faBalanceScaleRight;
constructor() { constructor() {
} }

View File

@@ -1,7 +1,9 @@
<div> <div *ngIf="songs$ | async as songs" [@fade]>
<app-list-header></app-list-header> <app-list-header [anyFilterActive]="anyFilterActive">
<app-filter [songs]="songs" route="songs"></app-filter>
</app-list-header>
<app-card *ngIf="songs$ | async as songs" [@fade] [padding]="false"> <app-card [padding]="false">
<app-list-item *ngFor="let song of songs" [routerLink]="song.id" [song]="song"></app-list-item> <app-list-item *ngFor="let song of songs" [routerLink]="song.id" [song]="song"></app-list-item>
</app-card> </app-card>
</div> </div>

View File

@@ -1,2 +0,0 @@
.song-list {
}

View File

@@ -6,6 +6,7 @@ import {combineLatest, Observable} from 'rxjs';
import {fade} from '../../../animations'; import {fade} from '../../../animations';
import {ActivatedRoute} from '@angular/router'; import {ActivatedRoute} from '@angular/router';
import {filterSong} from '../../../services/filter.helper'; import {filterSong} from '../../../services/filter.helper';
import {FilterValues} from './filter/filter-values';
@Component({ @Component({
selector: 'app-songs', selector: 'app-songs',
@@ -16,6 +17,7 @@ import {filterSong} from '../../../services/filter.helper';
export class SongListComponent implements OnInit { export class SongListComponent implements OnInit {
public songs$: Observable<Song[]>; public songs$: Observable<Song[]>;
public anyFilterActive = false;
constructor(private songService: SongService, private activatedRoute: ActivatedRoute) { constructor(private songService: SongService, private activatedRoute: ActivatedRoute) {
} }
@@ -23,7 +25,7 @@ export class SongListComponent implements OnInit {
ngOnInit() { ngOnInit() {
const filter$ = this.activatedRoute.queryParams.pipe( const filter$ = this.activatedRoute.queryParams.pipe(
debounceTime(300), debounceTime(300),
map(_ => _.q) map(_ => _ as FilterValues)
); );
const songs$ = this.songService.list$().pipe( const songs$ = this.songService.list$().pipe(
@@ -31,7 +33,35 @@ export class SongListComponent implements OnInit {
); );
this.songs$ = combineLatest([filter$, songs$]).pipe( this.songs$ = combineLatest([filter$, songs$]).pipe(
map(_ => _[1].filter(song => filterSong(song, _[0]))) map(_ => {
let songs = _[1];
let filter = _[0];
this.anyFilterActive = this.checkIfFilterActive(filter);
return songs.filter(song => this.filter(song, filter));
})
); );
}
private filter(song: Song, filter: FilterValues): boolean {
let baseFilter = filterSong(song, filter.q);
baseFilter = baseFilter && (!filter.type || filter.type === song.type);
baseFilter = baseFilter && (!filter.legalType || filter.legalType === song.legalType);
baseFilter = baseFilter && (!filter.flag || this.checkFlag(filter.flag, song.flags));
return baseFilter;
}
private checkIfFilterActive(filter: FilterValues): boolean {
return !!filter.q || !!filter.type || !!filter.legalType || !!filter.flag;
}
private checkFlag(flag: string, flags: string) {
if (!flags) return false;
const flagStrings = flags.split(';');
if (flagStrings.length === 0) return false;
return flagStrings.indexOf(flag) !== -1;
} }
} }

View File

@@ -6,10 +6,17 @@ import {CardModule} from '../../../widget-modules/components/card/card.module';
import {RouterModule} from '@angular/router'; import {RouterModule} from '@angular/router';
import {LegalTypeTranslatorModule} from '../../../widget-modules/pipes/legal-type-translator/legal-type-translator.module'; import {LegalTypeTranslatorModule} from '../../../widget-modules/pipes/legal-type-translator/legal-type-translator.module';
import {ListHeaderModule} from '../../../widget-modules/components/list-header/list-header.module'; import {ListHeaderModule} from '../../../widget-modules/components/list-header/list-header.module';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatInputModule} from '@angular/material/input';
import {FilterComponent} from './filter/filter.component';
import {ReactiveFormsModule} from '@angular/forms';
import {MatSelectModule} from '@angular/material/select';
import {SongTypeTranslaterModule} from '../../../widget-modules/pipes/song-type-translater/song-type-translater.module';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
@NgModule({ @NgModule({
declarations: [SongListComponent, ListItemComponent], declarations: [SongListComponent, ListItemComponent, FilterComponent],
exports: [SongListComponent], exports: [SongListComponent],
imports: [ imports: [
CommonModule, CommonModule,
@@ -17,7 +24,13 @@ import {ListHeaderModule} from '../../../widget-modules/components/list-header/l
CardModule, CardModule,
LegalTypeTranslatorModule, LegalTypeTranslatorModule,
ListHeaderModule ListHeaderModule,
MatFormFieldModule,
MatInputModule,
ReactiveFormsModule,
MatSelectModule,
SongTypeTranslaterModule,
FontAwesomeModule
] ]
}) })
export class SongListModule { export class SongListModule {

View File

@@ -36,7 +36,7 @@
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-chip-list #chipList aria-label="Fruit selection"> <mat-chip-list #chipList>
<mat-chip (removed)="removeFlag(flag)" *ngFor="let flag of flags" <mat-chip (removed)="removeFlag(flag)" *ngFor="let flag of flags"
[removable]="true" [selectable]="false"> [removable]="true" [selectable]="false">
{{flag}}&nbsp; {{flag}}&nbsp;

View File

@@ -16,10 +16,7 @@ export class FilterComponent {
} }
public async valueChange(text: string): Promise<void> { public async valueChange(text: string): Promise<void> {
const route = text const route = this.router.createUrlTree(['songs'], {queryParams: {q: text}, queryParamsHandling: 'merge'});
? this.router.createUrlTree(['songs'], {queryParams: {q: text}})
: this.router.createUrlTree(['songs']);
await this.router.navigateByUrl(route); await this.router.navigateByUrl(route);
} }
} }

View File

@@ -1,8 +1,15 @@
<div class="header"> <div class="header">
<button mat-icon-button>
<button (click)="onFilterClick()" [class.filter-active]="anyFilterActive" mat-icon-button>
<fa-icon [icon]="faFilter"></fa-icon> <fa-icon [icon]="faFilter"></fa-icon>
</button> </button>
<button mat-icon-button routerLink="new"> <button mat-icon-button routerLink="new">
<fa-icon [icon]="faNew"></fa-icon> <fa-icon [icon]="faNew"></fa-icon>
</button> </button>
</div> </div>
<div *ngIf="filterVisible || anyFilterActive" @fade>
<app-card>
<ng-content></ng-content>
</app-card>
</div>

View File

@@ -13,3 +13,8 @@
color: #A6C4F5; color: #A6C4F5;
} }
.filter-active {
color: #a21;
cursor: not-allowed;
}

View File

@@ -1,18 +1,21 @@
import {Component, OnInit} from '@angular/core'; import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {faFilter} from '@fortawesome/free-solid-svg-icons/faFilter'; import {faFilter} from '@fortawesome/free-solid-svg-icons/faFilter';
import {faBars} from '@fortawesome/free-solid-svg-icons/faBars';
import {faPlus} from '@fortawesome/free-solid-svg-icons/faPlus'; import {faPlus} from '@fortawesome/free-solid-svg-icons/faPlus';
import {fade} from '../../../animations';
@Component({ @Component({
selector: 'app-list-header', selector: 'app-list-header',
templateUrl: './list-header.component.html', templateUrl: './list-header.component.html',
styleUrls: ['./list-header.component.less'] styleUrls: ['./list-header.component.less'],
animations: [fade]
}) })
export class ListHeaderComponent implements OnInit { export class ListHeaderComponent implements OnInit {
public faNew = faPlus; public faNew = faPlus;
public faFilter = faFilter; public faFilter = faFilter;
public faMenu = faBars; public filterVisible = false;
@Output() filterVisibleChanged = new EventEmitter<boolean>();
@Input() anyFilterActive = false;
constructor() { constructor() {
} }
@@ -20,4 +23,7 @@ export class ListHeaderComponent implements OnInit {
ngOnInit() { ngOnInit() {
} }
public onFilterClick(): void {
this.filterVisible = !this.filterVisible || this.anyFilterActive;
}
} }

View File

@@ -4,6 +4,7 @@ import {ListHeaderComponent} from './list-header.component';
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome'; import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
import {MatButtonModule} from '@angular/material/button'; import {MatButtonModule} from '@angular/material/button';
import {RouterModule} from '@angular/router'; import {RouterModule} from '@angular/router';
import {CardModule} from '../card/card.module';
@NgModule({ @NgModule({
declarations: [ListHeaderComponent], declarations: [ListHeaderComponent],
@@ -14,7 +15,8 @@ import {RouterModule} from '@angular/router';
CommonModule, CommonModule,
FontAwesomeModule, FontAwesomeModule,
MatButtonModule, MatButtonModule,
RouterModule RouterModule,
CardModule,
] ]
}) })
export class ListHeaderModule { export class ListHeaderModule {