Skip to content
This repository has been archived by the owner on Apr 14, 2023. It is now read-only.

Commit

Permalink
merge master
Browse files Browse the repository at this point in the history
  • Loading branch information
pjlamb12 committed Aug 22, 2019
2 parents ed2669c + 4aa66be commit 9b13d31
Show file tree
Hide file tree
Showing 20 changed files with 548 additions and 71 deletions.
200 changes: 133 additions & 67 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ngx-plug-n-play",
"version": "1.0.6",
"version": "1.1.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
Expand Down
2 changes: 1 addition & 1 deletion projects/ngx-plug-n-play-lib/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ngx-plug-n-play",
"version": "1.0.6",
"version": "1.1.0",
"peerDependencies": {
"@angular/common": ">7.2.0",
"@angular/core": ">7.2.0",
Expand Down
38 changes: 38 additions & 0 deletions projects/ngx-plug-n-play-lib/src/lib/dropdown/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Dropdown Module

The `ngx-plug-n-play` Dropdown Module contains the `DropdownComponent` and a utility file. The component has the two `Inputs`, both of which default to true. They are:

## Dropdown Component API

- `hideResultsOnSelect: boolean` — If this is set to true, an event will be emitted after an item is selected telling the parent component that it's okay to hide the option list
- `closeOnOuterClick: boolean` — If this is set to true, the options list will close when the user clicks outside of the dropdown component.

There are two `Output`s for the component as well, and they are related to the `Input`s above:

- `updateShowResults: EventEmitter<Boolean>` &mdash; If the `hideResultsOnSelect` `Input` is true, or the `closeOnOuterClick` is true, then this `Output` will emit `true` so that the parent component can react accordingly.
- `dropdownItemSelected: EventEmitter<DropdownSelectedItem>` &mdash; When an item is selected, its `index` and the `textContent` of the item are emitted as an object of type `DropdownSelectedItem`

The utility file has one function in it that will be useful if you want to hide the currently selected item from the list. It's called `getRealItemFromListAfterSelection`.

- `getRealItemFromListAfterSelection: DropdownSelectedItem` &mdash; The indices of the items in the list get messed up if you are hiding the currently selected item from the list, so this function will make sure that you get the correct item from the list all the time. Use this function if you want to hide the currently selected item.

## Dropdown Component Demo

Here's an example of how to use the `DropdownComponent`:

```html
<pnp-dropdown
(updateShowResults)="showDropdownResults = $event"
[closeOnOuterClick]="true"
(dropdownItemSelected)="selectedItemUpdated($event)"
>
<button #dropdownTrigger dropdown-trigger>{{ triggerText }}</button>
<ul #dropdownOptions dropdown-options [class.open]="showDropdownResults">
<ng-template ngFor let-item [ngForOf]="dropdownItems" let-i="index">
<li *ngIf="!selectedItem || selectedItem.index !== i">{{ item }}</li>
</ng-template>
</ul>
</pnp-dropdown>
```

The `Inputs` and `Outputs` are discussed above, but the important part here to point out is that two elements need to be passed in via content projection, a dropdown trigger and a dropdown options element. The dropdown trigger needs an attribute of `dropdown-trigger` and a template variable of `#dropdownTrigger`. The dropdown options need an attribute of `dropdown-options` and a template variable of `#dropdownOptions`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface DropdownSelectedItem {
index: number;
textContent: string;
}
12 changes: 12 additions & 0 deletions projects/ngx-plug-n-play-lib/src/lib/dropdown/dropdown.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DropdownComponent } from './dropdown/dropdown.component';

export * from './dropdown.util';

@NgModule({
declarations: [DropdownComponent],
imports: [CommonModule],
exports: [DropdownComponent],
})
export class DropdownModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { getRealItemFromListAfterSelection } from './dropdown.util';

describe('DropdownUtil', () => {
it('should return the newItem if the previouslySelectedItem is falsy', () => {
const selectedItem = getRealItemFromListAfterSelection(null, { index: 0, textContent: 'Test' });
expect(selectedItem.index).toBe(0);
});

it('should return the previouslySelectedItem if the newItem is falsy', () => {
const selectedItem = getRealItemFromListAfterSelection({ index: 0, textContent: 'Test' }, null);
expect(selectedItem.index).toBe(0);
});

it('should return null if both items are falsy', () => {
const selectedItem = getRealItemFromListAfterSelection(undefined, null);
expect(selectedItem).toBeFalsy();
});

it('should return the an index of 1 if both items index is 0', () => {
const selectedItem = getRealItemFromListAfterSelection(
{ index: 0, textContent: 'Test Item 1' },
{ index: 0, textContent: 'Test Item 2' },
);
expect(selectedItem.index).toBe(1);
});

it('should return the an index of 2 if the new item has a lower index number than the previous item', () => {
const selectedItem = getRealItemFromListAfterSelection(
{ index: 2, textContent: 'Test Item 3' },
{ index: 1, textContent: 'Test Item 2' },
);
expect(selectedItem.index).toBe(1);
});
});
21 changes: 21 additions & 0 deletions projects/ngx-plug-n-play-lib/src/lib/dropdown/dropdown.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { DropdownSelectedItem } from './dropdown-selected-item.interface';

