Skip to content

Commit

Permalink
feat: add counter input component (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
dhhyi authored and shauke committed Jan 10, 2020
1 parent 25ce59a commit a7f5aee
Show file tree
Hide file tree
Showing 10 changed files with 216 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
<div *ngIf="product.inStock && product.availability" [formGroup]="parentForm" class="product-quantity">
<ng-container *ngIf="!readOnly">
<ng-container *ngIf="type === 'select'; else inputType">
<ng-container [ngSwitch]="type">
<ish-select
*ngSwitchCase="'select'"
[options]="quantityOptions"
[form]="parentForm"
[controlName]="controlName"
[label]="quantityLabel"
[labelClass]="labelClass"
[inputClass]="inputClass"
></ish-select>
</ng-container>

<ng-template #inputType>
<ish-counter
*ngSwitchCase="'counter'"
[form]="parentForm"
[controlName]="controlName"
[label]="quantityLabel"
[labelClass]="labelClass"
[inputClass]="inputClass"
[min]="allowZeroQuantity ? 0 : product.minOrderQuantity"
[max]="product.maxOrderQuantity"
></ish-counter>
<ish-input
*ngSwitchDefault
[type]="'number'"
[form]="parentForm"
[controlName]="controlName"
Expand All @@ -29,7 +38,7 @@
max: 'product.quantity.lessthan.text' | translate: { '0': product.maxOrderQuantity }
}"
></ish-input>
</ng-template>
</ng-container>
</ng-container>
<ng-container *ngIf="readOnly">
<span>{{ quantityLabel | translate }}: {{ quantity }}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MockComponent } from 'ng-mocks';

import { Product } from 'ish-core/models/product/product.model';
import { findAllIshElements } from 'ish-core/utils/dev/html-query-utils';
import { CounterComponent } from 'ish-shared/forms/components/counter/counter.component';
import { InputComponent } from 'ish-shared/forms/components/input/input.component';
import { SelectComponent } from 'ish-shared/forms/components/select/select.component';

