Skip to content

feat: generic number input component with min-max support #879

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const enum NumberInputAppearance {
Underline = 'underline',
Border = 'border'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
@import 'font';
@import 'color-palette';

.number-input {
@include body-2-regular($gray-9);
width: inherit;
height: inherit;
background: white;
text-align: center;
}

.border {
border: 1px solid $gray-2;
border-radius: 6px;
}

.underline {
border: none;
border-bottom: 1px solid $gray-2;
}

input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}

/* Firefox */
input[type='number'] {
-moz-appearance: textfield;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { fakeAsync } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { createHostFactory } from '@ngneat/spectator/jest';
import { NumberInputAppearance } from './number-input-appearance';
import { NumberInputComponent } from './number-input.component';

describe('Number Input Component', () => {
const hostFactory = createHostFactory({
component: NumberInputComponent,
imports: [FormsModule],
shallow: true
});

test('should apply disabled attribute when disabled', fakeAsync(() => {
const spectator = hostFactory(`
<ht-number-input [disabled]="true">
</ht-number-input>`);

spectator.tick();
expect(spectator.query('input')!.getAttributeNames().includes('disabled')).toBe(true);
}));

test('should apply border style correctly', fakeAsync(() => {
const spectator = hostFactory(
`
<ht-number-input [appearance]="appearance">
</ht-number-input>`,
{
hostProps: {
appearance: NumberInputAppearance.Border
}
}
);

spectator.tick();
expect(spectator.query('input')?.classList).toContain('border');
}));

test('should apply underline style correctly', fakeAsync(() => {
const spectator = hostFactory(
`
<ht-number-input [appearance]="appearance">
</ht-number-input>`,
{
hostProps: {
appearance: NumberInputAppearance.Underline
}
}
);

spectator.tick();
expect(spectator.query('input')?.classList).toContain('underline');
}));

test('should emit correct values on value change', fakeAsync(() => {
const spectator = hostFactory(`
<ht-number-input minValue="1" maxValue="10">
</ht-number-input>`);

spectator.tick();
spyOn(spectator.component.valueChange, 'emit');
spectator.triggerEventHandler('input', 'ngModelChange', 7);
expect(spectator.component.valueChange.emit).toHaveBeenCalledWith(7);

spectator.triggerEventHandler('input', 'ngModelChange', 15);
expect(spectator.component.valueChange.emit).toHaveBeenCalledWith(10);

spectator.triggerEventHandler('input', 'ngModelChange', -1);
expect(spectator.component.valueChange.emit).toHaveBeenCalledWith(1);
}));
});
58 changes: 58 additions & 0 deletions projects/components/src/number-input/number-input.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { NumberCoercer } from '@hypertrace/common';
import { NumberInputAppearance } from './number-input-appearance';

@Component({
selector: 'ht-number-input',
styleUrls: ['./number-input.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<input
type="number"
class="number-input"
[ngClass]="this.appearance"
[disabled]="this.disabled"
[ngModel]="this.value"
(ngModelChange)="this.onValueChange($event)"
/>
`
})
export class NumberInputComponent {
@Input()
public value?: number;

@Input()
public appearance: NumberInputAppearance = NumberInputAppearance.Border;

@Input()
public disabled: boolean = false;

@Input()
public minValue?: number;

@Input()
public maxValue?: number;

@Output()
public readonly valueChange: EventEmitter<number> = new EventEmitter();

private readonly numberCoercer: NumberCoercer = new NumberCoercer();

private enforceMinMaxAndEmit(): void {
if (this.value !== undefined && this.maxValue !== undefined && this.value > this.maxValue) {
this.value = this.maxValue;
}

if (this.value !== undefined && this.minValue !== undefined && this.value < this.minValue) {
this.value = this.minValue;
}

this.valueChange.emit(this.numberCoercer.coerce(this.value));
}

public onValueChange(value?: number): void {
this.value = value;

this.enforceMinMaxAndEmit();
}
}
11 changes: 11 additions & 0 deletions projects/components/src/number-input/number-input.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { NumberInputComponent } from './number-input.component';

@NgModule({
imports: [CommonModule, FormsModule],
declarations: [NumberInputComponent],
exports: [NumberInputComponent]
})
export class NumberInputModule {}
5 changes: 5 additions & 0 deletions projects/components/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ export { InputAppearance } from './input/input-appearance';
export * from './input/input.component';
export * from './input/input.module';

// Number Input
export { NumberInputAppearance } from './number-input/number-input-appearance';
export * from './number-input/number-input.component';
export * from './number-input/number-input.module';

// Json Tree
export { JsonViewerComponent } from './viewer/json-viewer/json-viewer.component';
export { JsonViewerModule } from './viewer/json-viewer/json-viewer.module';
Expand Down