export function getRealItemFromListAfterSelection(
previouslySelectedItem: DropdownSelectedItem,
newItem: DropdownSelectedItem,
) {
if (!previouslySelectedItem && newItem) {
return newItem;
}
if (!newItem && previouslySelectedItem) {
return previouslySelectedItem;
}
if (!newItem && !previouslySelectedItem) {
return null;
}

return {
index: previouslySelectedItem.index <= newItem.index ? newItem.index + 1 : newItem.index,
textContent: newItem.textContent,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<ng-content select="[dropdown-trigger]"></ng-content>
<ng-content select="[dropdown-options]"></ng-content>
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';

import { DropdownComponent } from './dropdown.component';
import { Component, ViewChild, ElementRef } from '@angular/core';
import { By } from '@angular/platform-browser';

@Component({
selector: 'app-test-host',
template: `
<pnp-dropdown (updateShowResults)="showResults = $event">
<button #dropdownTrigger dropdown-trigger class="btn btn-primary">Dropdown Trigger</button>
<ul #dropdownOptions dropdown-options class="{{ showResults ? 'open' : '' }}">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
</ul>
</pnp-dropdown>
`,
})
class TestHostComponent {
@ViewChild(DropdownComponent) dropdownComponent: DropdownComponent;
public typeaheadDebounceTime: number = 300;
public showResults: boolean;
valueChanged(newValue: string) {}
}

describe('DropdownComponent', () => {
let component: TestHostComponent;
let fixture: ComponentFixture<TestHostComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [TestHostComponent, DropdownComponent],
}).compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(TestHostComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should toggle the showResults value', () => {
expect(component.dropdownComponent.showResults).toBeFalsy();
component.dropdownComponent.toggleShowResults();
fixture.detectChanges();
expect(component.dropdownComponent.showResults).toBeTruthy();
});

it('should not have the open class when the showResults variable is false', () => {
const options: ElementRef = fixture.debugElement.query(By.css('[dropdown-options]'));

expect(options.nativeElement.classList.contains('open')).toBeFalsy();
});

it('should have the open class when the showResults variable is true', () => {
component.dropdownComponent.showResults = true;
component.showResults = true;
fixture.detectChanges();

const options: ElementRef = fixture.debugElement.query(By.css('[dropdown-options]'));

expect(options.nativeElement.classList.contains('open')).toBeTruthy();
});

it('should listen for clicks on the options', fakeAsync(() => {
spyOn(component.dropdownComponent, 'itemSelected');
spyOn(component.dropdownComponent.dropdownItemSelected, 'emit');
component.dropdownComponent.showResults = true;
component.showResults = true;
fixture.detectChanges();

const optionsElements: any[] = fixture.debugElement.queryAll(By.css('[dropdown-options] li'));

optionsElements[0].nativeElement.click();
tick();
expect(component.dropdownComponent.itemSelected).toHaveBeenCalled();
}));

it('should output the item that is clicked on in the list', fakeAsync(() => {
spyOn(component.dropdownComponent.dropdownItemSelected, 'emit');
component.dropdownComponent.showResults = true;
component.showResults = true;
fixture.detectChanges();
const optionsElements: any[] = fixture.debugElement.queryAll(By.css('[dropdown-options] li'));
const evt: Partial<Event> = {
target: optionsElements[0].nativeElement,
};

component.dropdownComponent.itemSelected(evt);

expect(component.dropdownComponent.dropdownItemSelected.emit).toHaveBeenCalled();
expect(component.dropdownComponent.dropdownItemSelected.emit).toHaveBeenCalledWith({
index: 0,
textContent: 'Item 1',
});
}));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
Component,
ContentChild,
AfterContentInit,
OnDestroy,
ElementRef,
HostListener,
Output,
EventEmitter,
Input,
} from '@angular/core';
import { fromEvent, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { DropdownSelectedItem } from '../dropdown-selected-item.interface';

@Component({
selector: 'pnp-dropdown',
templateUrl: './dropdown.component.html',
styleUrls: ['./dropdown.component.css'],
})
export class DropdownComponent implements AfterContentInit, OnDestroy {
@ContentChild('dropdownTrigger') dropdownTrigger: ElementRef;
@ContentChild('dropdownOptions') dropdownOptions: ElementRef;
@Output() updateShowResults: EventEmitter<boolean> = new EventEmitter<boolean>();
@Output() dropdownItemSelected: EventEmitter<DropdownSelectedItem> = new EventEmitter<DropdownSelectedItem>();
@Input() hideResultsOnSelect: boolean = true;
@Input() closeOnOuterClick: boolean = true;
public showResults: boolean = false;
private destroy$: Subject<boolean> = new Subject<boolean>();
private isComponentClicked: boolean;

constructor() {}

ngOnDestroy() {
this.destroy$.next(true);
}

ngAfterContentInit() {
this.setUpButtonClickListener();
this.setUpListItemClickListener();
}

@HostListener('click')
clickInside() {
this.isComponentClicked = true;
}

@HostListener('document:click', ['$event'])
clickout(evt: Event) {
if (!this.isComponentClicked && this.showResults && this.closeOnOuterClick) {
this.toggleShowResults();
}
this.isComponentClicked = false;
}

toggleShowResults() {
this.showResults = !this.showResults;
this.updateShowResults.emit(this.showResults);
}

setUpButtonClickListener() {
fromEvent(this.dropdownTrigger.nativeElement, 'click')
.pipe(takeUntil(this.destroy$))
.subscribe(() => this.toggleShowResults());
}

setUpListItemClickListener() {
fromEvent(this.dropdownOptions.nativeElement, 'click')
.pipe(takeUntil(this.destroy$))
.subscribe((evt: any) => {
this.itemSelected(evt);
});
}

itemSelected(evt: any) {
const lisArray = Array.from(this.dropdownOptions.nativeElement.children);
const index = lisArray.indexOf(evt.target);
const textContent = evt.target.textContent;
this.dropdownItemSelected.emit({ index, textContent });
if (this.hideResultsOnSelect) {
this.toggleShowResults();
}
}
}
3 changes: 3 additions & 0 deletions projects/ngx-plug-n-play-lib/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ export * from './lib/alert-toaster/alert-toaster.service';
export * from './lib/alert-toaster/alert-toaster.module';

export * from './lib/accordion/accordion.module';
export * from './lib/dropdown/dropdown.module';
export * from './lib/dropdown/dropdown.util';
export * from './lib/dropdown/dropdown-selected-item.interface';
2 changes: 2 additions & 0 deletions src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { AlertsToasterDemoComponent } from './alerts-toaster-demo/alerts-toaster
import { HomeComponent } from './home/home.component';
import { TypeaheadDemoComponent } from './typeahead-demo/typeahead-demo.component';
import { AccordionDemoComponent } from './accordion-demo/accordion-demo.component';
import { DropdownDemoComponent } from './dropdown-demo/dropdown-demo.component';

const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'typeahead-demo', component: TypeaheadDemoComponent },
{ path: 'alerts-toaster-demo', component: AlertsToasterDemoComponent },
{ path: 'accordion-demo', component: AccordionDemoComponent },
{ path: 'dropdown-demo', component: DropdownDemoComponent },
{ path: '**', redirectTo: '', pathMatch: 'full' },
];