Expand All @@ -19,7 +20,12 @@ describe('Product Quantity Component', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ReactiveFormsModule, TranslateModule.forRoot()],
declarations: [MockComponent(InputComponent), MockComponent(SelectComponent), ProductQuantityComponent],
declarations: [
MockComponent(CounterComponent),
MockComponent(InputComponent),
MockComponent(SelectComponent),
ProductQuantityComponent,
],
}).compileComponents();
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class ProductQuantityComponent implements OnInit, OnChanges {
@Input() product: Product;
@Input() parentForm: FormGroup;
@Input() controlName: string;
@Input() type?: 'select' | 'input';
@Input() type?: 'input' | 'select' | 'counter';
@Input() class?: string;

quantityOptions: SelectOption[];
Expand All @@ -56,7 +56,7 @@ export class ProductQuantityComponent implements OnInit, OnChanges {
}

getValidations(): ValidatorFn {
if (this.type !== 'select') {
if (this.type === 'input') {
return Validators.compose([
Validators.required,
Validators.min(this.allowZeroQuantity ? 0 : this.product.minOrderQuantity),
Expand Down
28 changes: 28 additions & 0 deletions src/app/shared/forms/components/counter/counter.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<div class="row form-group" [formGroup]="form">
<label *ngIf="displayLabel" [for]="uuid" class="col-form-label" [ngClass]="labelClass"
>{{ label | translate }}<span *ngIf="required" class="required">*</span>
</label>
<div [ngClass]="inputClass">
<div class="form-control p-0">
<div class="d-flex flex-row align-items-center justify-content-around">
<!-- display: inline -->
<button
class="btn btn-link mr-0"
(click)="decrease()"
[disabled]="cannotDecrease$ | async"
[attr.data-testing-id]="'decrease-' + controlName"
>{{ 'product.quantity.decrease.text' | translate }}</button
>
<span class="text-center w-100" [attr.data-testing-id]="controlName">{{ value$ | async }}</span>
<!-- display: inline -->
<button
class="btn btn-link mr-0"
(click)="increase()"
[disabled]="cannotIncrease$ | async"
[attr.data-testing-id]="'increase-' + controlName"
>{{ 'product.quantity.increase.text' | translate }}</button
>
</div>
</div>
</div>
</div>
97 changes: 97 additions & 0 deletions src/app/shared/forms/components/counter/counter.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { TranslateModule } from '@ngx-translate/core';
import { spy, verify } from 'ts-mockito';

import { CounterComponent } from './counter.component';

describe('Counter Component', () => {
let component: CounterComponent;
let fixture: ComponentFixture<CounterComponent>;
let element: HTMLElement;

const controlName = 'quantity';

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ReactiveFormsModule, TranslateModule.forRoot()],
declarations: [CounterComponent],
}).compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;

component.form = new FormGroup({
[controlName]: new FormControl(),
});
component.controlName = controlName;
});

it('should be created', () => {
expect(component).toBeTruthy();
expect(element).toBeTruthy();
expect(() => fixture.detectChanges()).not.toThrow();
});

describe('with value', () => {
beforeEach(() => {
component.form.get(controlName).setValue(42);
fixture.detectChanges();
});

it('should display value from form when rendered', () => {
const display = element.querySelector(`[data-testing-id=${controlName}]`);
expect(display.textContent).toMatchInlineSnapshot(`""`);
});

it('should increase value when increase button was clicked', () => {
const componentSpy = spy(component);
fixture.debugElement.query(By.css(`[data-testing-id=increase-${controlName}]`)).triggerEventHandler('click', {});
fixture.detectChanges();
verify(componentSpy.increase()).once();

const display = element.querySelector(`[data-testing-id=${controlName}]`);
expect(display.textContent).toMatchInlineSnapshot(`"43"`);
});

it('should decrease value when decrease button was clicked', () => {
const componentSpy = spy(component);
fixture.debugElement.query(By.css(`[data-testing-id=decrease-${controlName}]`)).triggerEventHandler('click', {});
fixture.detectChanges();
verify(componentSpy.decrease()).once();

const display = element.querySelector(`[data-testing-id=${controlName}]`);
expect(display.textContent).toMatchInlineSnapshot(`"41"`);
});

describe('with max', () => {
beforeEach(() => {
component.max = 42;
component.ngOnChanges();
fixture.detectChanges();
});

it('should disable increase button when max is reached', () => {
const increase = element.querySelector(`[data-testing-id=increase-${controlName}]`);
expect(increase.hasAttribute('disabled')).toBeTrue();
});
});

describe('with min', () => {
beforeEach(() => {
component.min = 42;
component.ngOnChanges();
fixture.detectChanges();
});

it('should disable decrease button when min is reached', () => {
const decrease = element.querySelector(`[data-testing-id=decrease-${controlName}]`);
expect(decrease.hasAttribute('disabled')).toBeTrue();
});
});
});
});
60 changes: 60 additions & 0 deletions src/app/shared/forms/components/counter/counter.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { Observable, ReplaySubject, Subject } from 'rxjs';
import { map, takeUntil } from 'rxjs/operators';

import { FormElementComponent } from 'ish-shared/forms/components/form-element/form-element.component';

@Component({
selector: 'ish-counter',
templateUrl: './counter.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CounterComponent extends FormElementComponent implements OnInit, OnDestroy, OnChanges {
@Input() min: number;
@Input() max: number;

value$ = new ReplaySubject<number>(1);
cannotDecrease$: Observable<boolean>;
cannotIncrease$: Observable<boolean>;

private destroy$ = new Subject();

constructor(protected translate: TranslateService) {
super(translate);
}

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

private get value(): number {
return +this.formControl.value;
}

ngOnChanges() {
this.value$.next(this.value);
}

ngOnInit() {
super.init();

this.cannotDecrease$ = this.value$.pipe(map(value => this.min !== undefined && value <= this.min));
this.cannotIncrease$ = this.value$.pipe(map(value => this.max !== undefined && value >= this.max));

this.formControl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(this.value$);
}

increase() {
(this.formControl as FormControl).setValue(this.value + 1, { emitEvent: true });
}

decrease() {
(this.formControl as FormControl).setValue(this.value - 1, { emitEvent: true });
}

get displayLabel(): boolean {
return !!this.label && !!this.label.trim();
}
}
2 changes: 2 additions & 0 deletions src/app/shared/forms/forms.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { CaptchaV2Component } from './components/captcha-v2/captcha-v2.component
import { CaptchaV3Component } from './components/captcha-v3/captcha-v3.component';
import { CaptchaComponent } from './components/captcha/captcha.component';
import { CheckboxComponent } from './components/checkbox/checkbox.component';
import { CounterComponent } from './components/counter/counter.component';
import { FormControlFeedbackComponent } from './components/form-control-feedback/form-control-feedback.component';
import { InputBirthdayComponent } from './components/input-birthday/input-birthday.component';
import { InputComponent } from './components/input/input.component';
Expand All @@ -32,6 +33,7 @@ const declaredComponents = [CaptchaV2Component, CaptchaV3Component];
const exportedComponents = [
CaptchaComponent,
CheckboxComponent,
CounterComponent,
FormControlFeedbackComponent,
InputBirthdayComponent,
InputComponent,
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/de_DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -1985,6 +1985,8 @@
"product.quantity.lessthan.text": "Geben Sie {{0}} oder weniger ein.",
"product.quantity.message.text": "Geben Sie die Anzahl ein.",
"product.quantity.notempty.text": "Geben Sie die Anzahl ein.",
"product.quantity.increase.text": "+",
"product.quantity.decrease.text": "",
"product.rating.five.text": "5",
"product.rating.four.text": "4",
"product.rating.not_rated": "Dieses Produkt wurde noch nicht bewertet.",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1985,6 +1985,8 @@
"product.quantity.lessthan.text": "Please enter {{0}} or less.",
"product.quantity.message.text": "Please enter a number.",
"product.quantity.notempty.text": "Please enter a number.",
"product.quantity.increase.text": "+",
"product.quantity.decrease.text": "",
"product.rating.five.text": "5",
"product.rating.four.text": "4",
"product.rating.not_rated": "This product hasn''t been rated yet.",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/fr_FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -1903,6 +1903,8 @@
"product.quantity.lessthan.text": "Veuillez entrer {{0}} ou moins.",
"product.quantity.message.text": "Veuillez entrer un numéro.",
"product.quantity.notempty.text": "Veuillez entrer un numéro.",
"product.quantity.increase.text": "+",
"product.quantity.decrease.text": "",
"product.rating.five.text": "5",
"product.rating.four.text": "4",
"product.rating.not_rated": "Ce produit n'a pas encore été évalué.",
Expand Down

0 comments on commit a7f5aee

Please sign in to comment.