searchbox

This commit is contained in:
2019-11-28 20:01:30 +01:00
committed by smuddy
parent 47f15f15ad
commit 6de7de7224
16 changed files with 141 additions and 16 deletions

20
src/app/animations.ts Normal file
View File

@@ -0,0 +1,20 @@
import {animate, query, stagger, state, style, transition, trigger} from '@angular/animations';
export const fade = [
// the fade-in/fade-out animation.
trigger('fade', [
// the "in" style determines the "resting" state of the element when it is visible.
state('in', style({opacity: 1, display: 'block', transform: 'translateY(0px)'})),
// fade in when created. this could also be written as transition('void => *')
transition(':enter', [
style({opacity: 0, display: 'block', transform: 'translateY(-20px)'}),
animate(300)
]),
// fade out when destroyed. this could also be written as transition('void => *')
transition(':leave',
animate(300, style({opacity: 0, display: 'block', transform: 'translateY(-20px)'})))
])
];

View File

@@ -16,6 +16,7 @@ import {AngularFirestoreModule} from '@angular/fire/firestore';
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
BrowserAnimationsModule,
AppRoutingModule, AppRoutingModule,
ServiceWorkerModule.register('ngsw-worker.js', {enabled: environment.production}), ServiceWorkerModule.register('ngsw-worker.js', {enabled: environment.production}),
BrowserAnimationsModule, BrowserAnimationsModule,

View File

@@ -2,11 +2,13 @@ import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {NavigationComponent} from './navigation/navigation.component'; import {NavigationComponent} from './navigation/navigation.component';
import {RouterModule} from '@angular/router'; import {RouterModule} from '@angular/router';
import {FilterComponent} from './navigation/filter/filter.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
NavigationComponent NavigationComponent,
FilterComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,

View File

@@ -0,0 +1,2 @@
<input placeholder="Suche" (input)="onInputChange($event.target.value)">

View File

@@ -0,0 +1,16 @@
@import "../../../../styles/styles";
input {
font-size: 16px;
background: transparent;
border: none;
border-bottom: 1px solid #ccc;
color: #888;
margin-right: 20px;
&:focus {
outline: none;
border-bottom: 2px solid @primary-color;
margin-bottom: -1px;
}
}

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,21 @@
import {Component} from '@angular/core';
import {Router} from '@angular/router';
@Component({
selector: 'app-filter',
templateUrl: './filter.component.html',
styleUrls: ['./filter.component.less']
})
export class FilterComponent {
constructor(private router: Router) {
}
public onInputChange(text: string): void {
const route = text
? this.router.createUrlTree(['songs'], {queryParams: {q: text}})
: this.router.createUrlTree(['songs']);
this.router.navigateByUrl(route);
}
}

View File

@@ -1,5 +1,9 @@
<nav class="head"> <nav class="head">
<a href="#" routerLink="/songs" routerLinkActive="active">Inhalt</a> <div class="links">
<a href="#" routerLink="/songs" routerLinkActive="active">Inhalt</a></div>
<div class="actions">
<app-filter></app-filter>
</div>
</nav> </nav>

View File

@@ -13,6 +13,7 @@ nav {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
justify-content: space-between;
a { a {
display: block; display: block;
@@ -38,3 +39,9 @@ nav {
} }
} }
.actions {
display: flex;
height: 100%;
align-items: center;
}

View File

@@ -27,7 +27,7 @@ describe('SongService', () => {
it('should list songs', async(() => { it('should list songs', async(() => {
const service: SongService = TestBed.get(SongService); const service: SongService = TestBed.get(SongService);
service.list().subscribe(songs => { service.list$().subscribe(songs => {
expect(songs).toEqual(<any>[ expect(songs).toEqual(<any>[
{title: 'title1'} {title: 'title1'}
]); ]);

View File

@@ -11,7 +11,7 @@ export class SongService {
constructor(private songDataService: SongDataService) { constructor(private songDataService: SongDataService) {
} }
public list = (): Observable<Song[]> => this.songDataService.list(); public list$ = (): Observable<Song[]> => this.songDataService.list();
public read = (songId: string): Observable<Song | undefined> => this.songDataService.read(songId); public read = (songId: string): Observable<Song | undefined> => this.songDataService.read(songId);
} }

View File

@@ -14,6 +14,7 @@
&:hover { &:hover {
background: @primary-color; background: @primary-color;
color: #fff;
} }
} }

View File

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

View File

@@ -40,7 +40,7 @@ describe('SongListComponent', () => {
it('should read songs from SongService', fakeAsync(() => { it('should read songs from SongService', fakeAsync(() => {
tick(); tick();
expect(component.songs).toEqual(<any>[ expect(component.songs$).toEqual(<any>[
{title: 'title1'} {title: 'title1'}
]); ]);
})); }));

View File

@@ -1,24 +1,50 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {SongService} from '../services/song.service'; import {SongService} from '../services/song.service';
import {Song} from '../models/song'; import {Song} from '../models/song';
import {map} from 'rxjs/operators'; import {debounceTime, map} from 'rxjs/operators';
import {Observable} from 'rxjs'; import {combineLatest, Observable} from 'rxjs';
import {fade} from '../../animations';
import {ActivatedRoute} from '@angular/router';
@Component({ @Component({
selector: 'app-songs', selector: 'app-songs',
templateUrl: './song-list.component.html', templateUrl: './song-list.component.html',
styleUrls: ['./song-list.component.less'] styleUrls: ['./song-list.component.less'],
animations: [fade]
}) })
export class SongListComponent implements OnInit { export class SongListComponent implements OnInit {
public songs: Observable<Song[]>; public songs$: Observable<Song[]>;
constructor(private songService: SongService) { constructor(private songService: SongService, private activatedRoute: ActivatedRoute) {
} }
ngOnInit() { ngOnInit() {
this.songs = this.songService.list().pipe(map(songs => const filter$ = this.activatedRoute.queryParams.pipe(
songs.sort((a, b) => a.number - b.number) debounceTime(300),
)); map(_ => _.q)
);
const songs$ = this.songService.list$().pipe(
map(songs => songs.sort((a, b) => a.number - b.number)),
);
this.songs$ = combineLatest([filter$, songs$]).pipe(
map(_ => _[1].filter(song => this.filter(song, _[0])))
);
} }
private filter(song: Song, filterValue: string): boolean {
if (!filterValue) {
return true;
}
const textMatch = song.text && this.normalize(song.text).indexOf(this.normalize(filterValue)) !== -1;
const titleMatch = song.title && this.normalize(song.title).indexOf(this.normalize(filterValue)) !== -1;
return textMatch || titleMatch;
}
private normalize( input: string): string {
return input.toLowerCase().replace(/\s/g, '');
}
} }

View File

@@ -1,4 +1,4 @@
@primary-color: #5285ff; @primary-color: #4286f4;
@navigation-background: #fffffff1; @navigation-background: #fffffff1;
@navigation-link-color: #555; @navigation-link-color: #555;