Expand Down
6 changes: 4 additions & 2 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {
AlertToasterModule,
TypeaheadModule,
DropdownModule,
AccordionModule,
} from '../../projects/ngx-plug-n-play-lib/src/public-api';
import { AlertsToasterDemoComponent } from './alerts-toaster-demo/alerts-toaster-demo.component';
Expand All @@ -13,16 +13,17 @@ import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { MainNavComponent } from './main-nav/main-nav.component';
import { TypeaheadDemoComponent } from './typeahead-demo/typeahead-demo.component';
import { DropdownDemoComponent } from './dropdown-demo/dropdown-demo.component';
import { AccordionDemoComponent } from './accordion-demo/accordion-demo.component';

@NgModule({
imports: [
BrowserModule,
BrowserAnimationsModule,
FormsModule,
AppRoutingModule,
TypeaheadModule,
AlertToasterModule,
DropdownModule,
AccordionModule,
],
declarations: [
Expand All @@ -31,6 +32,7 @@ import { AccordionDemoComponent } from './accordion-demo/accordion-demo.componen
HomeComponent,
TypeaheadDemoComponent,
AlertsToasterDemoComponent,
DropdownDemoComponent,
AccordionDemoComponent,
],
bootstrap: [AppComponent],
Expand Down
Loading

0 comments on commit 9b13d31

Please sign in to comment.