diff --git a/src/app/app.component.html b/src/app/app.component.html index 80a3f6a..779fa92 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,3 +1,17 @@ + + + My App + + + + + @@ -23,4 +37,15 @@
Comment Value: {{parseComment(form)}}
+ + Choose an option + + + @for (item of filteredOptions; track $index) { + {{item}} + } + + + \ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 7289a64..b65713e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -3,12 +3,21 @@ import { RouterOutlet } from '@angular/router'; import { CommentBoxComponent } from "./comment-box/comment-box.component"; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; -import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ProgressStepperComponent } from "./progress-stepper/progress-stepper.component"; +import { Observable, startWith, map } from 'rxjs'; +import { MatOptionModule } from '@angular/material/core'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatChipsModule } from '@angular/material/chips'; +import { AsyncPipe } from '@angular/common'; +import { MatSelectModule } from '@angular/material/select'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatIconModule } from '@angular/material/icon'; +import { SelectFilterComponent } from "./select-filter/select-filter.component"; @Component({ selector: 'app-root', - imports: [RouterOutlet, CommentBoxComponent, MatFormFieldModule, MatInputModule, ReactiveFormsModule, ProgressStepperComponent], + imports: [RouterOutlet, MatToolbarModule, MatIconModule, CommentBoxComponent, MatFormFieldModule, MatInputModule, ReactiveFormsModule, ProgressStepperComponent, MatOptionModule, MatSelectModule, MatAutocompleteModule, MatChipsModule, AsyncPipe, FormsModule, SelectFilterComponent], templateUrl: './app.component.html', styleUrl: './app.component.scss' }) @@ -24,4 +33,16 @@ export class AppComponent { if (comment.value) return comment.value } + + allOptions = ['Apple', 'Banana', 'Orange', 'Pear', 'Grape']; + filteredOptions = this.allOptions.slice(); + selectedItemsList = new FormControl(); + + selectAll(): void { + this.selectedItemsList.setValue(this.allOptions); + } + + unselectAll(): void { + this.selectedItemsList.setValue([]); + } } diff --git a/src/app/select-filter/select-filter.component.html b/src/app/select-filter/select-filter.component.html new file mode 100644 index 0000000..a20eb06 --- /dev/null +++ b/src/app/select-filter/select-filter.component.html @@ -0,0 +1,18 @@ +
+
+ + @if (localSpinner) { + + } +
+ @if (noResults) { +
+ {{noResultsMessage}} +
+ } +
+
+
Alle auswählen
+
Alle abwählen
+
\ No newline at end of file diff --git a/src/app/select-filter/select-filter.component.scss b/src/app/select-filter/select-filter.component.scss new file mode 100644 index 0000000..337b9de --- /dev/null +++ b/src/app/select-filter/select-filter.component.scss @@ -0,0 +1,69 @@ +.mat-filter { + position: sticky; + top: -8px; + border-bottom-width: 1px; + border-bottom-style: solid; + border-bottom-color: grey; + z-index: 100; + font-size: inherit; + box-shadow: none; + border-radius: 0; + padding: 8px; + -webkit-box-sizing: border-box; + box-sizing: border-box; + + background-color: var(--mat-sys-surface-container); +} + +.mat-filter-input { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + outline: none; + border: 0; + background-color: unset; + color: gray; + width: 100%; +} + +.spinner { + position: absolute; + right: 16px; + top: calc(50% - 8px); +} + +.noResultsMessage { + margin-top: 10px; + font-family: Roboto, "Helvetica Neue", sans-serif; + font-size: 16px; +} + +.selection-buttons { + display: grid; + grid-template-columns: 50% 50%; + justify-content: center; + font-size: 0.8rem; + + border-bottom-width: 1px; + border-bottom-style: solid; + border-bottom-color: grey; + + .button { + display: flex; + padding: 0.15rem; + justify-content: center; + + &:hover { + cursor: pointer; + + background-color: var( + --mat-option-hover-state-layer-color, + color-mix( + in srgb, + var(--mat-sys-on-surface) calc(var(--mat-sys-hover-state-layer-opacity) * 100%), + transparent + ) + ); + } + } +} diff --git a/src/app/select-filter/select-filter.component.spec.ts b/src/app/select-filter/select-filter.component.spec.ts new file mode 100644 index 0000000..eb46298 --- /dev/null +++ b/src/app/select-filter/select-filter.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SelectFilterComponent } from './select-filter.component'; + +describe('SelectFilterComponent', () => { + let component: SelectFilterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SelectFilterComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SelectFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/select-filter/select-filter.component.ts b/src/app/select-filter/select-filter.component.ts new file mode 100644 index 0000000..c12dc0f --- /dev/null +++ b/src/app/select-filter/select-filter.component.ts @@ -0,0 +1,85 @@ +import { A, Z, ZERO, NINE, SPACE } from '@angular/cdk/keycodes'; +import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import { FormGroup, FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { Subscription } from 'rxjs'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatRippleModule } from '@angular/material/core'; + +@Component({ + selector: 'app-select-filter', + imports: [ReactiveFormsModule, MatProgressSpinnerModule, MatRippleModule], + templateUrl: './select-filter.component.html', + styleUrl: './select-filter.component.scss' +}) +export class SelectFilterComponent { + + private searchFormValueChangesSubscription!: Subscription; + @ViewChild('input', { static: true }) input!: ElementRef; + + @Input('array') array: any[] = []; + @Input('showSpinner') showSpinner = true; + @Input('noResultsMessage') noResultsMessage = 'No results'; + + noResults = false; + + localSpinner = false; + @Output() filteredReturn = new EventEmitter(); + @Output() selectAll = new EventEmitter(); + @Output() unselectAll = new EventEmitter(); + + public filteredItems: any = []; + public searchForm: FormGroup; + + constructor(fb: FormBuilder) { + this.searchForm = fb.group({ + value: '' + }); + } + + ngOnInit() { + this.searchFormValueChangesSubscription = this.searchForm.valueChanges.subscribe(change => { + if (this.showSpinner) { + this.localSpinner = true; + } + if (change.value) { + // IF THE DISPLAY MEMBER INPUT IS SET WE CHECK THE SPECIFIC PROPERTY + + this.filteredItems = this.array.filter((name: string) => name.toLowerCase().includes(change.value.toLowerCase())); + // OTHERWISE, WE CHECK THE ENTIRE STRING + + // NO RESULTS VALIDATION + + this.noResults = this.filteredItems == null || this.filteredItems.length === 0; + + + } else { + this.filteredItems = this.array.slice(); + this.noResults = false; + } + this.filteredReturn.emit(this.filteredItems); + setTimeout(() => { + if (this.showSpinner) { + this.localSpinner = false; + } + }, 2000); + }); + + setTimeout(() => { + this.input.nativeElement.focus(); + }, 500); + } + + handleKeydown(event: KeyboardEvent) { + // PREVENT PROPAGATION FOR ALL ALPHANUMERIC CHARACTERS IN ORDER TO AVOID SELECTION ISSUES + if ((event.key && event.key.length === 1) || + (event.keyCode >= A && event.keyCode <= Z) || + (event.keyCode >= ZERO && event.keyCode <= NINE) || + (event.keyCode === SPACE)) { + event.stopPropagation(); + } + } + ngOnDestroy() { + this.filteredReturn.emit(this.array); + this.searchFormValueChangesSubscription.unsubscribe(); + } +}