currency filter (#305)

* something

* working something

* good working something

* currency filter

* prettier

* save on close

* fixes

* more fixes

* fix

* remove test data
This commit is contained in:
Denis Ezhov 2020-10-23 17:45:21 +03:00 committed by GitHub
parent 9abfed21c7
commit 16243f92f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 302 additions and 8 deletions

View File

@ -5,10 +5,11 @@ import { FlexModule } from '@angular/flex-layout';
import { FilterShopsModule } from '@dsh/app/shared/*';
import { DaterangeFilterModule } from '@dsh/components/filters/daterange-filter';
import { CurrencyFilterModule } from '../../../../shared/components/filters/currency-filter';
import { AnalyticsSearchFiltersComponent } from './analytics-search-filters.component';
@NgModule({
imports: [CommonModule, DaterangeFilterModule, FilterShopsModule, FlexModule],
imports: [CommonModule, DaterangeFilterModule, FilterShopsModule, FlexModule, CurrencyFilterModule],
exports: [AnalyticsSearchFiltersComponent],
declarations: [AnalyticsSearchFiltersComponent],
})

View File

@ -0,0 +1,12 @@
<dsh-radio-group-filter
*transloco="let t; scope: 'currency-filter'; read: 'currencyFilter'"
[label]="t.label"
[selected]="selected"
withoutClear
[formatSelectedLabel]="formatCurrencyLabel"
(selectedChange)="this.selectedChange.emit($event)"
>
<dsh-radio-group-filter-option *ngFor="let currency of currencies" [value]="currency">
{{ currency }}
</dsh-radio-group-filter-option>
</dsh-radio-group-filter>

View File

@ -0,0 +1,17 @@
import { getCurrencySymbol } from '@angular/common';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'dsh-currency-filter',
templateUrl: 'currency-filter.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CurrencyFilterComponent {
@Input() currencies: string[];
@Input() selected?: string;
@Output() selectedChange = new EventEmitter<string>();
formatCurrencyLabel(currency: string): string {
return getCurrencySymbol(currency, 'narrow');
}
}

View File

@ -0,0 +1,17 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { TranslocoModule } from '@ngneat/transloco';
import { MultiselectFilterModule } from '@dsh/components/filters/multiselect-filter';
import { RadioGroupFilterModule } from '@dsh/components/filters/radio-group-filter';
import { CurrencyFilterComponent } from './currency-filter.component';
const EXPORTED_DECLARATIONS = [CurrencyFilterComponent];
@NgModule({
imports: [MultiselectFilterModule, CommonModule, TranslocoModule, RadioGroupFilterModule],
declarations: EXPORTED_DECLARATIONS,
exports: EXPORTED_DECLARATIONS,
})
export class CurrencyFilterModule {}

View File

@ -0,0 +1,2 @@
export * from './currency-filter.component';
export * from './currency-filter.module';

View File

