diff --git a/projects/go-lib/src/lib/animations/search.animation.ts b/projects/go-lib/src/lib/animations/search.animation.ts new file mode 100644 index 000000000..778306c11 --- /dev/null +++ b/projects/go-lib/src/lib/animations/search.animation.ts @@ -0,0 +1,57 @@ +import { + animate, + state, + style, + transition, + trigger +} from '@angular/animations'; + +const timing = '.5s' +const easing = 'cubic-bezier(.25, .8, .25, 1)'; + +export const searchLoaderAnim = trigger('searchLoaderAnim', [ + transition(':enter', [ + style({ + height: 0, + opacity: 0, + padding: 0 + }), + animate(timing + ' ' + easing, style({ + height: '*', + opacity: 1, + padding: '2rem' + })) + ]), + transition(':leave', [ + animate(timing + ' ' + easing, style({ + height: 0, + opacity: 0, + padding: 0 + })) + ]) +]) + +export const searchResultsAnim = trigger('searchResultsAnim', [ + transition(':enter', [ + style({ + height: 0, + margin: 0, + opacity: 0 + }), + animate(timing + ' .25s ' + easing, style({ + height: '*', + margin: '1rem 0 0.5rem 0', + opacity: 1 + })) + ]), + transition(':leave', [ + style({ + overflowY: 'hidden' + }), + animate(timing + ' ' + easing, style({ + height: 0, + margin: 0, + opacity: 0 + })) + ]) +]) \ No newline at end of file diff --git a/projects/go-lib/src/lib/components/go-search/go-search.component.html b/projects/go-lib/src/lib/components/go-search/go-search.component.html new file mode 100644 index 000000000..57e094975 --- /dev/null +++ b/projects/go-lib/src/lib/components/go-search/go-search.component.html @@ -0,0 +1,37 @@ + \ No newline at end of file diff --git a/projects/go-lib/src/lib/components/go-search/go-search.component.scss b/projects/go-lib/src/lib/components/go-search/go-search.component.scss new file mode 100644 index 000000000..014d0addd --- /dev/null +++ b/projects/go-lib/src/lib/components/go-search/go-search.component.scss @@ -0,0 +1,143 @@ +@import '~@tangoe/gosheets/base/variables'; +@import '~@tangoe/gosheets/base/mixins'; + +.go-search { + position: relative; +} + +.go-search__container { + background: $theme-light-bg; + border: 1px solid $theme-light-border; + border-radius: 1rem; + box-shadow: none; + color: $theme-light-border; + display: flex; + flex-direction: column; + position: absolute; + top: calc(50% - ((1.875rem + 2px) / 2)); + // height of input + border, halfed + width: 600px; + @include transition(all); + + &:hover { + background: lighten($theme-light-app-bg, 3%); + } +} + +.go-search__container--active { + border: 0; + box-shadow: $global-box-shadow; + padding: 0.5rem; + top: calc(50% - (2.875rem / 2)); + // height of input with padding, halfed + + &:hover { + background: $theme-light-bg; + } +} + +.go-search__field { + align-items: center; + display: flex; + @include transition(all); +} + +.go-search__submit { + align-items: center; + background: transparent; + border: 0; + color: $theme-light-border; + display: flex; + font-size: 1rem; + padding: 0 0.5rem; + + &:hover { + cursor: pointer; + } + + &:active, &:focus { + outline: none; + } +} + +.go-search__icon { + height: 1rem; +} + +.go-search__input { + background: transparent; + border: 0; + flex: 1; + font-family: $base-font-stack; + font-size: 0.875rem; + font-weight: 300; + letter-spacing: 0.02rem; + min-width: 250px; + padding: .5rem .5rem .5rem 0; + + &:-ms-input-placeholder { + color: $theme-light-color; + } + + &::placeholder { + color: $theme-light-color; + } + + &:active, &:focus { + outline: none; + } +} + +.go-search__loader-container { + display: flex; + height: calc(4rem + 50px); + justify-content: center; + overflow: hidden; + padding: 2rem; + position: relative; +} + +.go-search__loader { + position: absolute; +} + +.go-search__results { + background: $theme-light-bg; + color: $theme-light-color; + font-size: 0.875rem; + max-height: 400px; + margin: 1rem 0 0.5rem 0; + overflow-x: hidden; + overflow-y: auto; + padding: 0 0.5rem; +} + +/** +* This section should be included in gosheets as a global change. +* Until that happens, we should keep this here. +**/ +::-webkit-scrollbar { + height: 12px; + width: 12px; + + @media (max-width: 768px) { + height: 0 !important; + width: 0 !important; + } +} + +::-webkit-scrollbar-track { + background-color: $theme-light-app-bg; + border-radius: 6px; +} + +::-webkit-scrollbar-thumb { + background: $base-light-secondary; + border: 2px solid $theme-light-app-bg; + border-radius: 6px; + @include transition(all); + + &:hover { + background: $ui-color-neutral-gradient; + } +} diff --git a/projects/go-lib/src/lib/components/go-search/go-search.component.spec.ts b/projects/go-lib/src/lib/components/go-search/go-search.component.spec.ts new file mode 100644 index 000000000..75e7cf4c8 --- /dev/null +++ b/projects/go-lib/src/lib/components/go-search/go-search.component.spec.ts @@ -0,0 +1,36 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; + +import { GoSearchComponent } from './go-search.component'; + +import { GoIconModule } from '../go-icon/go-icon.module'; +import { GoLoaderModule } from '../go-loader/go-loader.module'; + +describe('GoSearchComponent', () => { + let component: GoSearchComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ GoSearchComponent ], + imports: [ + CommonModule, + GoIconModule, + GoLoaderModule, + ReactiveFormsModule + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(GoSearchComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/go-lib/src/lib/components/go-search/go-search.component.ts b/projects/go-lib/src/lib/components/go-search/go-search.component.ts new file mode 100644 index 000000000..98a9e3578 --- /dev/null +++ b/projects/go-lib/src/lib/components/go-search/go-search.component.ts @@ -0,0 +1,82 @@ +import { Component, ElementRef, HostListener, OnInit } from '@angular/core'; +import { AnimationEvent } from '@angular/animations'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; + +import { searchLoaderAnim, searchResultsAnim } from '../../animations/search.animation'; +import { GoSearchService } from './go-search.service'; + +@Component({ + selector: 'go-search', + templateUrl: './go-search.component.html', + styleUrls: ['./go-search.component.scss'], + animations: [searchLoaderAnim, searchResultsAnim] +}) +export class GoSearchComponent implements OnInit { + + goSearchForm: FormGroup; + searchActive: boolean = false; + resultsOverflow: string = 'hidden'; + + @HostListener('document:click') onDocumentClick(event) { + this.closeSearchEvent(event); + } + + constructor( + public goSearchService: GoSearchService, + private elementRef: ElementRef, + private fb: FormBuilder + ) { + this.goSearchForm = this.fb.group({ + term: '' + }); + } + + ngOnInit(): void { + this.goSearchForm.valueChanges.pipe( + debounceTime(500), + distinctUntilChanged() + ).subscribe(changes => { + if (changes['term'].length >= this.goSearchService.termLength) { + this.goSearchService.showNoResultsMessage = false; + this.goSearchService.showLoader = true; + this.goSearchService.updateSearchTerm(changes['term']); + } else { + this.goSearchService.showNoResultsMessage = false; + this.goSearchService.hasResults = false; + this.goSearchService.showLoader = false; + } + }); + } + + resultsStarted(event: AnimationEvent): void { + this.resultsOverflow = 'hidden'; + } + + resultsEnded(event: AnimationEvent): void { + this.resultsOverflow = event.toState === null ? 'auto' : 'hidden'; + } + + toggleActive(): void { + this.searchActive = true; + } + + leaveInput(event: any): void { + if (!this.elementRef.nativeElement.contains(event.relatedTarget)) { + this.closeSearch(); + } + } + + closeSearchEvent(event: any): void { + if (event && !this.elementRef.nativeElement.contains(event.target)) { + this.closeSearch(); + } + } + + closeSearch(): void { + this.searchActive = false; + this.goSearchService.hasResults = false; + this.goSearchService.showNoResultsMessage = false; + this.goSearchForm.reset('', {onlySelf: true, emitEvent: false}); + } +} diff --git a/projects/go-lib/src/lib/components/go-search/go-search.module.ts b/projects/go-lib/src/lib/components/go-search/go-search.module.ts new file mode 100644 index 000000000..174a88911 --- /dev/null +++ b/projects/go-lib/src/lib/components/go-search/go-search.module.ts @@ -0,0 +1,25 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ReactiveFormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { GoIconModule } from '../go-icon/go-icon.module'; +import { GoLoaderModule } from '../go-loader/go-loader.module'; + +import { GoSearchComponent } from './go-search.component'; + +@NgModule({ + declarations: [GoSearchComponent], + imports: [ + BrowserAnimationsModule, + BrowserModule, + CommonModule, + GoIconModule, + GoLoaderModule, + ReactiveFormsModule + ], + exports: [GoSearchComponent] +}) + +export class GoSearchModule { } diff --git a/projects/go-lib/src/lib/components/go-search/go-search.service.ts b/projects/go-lib/src/lib/components/go-search/go-search.service.ts new file mode 100644 index 000000000..97528cdd5 --- /dev/null +++ b/projects/go-lib/src/lib/components/go-search/go-search.service.ts @@ -0,0 +1,67 @@ +import { Injectable } from '@angular/core'; + +import { Subject } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class GoSearchService { + + /** + * Whether or not the service making requests returned results + */ + hasResults: boolean = false; + + /** + * The message to be shown when no results are returned from the server that match the search term + */ + noResultsMessage: string = 'No Results Found'; + + /** + * Whether or not to show the noResultsMessage + */ + showNoResultsMessage: boolean = false; + + /** + * Whether or not to show the loader in the search bar + */ + showLoader: boolean = false; + + /** + * The term entered by the user to search + */ + searchTerm: Subject = new Subject(); + + /** + * Minimum number of characters to trigger a search + */ + termLength: number = 3; + + /** + * Use this method to update the search term + * @param term {string} The search term entered by the user + */ + updateSearchTerm(term: string): void { + this.searchTerm.next(term); + } + + /** + * Use this method when you get a response from + * the server that was successful, **with results** + */ + successResponse(): void { + this.hasResults = true; + this.showLoader = false; + this.showNoResultsMessage = false; + } + + /** + * Use this method when you get a response from + * the server that was successful, but **with no results** + */ + notFoundResponse(): void { + this.hasResults = false; + this.showLoader = false; + this.showNoResultsMessage = true; + } +} diff --git a/projects/go-lib/src/lib/go-shared.module.ts b/projects/go-lib/src/lib/go-shared.module.ts index 75643cd45..6f1a220a5 100644 --- a/projects/go-lib/src/lib/go-shared.module.ts +++ b/projects/go-lib/src/lib/go-shared.module.ts @@ -5,6 +5,7 @@ import { GoCardModule } from './components/go-card/go-card.module'; import { GoIconModule } from './components/go-icon/go-icon.module'; import { GoLoaderModule } from './components/go-loader/go-loader.module'; import { GoModalModule } from './components/go-modal/go-modal.module'; +import { GoSearchModule } from './components/go-search/go-search.module'; import { GoSideNavModule } from './components/go-side-nav/go-side-nav.module'; import { GoTableModule } from './components/go-table/go-table.module'; import { GoToastModule } from './components/go-toast/go-toast.module'; @@ -18,6 +19,7 @@ import { GoToasterModule } from './components/go-toaster/go-toaster.module'; GoIconModule, GoLoaderModule, GoModalModule, + GoSearchModule, GoSideNavModule, GoTableModule, GoToastModule, @@ -30,6 +32,7 @@ import { GoToasterModule } from './components/go-toaster/go-toaster.module'; GoIconModule, GoLoaderModule, GoModalModule, + GoSearchModule, GoSideNavModule, GoTableModule, GoToastModule, diff --git a/projects/go-lib/src/public_api.ts b/projects/go-lib/src/public_api.ts index e160528fe..455a1be17 100644 --- a/projects/go-lib/src/public_api.ts +++ b/projects/go-lib/src/public_api.ts @@ -36,6 +36,11 @@ export * from './lib/components/go-off-canvas/go-off-canvas.component'; export * from './lib/components/go-off-canvas/go-off-canvas.module'; export * from './lib/components/go-off-canvas/go-off-canvas.service'; +// Search +export * from './lib/components/go-search/go-search.component'; +export * from './lib/components/go-search/go-search.module'; +export * from './lib/components/go-search/go-search.service'; + // Side Nav export * from './lib/components/go-side-nav/go-side-nav.module'; export * from './lib/components/go-side-nav/nav-group.model'; diff --git a/projects/go-tester/src/app/app.component.html b/projects/go-tester/src/app/app.component.html index 1dc558219..57ea07478 100644 --- a/projects/go-tester/src/app/app.component.html +++ b/projects/go-tester/src/app/app.component.html @@ -16,6 +16,15 @@

+
+

Search

+
+ + + +
+
+

Loader

diff --git a/projects/go-tester/src/app/app.component.ts b/projects/go-tester/src/app/app.component.ts index 874af89c7..abdadc835 100644 --- a/projects/go-tester/src/app/app.component.ts +++ b/projects/go-tester/src/app/app.component.ts @@ -67,13 +67,13 @@ export class AppComponent implements OnInit { totalCount: data.totalCount }); this.tableLoading = false; - }) + }); - setTimeout(() => { - this.goToasterService.toastInfo({ message: 'Check this out'}); - this.goToasterService.toastSuccess({message: 'Check this out' }); - this.goToasterService.toastError({ message: 'Check this out' }); - }, 1500); + // setTimeout(() => { + // this.goToasterService.toastInfo({ message: 'Check this out'}); + // this.goToasterService.toastSuccess({message: 'Check this out' }); + // this.goToasterService.toastError({ message: 'Check this out' }); + // }, 1500); } stopLoaderAnimation() { @@ -92,7 +92,7 @@ export class AppComponent implements OnInit { setTimeout(() => { currentTableConfig.tableData = data.results; currentTableConfig.totalCount = data.totalCount; - + this.tableConfig = currentTableConfig; this.tableLoading = false; }, 2000); diff --git a/projects/go-tester/src/app/app.module.ts b/projects/go-tester/src/app/app.module.ts index c58a8dd2f..5a30b9791 100644 --- a/projects/go-tester/src/app/app.module.ts +++ b/projects/go-tester/src/app/app.module.ts @@ -12,20 +12,22 @@ import { GoIconModule, GoLoaderModule, GoOffCanvasModule, + GoSearchModule, GoSideNavModule, GoTableModule, - GoToastModule, - GoToasterModule + GoToasterModule, + GoToastModule } from '../../../go-lib/src/public_api'; import { AppComponent } from './app.component'; import { AppService } from './app.service'; - +import { SearchTestComponent } from './components/search-test.component'; @NgModule({ declarations: [ AppComponent, - DummyComponent + DummyComponent, + SearchTestComponent ], imports: [ BrowserModule, @@ -35,6 +37,7 @@ import { AppService } from './app.service'; GoIconModule, GoLoaderModule, GoOffCanvasModule, + GoSearchModule, GoSideNavModule, GoTableModule, GoToastModule, diff --git a/projects/go-tester/src/app/app.service.ts b/projects/go-tester/src/app/app.service.ts index 5684618c5..f33790506 100644 --- a/projects/go-tester/src/app/app.service.ts +++ b/projects/go-tester/src/app/app.service.ts @@ -31,6 +31,14 @@ export class AppService { })); } + getMockSearch(term: string) { + return this.http.get("../assets/MOCK_DATA_1000.json").pipe(map(data => { + return data.filter(item => { + return item.id.toString().includes(term) || item.name.first.includes(term) || item.name.last.includes(term) || item.email.includes(term); + }) + })) + } + /***** Private Methods *****/ private paginateData(paging: GoTablePageConfig, results: any[]) : any[] { return results.slice(paging.offset, paging.offset + paging.perPage); diff --git a/projects/go-tester/src/app/components/search-test.component.html b/projects/go-tester/src/app/components/search-test.component.html new file mode 100644 index 000000000..e60e961ed --- /dev/null +++ b/projects/go-tester/src/app/components/search-test.component.html @@ -0,0 +1,10 @@ + + + + + + + + + +
{{ item.id }}{{ item.name.first }}{{ item.name.last }}{{ item.email }}{{ item.gender }}{{ item.ip_address }}
\ No newline at end of file diff --git a/projects/go-tester/src/app/components/search-test.component.ts b/projects/go-tester/src/app/components/search-test.component.ts new file mode 100644 index 000000000..2088b225d --- /dev/null +++ b/projects/go-tester/src/app/components/search-test.component.ts @@ -0,0 +1,39 @@ +import { Component, OnInit } from '@angular/core'; + +import { GoSearchService } from '../../../../go-lib/src/public_api'; + +import { AppService } from '../app.service'; + +@Component({ + selector: 'app-search-test', + templateUrl: './search-test.component.html' +}) +export class SearchTestComponent implements OnInit { + + results: any[]; + + constructor( + private searchService: GoSearchService, + private appService: AppService + ) { } + + ngOnInit() { + this.searchService.searchTerm.subscribe(searchTerm => { + // this section is dependent upon what the data looks like + // the loader and hasResults should be updated accordingly + this.appService + .getMockSearch(searchTerm) + .subscribe(results => { + setTimeout(() => { + if (results.length === 0) { + this.results = null; + this.searchService.notFoundResponse(); + } else { + this.results = results; + this.searchService.successResponse(); + } + }, 1000); + }); + }); + } +}