Add CustomerSearch Service

This commit is contained in:
Sebastian
2020-09-29 16:38:51 +02:00
parent 4a7032fb1b
commit 255d6bbb9d
8 changed files with 221 additions and 113 deletions

View File

@@ -1,35 +1,31 @@
import {
Component,
ChangeDetectionStrategy,
ChangeDetectorRef,
OnDestroy,
OnInit,
ViewChild,
AfterContentInit,
forwardRef,
NgZone,
} from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { Router } from '@angular/router';
import { CrmCustomerService } from '@domain/crm';
import { AutocompleteDTO } from '@swagger/crm';
import { UiSearchboxAutocompleteComponent } from '@ui/searchbox';
import { Observable, Subject } from 'rxjs';
import {
catchError,
debounceTime,
filter,
map,
switchMap,
takeUntil,
tap,
} from 'rxjs/operators';
import { CustomerSearchService } from './customer-search.service';
import { CustomerSearch } from './customer-search.service';
@Component({
selector: 'page-customer-search',
templateUrl: 'customer-search.component.html',
styleUrls: ['customer-search.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [CustomerSearchService],
providers: [
{
provide: CustomerSearch,
useExisting: forwardRef(() => CustomerSearchComponent),
},
],
})
export class CustomerSearchComponent {}
export class CustomerSearchComponent extends CustomerSearch {
constructor(
protected customerSearch: CrmCustomerService,
zone: NgZone,
router: Router
) {
super(zone, router);
}
}

View File

@@ -1,6 +1,85 @@
import { Injectable } from '@angular/core';
import { Injectable, NgZone, OnDestroy, OnInit } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { CrmCustomerService } from '@domain/crm';
import { AutocompleteDTO } from '@swagger/crm';
import { Observable, Subject } from 'rxjs';
import {
catchError,
debounceTime,
filter,
map,
shareReplay,
switchMap,
takeUntil,
} from 'rxjs/operators';
@Injectable()
export class CustomerSearchService {
constructor() {}
export abstract class CustomerSearch implements OnInit, OnDestroy {
protected abstract customerSearch: CrmCustomerService;
private destroy$ = new Subject();
queryControl: FormControl;
autocompleteResult$: Observable<AutocompleteDTO[]>;
inputChange$ = new Subject<string>();
constructor(private zone: NgZone, private router: Router) {}
ngOnInit() {
this.initQueryControl();
this.initAutocomplete();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
private initAutocomplete() {
this.autocompleteResult$ = this.inputChange$.pipe(
takeUntil(this.destroy$),
filter(() => this.queryControl.valid),
debounceTime(150),
switchMap((queryString) =>
this.customerSearch.complete(queryString).pipe(
map((response) => response.result),
catchError(() => {
// TODO Dialog Service impl. fur Anzeige der Fehlermeldung
return [];
})
)
),
shareReplay()
);
}
private initQueryControl() {
this.queryControl = new FormControl('', [Validators.required]);
}
getQueryParams() {
return {
query: this.queryControl.value,
};
}
setQueryParams(queryParams: Params) {
this.queryControl.setValue(queryParams.query || '', { emitEvent: false });
}
parseQueryParams(activatedRoute: ActivatedRoute) {
this.setQueryParams(activatedRoute.snapshot.queryParams);
}
updateUrlQueryParams() {
this.zone.run(() => {
this.router.navigate([], {
queryParams: this.getQueryParams(),
queryParamsHandling: 'merge',
});
});
}
}

View File

@@ -0,0 +1,13 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-search-filter',
templateUrl: 'search-filter.component.html',
styleUrls: ['search-filter.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SearchFilterComponent implements OnInit {
constructor() {}
ngOnInit() {}
}

View File

@@ -9,20 +9,20 @@
</p>
<ui-searchbox>
<input
[formControl]="queryControl"
[formControl]="search.queryControl"
type="text"
uiSearchboxInput
placeholder="Name, E-Mail, Kundennummer, ..."
(inputChange)="inputChange$.next($event)"
(inputChange)="search.inputChange$.next($event)"
/>
<ui-searchbox-warning>
Keine Suchergebnisse
</ui-searchbox-warning>
<button
*ngIf="queryControl?.value?.length"
*ngIf="search.queryControl?.value?.length"
type="reset"
uiSearchboxClearButton
(click)="queryControl.reset()"
(click)="search.queryControl.reset()"
>
<ui-icon icon="close" size="22px"></ui-icon>
</button>
@@ -37,7 +37,7 @@
<button
uiSearchboxAutocompleteOption
[value]="result?.query"
*ngFor="let result of autocompleteResult$ | async"
*ngFor="let result of search.autocompleteResult$ | async"
>
{{ result?.display }}
</button>

View File

@@ -0,0 +1,85 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { AutocompleteDTO } from '@swagger/crm';
import { UiIconModule } from '@ui/icon';
import { UiSearchboxModule } from '@ui/searchbox';
import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
import { skip, take } from 'rxjs/operators';
import { CustomerSearch } from '../customer-search.service';
import { CustomerSearchMainComponent } from './search-main.component';
fdescribe('CustomerSearchMainComponent', () => {
let fixture: ComponentFixture<CustomerSearchMainComponent>;
let component: CustomerSearchMainComponent;
let searchService: jasmine.SpyObj<CustomerSearch>;
const mockActivatedRoute = ({
snapshot: { queryParams: { query: 'test' } },
} as unknown) as ActivatedRoute;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [CustomerSearchMainComponent],
imports: [
RouterTestingModule,
ReactiveFormsModule,
UiSearchboxModule,
UiIconModule,
],
providers: [
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
{
provide: CustomerSearch,
useClass: CustomerSearchMock,
},
],
}).compileComponents();
fixture = TestBed.createComponent(CustomerSearchMainComponent);
component = fixture.componentInstance;
component.search.queryControl = new FormControl('');
searchService = TestBed.inject(CustomerSearch) as jasmine.SpyObj<
CustomerSearch
>;
});
it('should be created', () => {
expect(component instanceof CustomerSearchMainComponent).toBeTruthy();
});
describe('initAutocomplete', () => {
let result: AutocompleteDTO[];
beforeEach(() => {
result = [{ id: 'test 1' }, { id: 'test 2' }];
});
it('should be called OnInit', async () => {
spyOn(component, 'initAutocomplete').and.callFake(() => {});
spyOnProperty(searchService, 'autocompleteResult$').and.returnValue(
of(result)
);
spyOn(component.autocomplete, 'open').and.callFake(() => {});
component.ngOnInit();
await searchService.autocompleteResult$.pipe(take(1)).toPromise();
expect(component.initAutocomplete).toHaveBeenCalledTimes(1);
// expect(component.autocomplete.open).toHaveBeenCalledTimes(1);
});
});
});
export class CustomerSearchMock {
get autocompleteResult$(): Observable<AutocompleteDTO[]> {
return of([{ id: '1' }]);
}
parseQueryParams() {}
setQueryParams() {}
getQueryParams() {}
}

