-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #358 from PermanentOrg/PER-9446-form-input
PER-9446-form input
- Loading branch information
Showing
40 changed files
with
710 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,23 @@ | ||
/* @format */ | ||
import { NgModule } from '@angular/core'; | ||
import { CommonModule } from '@angular/common'; | ||
import { ToggleComponent } from './components/toggle/toggle.component'; | ||
import { | ||
FaIconLibrary, | ||
FontAwesomeModule, | ||
} from '@fortawesome/angular-fontawesome'; | ||
import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons'; | ||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; | ||
import { FormInputComponent } from './components/form-input/form-input.component'; | ||
import { ButtonComponent } from './components/button/button.component'; | ||
import { ToggleComponent } from './components/toggle/toggle.component'; | ||
|
||
@NgModule({ | ||
declarations: [ToggleComponent, ButtonComponent], | ||
imports: [CommonModule], | ||
exports: [ToggleComponent, ButtonComponent], | ||
declarations: [FormInputComponent, ToggleComponent, ButtonComponent], | ||
imports: [CommonModule, FontAwesomeModule, FormsModule, ReactiveFormsModule], | ||
exports: [FormInputComponent], | ||
}) | ||
export class ComponentsModule {} | ||
export class ComponentsModule { | ||
constructor(private library: FaIconLibrary) { | ||
library.addIcons(faExclamationCircle); | ||
} | ||
} |
32 changes: 32 additions & 0 deletions
32
src/app/component-library/components/form-input/form-input.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<!-- @format --> | ||
<div | ||
class="input-vertical-label" | ||
[ngClass]="[ | ||
'input-vertical-label-' + variant, | ||
isLabelHidden() ? 'hidden' : '' | ||
]" | ||
> | ||
{{ placeholder }} | ||
</div> | ||
<fa-icon | ||
*ngIf="errors" | ||
class="error-icon" | ||
[icon]="['fas', 'exclamation-circle']" | ||
></fa-icon> | ||
<input | ||
[type]="type" | ||
(input)="onInputChange($event.target.value)" | ||
class="form-control form-input" | ||
[ngClass]="[ | ||
'form-input-' + variant, | ||
config && config.validateDirty ? 'validate-dirty' : '', | ||
errors ? 'form-input-errors' : '' | ||
]" | ||
[id]="fieldName" | ||
[name]="fieldName" | ||
[placeholder]="placeholder" | ||
[formControl]="control" | ||
/> | ||
<div class="input-vertical-error" [ngClass]="{ hidden: !errors }"> | ||
{{ errors }} | ||
</div> |
93 changes: 93 additions & 0 deletions
93
src/app/component-library/components/form-input/form-input.component.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
/* @format */ | ||
@import 'fonts'; | ||
@import 'variables'; | ||
|
||
:host { | ||
position: relative; | ||
} | ||
|
||
.form-input { | ||
border-radius: 12px; | ||
font-family: 'UsualRegular', sans-serif; | ||
font-size: 14px; | ||
line-height: 24px; | ||
font-weight: 600; | ||
border: 1px solid $form-border; | ||
padding: 12px; | ||
height: 48px; | ||
|
||
&-light.form-input-errors { | ||
background-color: $form-background-error; | ||
border: 1px solid $form-border-error; | ||
} | ||
|
||
&:focus { | ||
box-shadow: none; | ||
background-color: $form-background-focus; | ||
border: 1px solid $form-border-focus; | ||
} | ||
|
||
&::placeholder { | ||
text-transform: uppercase; | ||
font-family: 'UsualRegular', sans-serif; | ||
line-height: 16px; | ||
letter-spacing: 16%; | ||
font-weight: 400; | ||
font-size: 10px; | ||
} | ||
|
||
&-default { | ||
color: $form-default-text-color; | ||
} | ||
|
||
&-dark { | ||
background-color: $form-background-dark; | ||
opacity: 0.8; | ||
color: white; | ||
border: 1px solid transparent; | ||
&:focus { | ||
background-color: $form-background-dark !important; | ||
opacity: 1; | ||
border: 1px solid $form-border; | ||
} | ||
&::placeholder { | ||
color: white; | ||
} | ||
} | ||
} | ||
|
||
.input-vertical-label { | ||
position: absolute; | ||
font-family: 'UsualRegular', sans-serif; | ||
font-size: 10px; | ||
font-weight: 400; | ||
text-transform: uppercase; | ||
left: 12px; | ||
top: 4px; | ||
&-dark { | ||
color: $form-label-dark; | ||
} | ||
} | ||
|
||
.hidden { | ||
display: none; | ||
} | ||
|
||
.input-vertical-error { | ||
font-family: 'UsualRegular', sans-serif; | ||
font-size: 12px; | ||
font-weight: 400; | ||
line-height: 16px; | ||
letter-spacing: 0em; | ||
text-align: left; | ||
color: $form-error-red; | ||
margin-left: 15px; | ||
margin-top: 5px; | ||
} | ||
|
||
.error-icon { | ||
position: absolute; | ||
right: 20px; | ||
top: 12px; | ||
color: $form-error-red; | ||
} |
161 changes: 161 additions & 0 deletions
161
src/app/component-library/components/form-input/form-input.component.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
/* @format */ | ||
import { Shallow } from 'shallow-render'; | ||
import { ReactiveFormsModule, UntypedFormControl } from '@angular/forms'; | ||
import { ComponentsModule } from '../../components.module'; | ||
import { FormInputComponent, FormInputConfig } from './form-input.component'; | ||
|
||
type AttrOnOff = 'on' | 'off'; | ||
|
||
describe('FormInputComponent', () => { | ||
let shallow: Shallow<FormInputComponent>; | ||
|
||
beforeEach(async () => { | ||
shallow = new Shallow(FormInputComponent, ComponentsModule).import( | ||
ReactiveFormsModule | ||
); | ||
}); | ||
|
||
it('should create with an empty control', async () => { | ||
const mockControl = new UntypedFormControl(''); | ||
const { instance } = await shallow.render({ | ||
bind: { control: mockControl }, | ||
}); | ||
|
||
expect(instance).toBeTruthy(); | ||
}); | ||
|
||
it('should create with a control', async () => { | ||
const mockControl = new UntypedFormControl('input'); | ||
const { instance } = await shallow.render({ | ||
bind: { control: mockControl }, | ||
}); | ||
|
||
expect(instance).toBeTruthy(); | ||
}); | ||
|
||
it('should bind the input type with empty form control', async () => { | ||
const mockControl = new UntypedFormControl(''); | ||
const { find } = await shallow.render({ | ||
bind: { type: 'password', control: mockControl }, | ||
}); | ||
|
||
const inputElement = find('input').nativeElement; | ||
|
||
expect(inputElement.type).toBe('password'); | ||
}); | ||
|
||
it('should bind the input type with form control', async () => { | ||
const control = new UntypedFormControl('input'); | ||
const { find } = await shallow.render({ | ||
bind: { control: control, type: 'password' }, | ||
}); | ||
|
||
const inputElement = find('input').nativeElement; | ||
|
||
expect(inputElement.type).toBe('password'); | ||
}); | ||
|
||
it('should hide label for empty number inputs', async () => { | ||
const mockControl = new UntypedFormControl(''); | ||
|
||
const { instance } = await shallow.render({ | ||
bind: { type: 'number', control: mockControl }, | ||
}); | ||
|
||
expect(instance.isLabelHidden()).toBeTrue(); | ||
}); | ||
|
||
it('should show label for non-empty text inputs', async () => { | ||
const mockControl = new UntypedFormControl('Some text'); | ||
|
||
const { instance } = await shallow.render({ | ||
bind: { type: 'text', control: mockControl }, | ||
}); | ||
|
||
expect(instance.isLabelHidden()).toBeFalse(); | ||
}); | ||
|
||
it('should set input attributes based on config', async () => { | ||
const mockControl = new UntypedFormControl('Some text'); | ||
|
||
const config: FormInputConfig = { | ||
autocorrect: 'off', | ||
autocapitalize: 'off', | ||
spellcheck: 'off', | ||
}; | ||
const { find } = await shallow.render({ | ||
bind: { config, control: mockControl }, | ||
}); | ||
|
||
const inputElement = find('input').nativeElement; | ||
|
||
expect(inputElement.getAttribute('autocorrect')).toBe(config.autocorrect); | ||
|
||
expect(inputElement.getAttribute('autocapitalize')).toBe( | ||
config.autocapitalize | ||
); | ||
|
||
expect(inputElement.getAttribute('spellcheck')).toBe(config.spellcheck); | ||
}); | ||
|
||
it('should emit valueChange event on input value change', async () => { | ||
const mockControl = new UntypedFormControl(''); | ||
|
||
const mockValue = 'test value'; | ||
const { instance, find } = await shallow.render({ | ||
bind: { control: mockControl }, | ||
}); | ||
|
||
const spy = spyOn(instance.valueChangeSubject, 'next'); | ||
|
||
instance.onInputChange(mockValue); | ||
|
||
expect(spy).toHaveBeenCalledWith(mockValue); | ||
}); | ||
|
||
it('should apply right-align class based on config', async () => { | ||
const mockControl = new UntypedFormControl(''); | ||
const { instance, fixture } = await shallow.render({ | ||
bind: { config: { textAlign: 'right' }, control: mockControl }, | ||
}); | ||
fixture.detectChanges(); | ||
|
||
expect(instance.rightAlign).toBeTrue(); | ||
}); | ||
it('should bind the placeholder attribute to the input element', async () => { | ||
const mockControl = new UntypedFormControl(''); | ||
const placeholderValue = 'Enter text here'; | ||
const { find } = await shallow.render({ | ||
bind: { placeholder: placeholderValue, control: mockControl }, | ||
}); | ||
|
||
const inputElement = find('input').nativeElement; | ||
|
||
expect(inputElement.placeholder).toBe(placeholderValue); | ||
}); | ||
|
||
it('should return the correct error from the validation array', async () => { | ||
const mockControl = new UntypedFormControl(''); | ||
const { instance } = await shallow.render({ | ||
bind: { | ||
validators: [ | ||
{ | ||
validation: 'minLength', | ||
message: 'Must be at least 3 characters', | ||
value: 3, | ||
}, | ||
{ | ||
validation: 'maxLength', | ||
message: 'Must be at most 10 characters', | ||
value: 10, | ||
}, | ||
], | ||
control: mockControl, | ||
}, | ||
}); | ||
|
||
const errorMessage = instance.getInputErrorFromValue('aa'); | ||
|
||
expect(errorMessage).toBe('Must be at least 3 characters'); | ||
}); | ||
}); |
Oops, something went wrong.