@ -1,8 +1,9 @@
import { NgModule } from '@angular/core';
import { CurrencyFilterModule } from './currency-filter';
import { FilterShopsModule } from './filter-shops';
const EXPORTED_MODULES = [FilterShopsModule];
const EXPORTED_MODULES = [FilterShopsModule, CurrencyFilterModule];
@NgModule({
imports: EXPORTED_MODULES,

View File

@ -0,0 +1,3 @@
{
"label": "Валюта"
}

View File

@ -1,8 +1,8 @@
<mat-divider></mat-divider>
<div class="actions">
<div *transloco="let t" class="actions">
<ng-content></ng-content>
<div *transloco="let t" fxFlex fxLayoutAlign="space-between" fxLayoutGap="20px">
<div *ngIf="!withoutClear; else saveOnly" fxFlex fxLayoutAlign="space-between" fxLayoutGap="20px">
<div>
<button dsh-button (click)="clear.emit($event)">{{ t.clear }}</button>
</div>
@ -10,4 +10,7 @@
<button dsh-button color="accent" (click)="save.emit($event)">{{ t.save }}</button>
</div>
</div>
<ng-template #saveOnly>
<button fxFlex dsh-button color="accent" (click)="save.emit($event)">{{ t.save }}</button>
</ng-template>
</div>

View File

@ -1,4 +1,6 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Output } from '@angular/core';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { coerceBoolean } from '../../../../utils/coerce';
@Component({
selector: 'dsh-filter-button-actions',
@ -7,6 +9,7 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Output } from '@angul
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FilterButtonActionsComponent {
@Input() @coerceBoolean withoutClear = false;
@Output() clear = new EventEmitter<MouseEvent>();
@Output() save = new EventEmitter<MouseEvent>();
}

View File

@ -22,5 +22,5 @@
</div>
</div>
</dsh-filter-button-content>
<dsh-filter-button-actions (clear)="clear()" (save)="save(); filter.close()"></dsh-filter-button-actions>
<dsh-filter-button-actions (clear)="clear()" (save)="filter.close()"></dsh-filter-button-actions>
</dsh-filter>

View File

@ -12,10 +12,19 @@ import {
} from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { BehaviorSubject, combineLatest, merge, Observable, ReplaySubject, Subject } from 'rxjs';
import { map, mapTo, pluck, shareReplay, startWith, switchMap, withLatestFrom } from 'rxjs/operators';
import {
distinctUntilChanged,
map,
mapTo,
pluck,
shareReplay,
startWith,
switchMap,
withLatestFrom,
} from 'rxjs/operators';
import { ComponentChanges } from '../../../type-utils';
import { mapItemsToLabel } from './mapItemsToLabel';
import { mapItemsToLabel } from './map-items-to-label';
import { MultiselectFilterOptionComponent } from './multiselect-filter-option';
@Component({
@ -95,6 +104,7 @@ export class MultiselectFilterComponent<T = any> implements OnInit, OnChanges, A
.pipe(
withLatestFrom(this.selectedValues$),
pluck(1),
distinctUntilChanged(),
map((selected) => this.mapInputValuesToOptions(selected)),
map((options) => options.map(({ value }) => value))
)

View File

@ -0,0 +1,3 @@
export * from './radio-group-filter.component';
export * from './radio-group-filter-option';
export * from './radio-group-filter.module';

View File

@ -0,0 +1,17 @@
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export const mapItemToLabel = (
s: Observable<{
selectedItemLabel: string;
label: string;
formatSelectedLabel: <T>(o: T) => string;
}>
): Observable<string> =>
s.pipe(
map(({ selectedItemLabel, label, formatSelectedLabel }) =>
selectedItemLabel
? `${label} · ${formatSelectedLabel ? formatSelectedLabel(selectedItemLabel) : selectedItemLabel}`
: label
)
);

View File

@ -0,0 +1 @@
export * from './radio-group-filter-option.component';

View File

@ -0,0 +1,3 @@
<mat-radio-button [checked]="selected$ | async" (change)="toggle.emit()">
<ng-content></ng-content>
</mat-radio-button>

View File

@ -0,0 +1,30 @@
import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, ViewChild } from '@angular/core';
import { MatRadioButton } from '@angular/material/radio';
import { BehaviorSubject } from 'rxjs';
@Component({
selector: 'dsh-radio-group-filter-option',
templateUrl: 'radio-group-filter-option.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RadioGroupFilterOptionComponent<T = any> {
@Input() value: T;
toggle = new EventEmitter<void>();
@ViewChild(MatRadioButton, { read: ElementRef }) private content: ElementRef;
get label() {
return (this.content?.nativeElement?.textContent || '').trim();
}
get selected() {
return this.selected$.value;
}
selected$ = new BehaviorSubject(false);
select(isSelected: boolean) {
this.selected$.next(isSelected);
}
}

View File

@ -0,0 +1,12 @@
<dsh-filter [title]="title$ | async" [active]="!!(savedSelectedOption$ | async)" (closed)="save()" #filter>
<dsh-filter-button-content class="content">
<div fxLayout="column" fxLayoutGap="16px">
<ng-content></ng-content>
</div>
</dsh-filter-button-content>
<dsh-filter-button-actions
[withoutClear]="withoutClear"
(save)="filter.close()"
(clear)="clear()"
></dsh-filter-button-actions>
</dsh-filter>

View File

@ -0,0 +1,3 @@
.content {
width: 360px;
}

View File

@ -0,0 +1,122 @@
import {
AfterContentInit,
ChangeDetectionStrategy,
Component,
ContentChildren,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
QueryList,
} from '@angular/core';
import { BehaviorSubject, combineLatest, merge, Observable, ReplaySubject, Subject } from 'rxjs';
import {
distinctUntilChanged,
map,
mapTo,
pluck,
shareReplay,
startWith,
switchMap,
withLatestFrom,
} from 'rxjs/operators';
import { ComponentChanges } from '../../../type-utils';
import { coerceBoolean } from '../../../utils/coerce';
import { mapItemToLabel } from './map-item-to-label';
import { RadioGroupFilterOptionComponent } from './radio-group-filter-option';
@Component({
selector: 'dsh-radio-group-filter',
templateUrl: 'radio-group-filter.component.html',
styleUrls: ['radio-group-filter.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RadioGroupFilterComponent<T = any> implements OnInit, OnChanges, AfterContentInit {
@Input() label: string;
@Input() @coerceBoolean withoutClear = false;
@Input() selected?: T;
@Output() selectedChange = new EventEmitter<T>();
@ContentChildren(RadioGroupFilterOptionComponent) options: QueryList<RadioGroupFilterOptionComponent<T>>;
private save$ = new Subject<void>();
private clear$ = new Subject<void>();
private selectFromInput$ = new ReplaySubject<T>(1);
private options$ = new BehaviorSubject<RadioGroupFilterOptionComponent<T>[]>([]);
private selectedValue$ = new BehaviorSubject<T>(undefined);
savedSelectedOption$: Observable<RadioGroupFilterOptionComponent<T>> = combineLatest([
merge(this.selectFromInput$, this.save$.pipe(withLatestFrom(this.selectedValue$), pluck(1))),
this.options$,
]).pipe(
map(([selected]) => this.mapInputValueToOption(selected)),
startWith(null),
shareReplay(1)
);
title$: Observable<string> = this.savedSelectedOption$.pipe(
map((selectedOption) => ({
selectedItemLabel: selectedOption?.label,
label: this.label,
formatSelectedLabel: this.formatSelectedLabel,
})),
mapItemToLabel,
shareReplay(1)
);
@Input() formatSelectedLabel?: (o: T) => string = (o) => o.toString();
@Input() compareWith?: (o1: T, o2: T) => boolean = (o1, o2) => o1 === o2;
ngOnInit() {
combineLatest([this.selectedValue$, this.options$]).subscribe(([selectedValue, options]) =>
options.forEach((o) => o.select(this.compareWith(selectedValue, o.value)))
);
merge(this.selectFromInput$, this.clear$.pipe(mapTo(undefined))).subscribe((selectedValue) =>
this.selectedValue$.next(selectedValue)
);
this.options$
.pipe(switchMap((options) => merge(...options.map((option) => option.toggle.pipe(mapTo(option.value))))))
.subscribe((selected) => this.selectedValue$.next(selected));
this.save$
.pipe(
withLatestFrom(this.selectedValue$),
pluck(1),
distinctUntilChanged(),
map((selected) => this.mapInputValueToOption(selected)),
map((option) => option?.value)
)
.subscribe((selectedValue) => this.selectedChange.emit(selectedValue));
}
ngAfterContentInit() {
this.options.changes
.pipe(
startWith(this.options),
map((o: RadioGroupFilterComponent<T>['options']) => o.toArray())
)
.subscribe((o) => this.options$.next(o));
}
ngOnChanges({ selected }: ComponentChanges<RadioGroupFilterComponent>) {
if (selected && selected.currentValue) {
this.selectFromInput$.next(selected.currentValue);
}
}
private mapInputValueToOption(inputValue: T) {
return this.options.toArray().find((o) => this.compareWith(inputValue, o.value));
}
save() {
this.save$.next();
}
clear() {
this.clear$.next();
}
}

View File

@ -0,0 +1,34 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDividerModule } from '@angular/material/divider';
import { MatInputModule } from '@angular/material/input';
import { MatRadioModule } from '@angular/material/radio';
import { TranslocoModule } from '@ngneat/transloco';
import { ButtonModule } from '../../buttons';
import { FilterModule } from '../filter';
import { RadioGroupFilterOptionComponent } from './radio-group-filter-option';
import { RadioGroupFilterComponent } from './radio-group-filter.component';
const EXPORTED_DECLARATIONS = [RadioGroupFilterComponent, RadioGroupFilterOptionComponent];
@NgModule({
imports: [
CommonModule,
MatDividerModule,
ButtonModule,
TranslocoModule,
FlexLayoutModule,
MatInputModule,
MatCheckboxModule,
ReactiveFormsModule,
FilterModule,
MatRadioModule,
],
declarations: EXPORTED_DECLARATIONS,
exports: EXPORTED_DECLARATIONS,
})
export class RadioGroupFilterModule {}