View File

@@ -2,27 +2,16 @@ import {
Component,
ChangeDetectionStrategy,
ChangeDetectorRef,
NgZone,
OnDestroy,
OnInit,
ViewChild,
} from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { Router, ActivatedRoute } from '@angular/router';
import { CrmCustomerService } from '@domain/crm';
import { AutocompleteDTO } from '@swagger/crm';
import { ActivatedRoute } from '@angular/router';
import { UiSearchboxAutocompleteComponent } from '@ui/searchbox';
import { Subject, Observable } from 'rxjs';
import {
catchError,
debounceTime,
filter,
map,
switchMap,
takeUntil,
tap,
} from 'rxjs/operators';
import { CustomerSearchService } from '../customer-search.service';
import { Subject } from 'rxjs';
import { delay, takeUntil, tap } from 'rxjs/operators';
import { CustomerSearch } from '../customer-search.service';
@Component({
selector: 'customer-search-main',
@@ -33,12 +22,6 @@ import { CustomerSearchService } from '../customer-search.service';
export class CustomerSearchMainComponent implements OnInit, OnDestroy {
private destroy$ = new Subject();
queryControl: FormControl;
autocompleteResult$: Observable<AutocompleteDTO[]>;
inputChange$ = new Subject<string>();
@ViewChild(UiSearchboxAutocompleteComponent, {
read: UiSearchboxAutocompleteComponent,
static: true,
@@ -46,17 +29,14 @@ export class CustomerSearchMainComponent implements OnInit, OnDestroy {
autocomplete: UiSearchboxAutocompleteComponent;
constructor(
public search: CustomerSearchService,
public search: CustomerSearch,
public cdr: ChangeDetectorRef,
private customerSearch: CrmCustomerService,
private router: Router,
private activatedRoute: ActivatedRoute,
private zone: NgZone
private activatedRoute: ActivatedRoute
) {}
ngOnInit() {
this.initQueryControl();
this.initAutocomplete();
this.search.parseQueryParams(this.activatedRoute);
}
ngOnDestroy() {
@@ -65,56 +45,21 @@ export class CustomerSearchMainComponent implements OnInit, OnDestroy {
}
initAutocomplete() {
this.autocompleteResult$ = this.inputChange$.pipe(
takeUntil(this.destroy$),
filter(() => this.queryControl.valid),
debounceTime(150),
switchMap((queryString) =>
this.customerSearch.complete(queryString).pipe(
map((response) => response.result),
catchError(() => {
// TODO Dialog Service impl. fur Anzeige der Fehlermeldung
return [];
})
)
),
tap((result) => {
if (result.length) {
this.autocomplete.open();
} else {
this.autocomplete.close();
}
setTimeout(() => {
this.cdr.detectChanges();
});
})
);
}
initQueryControl() {
this.queryControl = new FormControl(
this.activatedRoute.snapshot.queryParams.query || '',
[Validators.required]
);
this.queryControl.valueChanges
this.search.autocompleteResult$
.pipe(
takeUntil(this.destroy$),
debounceTime(500),
tap((query) => this.updateQueryParam({ query }))
tap((results) => {
console.log({ results });
if (results.length > 0) {
this.autocomplete.open();
} else {
this.autocomplete.close();
}
}),
delay(1)
)
.subscribe(() => {
this.cdr.detectChanges();
});
}
updateQueryParam(params: { query?: string }) {
this.zone.run(() => {
this.router.navigate(['./'], {
queryParams: params,
relativeTo: this.activatedRoute,
queryParamsHandling: 'merge',
});
});
}
}

9
package-lock.json generated
View File

@@ -10843,15 +10843,6 @@
"integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==",
"dev": true
},
"jasmine-marbles": {
"version": "0.6.0",
"resolved": "https://pkgs.dev.azure.com/hugendubel/_packaging/hugendubel@Local/npm/registry/jasmine-marbles/-/jasmine-marbles-0.6.0.tgz",
"integrity": "sha1-943Bo7xFKXbeEO6LR8c9YWUyqVQ=",
"dev": true,
"requires": {
"lodash": "^4.5.0"
}
},
"jasmine-spec-reporter": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-5.0.2.tgz",

View File

@@ -93,7 +93,6 @@
"codelyzer": "^5.1.2",
"husky": "^4.2.3",
"jasmine-core": "~3.5.0",
"jasmine-marbles": "^0.6.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~5.0.0",
"karma-chrome-launcher": "~3.1.0",