Skip to content

Commit 342deb3

Browse files
authored
Form changes (#1281)
* refactor: forms wip * refactor: added more changes for supporting forms * refactor: adding more changes * refactor: self review
1 parent aa91526 commit 342deb3

File tree

14 files changed

+270
-23
lines changed

14 files changed

+270
-23
lines changed

projects/components/src/button/button.component.test.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { TrackDirective } from '@hypertrace/common';
44
import { createHostFactory, Spectator } from '@ngneat/spectator/jest';
55
import { MockDirective } from 'ng-mocks';
66
import { IconSize } from '../icon/icon-size';
7-
import { ButtonRole, ButtonSize, ButtonStyle } from './button';
7+
import { ButtonRole, ButtonSize, ButtonStyle, ButtonType } from './button';
88
import { ButtonComponent } from './button.component';
99
import { ButtonModule } from './button.module';
1010

@@ -26,7 +26,22 @@ describe('Button Component', () => {
2626
}
2727
});
2828

29-
expect(spectator.query('.button')).toHaveClass('button secondary small solid');
29+
const buttonEl = spectator.query('.button');
30+
expect(buttonEl).toHaveClass('button secondary small solid');
31+
expect(buttonEl).toHaveAttribute('type', 'button');
32+
});
33+
34+
test('should set correct button type', () => {
35+
spectator = createHost(`<ht-button [label]="label" [type]="type"></ht-button>`, {
36+
hostProps: {
37+
label: 'Button',
38+
type: ButtonType.Submit
39+
}
40+
});
41+
42+
const buttonEl = spectator.query('.button');
43+
expect(buttonEl).toHaveClass('button secondary small solid');
44+
expect(buttonEl).toHaveAttribute('type', 'submit');
3045
});
3146

3247
test('should have correct style class for selected role', () => {

projects/components/src/button/button.component.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,21 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
22
import { IconType } from '@hypertrace/assets-library';
33
import { isNil } from 'lodash-es';
44
import { IconSize } from '../icon/icon-size';
5-
import { ButtonRole, ButtonSize, ButtonStyle } from './button';
5+
import { ButtonRole, ButtonSize, ButtonStyle, ButtonType } from './button';
66

77
@Component({
88
selector: 'ht-button',
99
styleUrls: ['./button.component.scss'],
1010
changeDetection: ChangeDetectionStrategy.OnPush,
1111
template: `
1212
<ht-event-blocker event="click" class="button-container" [enabled]="this.disabled">
13-
<button class="button" [ngClass]="this.getStyleClasses()" [htTrack] [htTrackLabel]="this.label">
13+
<button
14+
class="button"
15+
[ngClass]="this.getStyleClasses()"
16+
[htTrack]
17+
[htTrackLabel]="this.label"
18+
[type]="this.type"
19+
>
1420
<ht-icon
1521
*ngIf="this.icon && !this.trailingIcon"
1622
[icon]="this.icon"
@@ -45,6 +51,9 @@ export class ButtonComponent {
4551
@Input()
4652
public trailingIcon?: boolean;
4753

54+
@Input()
55+
public type: ButtonType = ButtonType.Button;
56+
4857
@Input()
4958
public role: ButtonRole = ButtonRole.Secondary;
5059

projects/components/src/button/button.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,8 @@ export const enum ButtonStyle {
2626
Text = 'text', // No No Yes No Yes
2727
PlainText = 'plain-text' // No No Yes No No
2828
}
29+
30+
export const enum ButtonType {
31+
Button = 'button',
32+
Submit = 'submit'
33+
}

projects/components/src/form-field/form-field.component.scss

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
@import 'mixins';
22

33
.form-field {
4+
display: flex;
5+
flex-direction: column;
6+
47
&.optional {
58
margin-bottom: 18px;
69
}
@@ -26,6 +29,31 @@
2629
}
2730
}
2831

32+
.content {
33+
flex: 1 1 auto;
34+
width: 100%;
35+
border: 1px solid $gray-2;
36+
border-radius: 6px;
37+
width: 100%;
38+
background: white;
39+
40+
&.error-border {
41+
border: 1px solid $red-3;
42+
}
43+
}
44+
45+
.error {
46+
display: flex;
47+
align-items: center;
48+
color: $red-6;
49+
margin-top: 4px;
50+
51+
.error-label {
52+
@include footnote($red-6);
53+
margin-left: 4px;
54+
}
55+
}
56+
2957
.error-message {
3058
@include footnote($red-6);
3159
}

projects/components/src/form-field/form-field.component.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,32 @@ describe('Form Field Component', () => {
5454
expect(labels[0].label).toEqual('Label');
5555
expect(labels[1].label).toEqual('(Optional)');
5656
});
57+
58+
test('should show error when showFormError and errorLabel is present', () => {
59+
const spectator = hostFactory(
60+
`
61+
<ht-form-field [label]="label" [showFormError]="showFormError" [errorLabel]="errorLabel">
62+
</ht-form-field>`,
63+
{
64+
hostProps: {
65+
label: 'Label',
66+
errorLabel: 'Invalid Form element',
67+
showFormError: true
68+
}
69+
}
70+
);
71+
72+
expect(spectator.query('.error')).toExist();
73+
74+
const labels = spectator.queryAll(LabelComponent);
75+
expect(labels[0].label).toEqual('Label');
76+
// Error label is shown when showFormError is true
77+
expect(labels[1].label).toEqual('Invalid Form element');
78+
79+
spectator.setHostInput({
80+
showFormError: false
81+
});
82+
83+
expect(spectator.query('.error')).not.toExist();
84+
});
5785
});

projects/components/src/form-field/form-field.component.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
2+
import { IconType } from '@hypertrace/assets-library';
23
import { IconSize } from '../icon/icon-size';
34

45
@Component({
@@ -18,8 +19,21 @@ import { IconSize } from '../icon/icon-size';
1819
[htTooltip]="this.iconTooltip"
1920
></ht-icon>
2021
</div>
21-
<ng-content></ng-content>
22-
<ht-label *ngIf="!this.isOptional" class="error-message" [label]="this.errorLabel"></ht-label>
22+
<div class="content" [ngClass]="{ 'error-border': this.showFormError && this.errorLabel }">
23+
<ng-content></ng-content>
24+
</div>
25+
<!-- For Backward Compatibility: Start -->
26+
<ht-label
27+
*ngIf="!this.isOptional && this.showFormError === undefined"
28+
class="error-message"
29+
[label]="this.errorLabel"
30+
></ht-label>
31+
<!-- For Backward Compatibility: End -->
32+
33+
<div class="error" *ngIf="this.showFormError && this.errorLabel">
34+
<ht-icon icon="${IconType.Error}" size="${IconSize.Small}"></ht-icon>
35+
<ht-label class="error-label" [label]="this.errorLabel"></ht-label>
36+
</div>
2337
</section>
2438
`
2539
})
@@ -41,4 +55,7 @@ export class FormFieldComponent {
4155

4256
@Input()
4357
public errorLabel?: string = '';
58+
59+
@Input()
60+
public showFormError?: boolean = true;
4461
}

projects/components/src/input/input.component.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
2+
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
23
import { NumberCoercer, TypedSimpleChanges } from '@hypertrace/common';
34
import { InputAppearance } from './input-appearance';
45

56
@Component({
67
selector: 'ht-input',
78
styleUrls: ['./input.component.scss'],
89
changeDetection: ChangeDetectionStrategy.OnPush,
10+
providers: [
11+
{
12+
provide: NG_VALUE_ACCESSOR,
13+
multi: true,
14+
useExisting: InputComponent
15+
}
16+
],
917
template: `
1018
<mat-form-field [ngClass]="this.getStyleClasses()" floatLabel="never">
1119
<input
@@ -20,7 +28,7 @@ import { InputAppearance } from './input-appearance';
2028
</mat-form-field>
2129
`
2230
})
23-
export class InputComponent<T extends string | number> implements OnChanges {
31+
export class InputComponent<T extends string | number> implements ControlValueAccessor, OnChanges {
2432
@Input()
2533
public placeholder?: string;
2634

@@ -52,10 +60,31 @@ export class InputComponent<T extends string | number> implements OnChanges {
5260
}
5361
}
5462

63+
private propagateControlValueChange?: (value: T | undefined) => void;
64+
private propagateControlValueChangeOnTouch?: (value: T | undefined) => void;
65+
5566
public onValueChange(value?: string): void {
5667
const coercedValue = this.coerceValueIfNeeded(value);
5768
this.value = coercedValue;
5869
this.valueChange.emit(coercedValue);
70+
this.propagateValueChangeToFormControl(coercedValue);
71+
}
72+
73+
public getStyleClasses(): string[] {
74+
return [this.appearance, this.disabled ? 'disabled' : ''];
75+
}
76+
77+
public writeValue(value?: string): void {
78+
const coercedValue = this.coerceValueIfNeeded(value);
79+
this.value = coercedValue;
80+
}
81+
82+
public registerOnChange(onChange: (value: T | undefined) => void): void {
83+
this.propagateControlValueChange = onChange;
84+
}
85+
86+
public registerOnTouched(onTouch: (value: T | undefined) => void): void {
87+
this.propagateControlValueChangeOnTouch = onTouch;
5988
}
6089

6190
private coerceValueIfNeeded(value?: string): T | undefined {
@@ -67,7 +96,8 @@ export class InputComponent<T extends string | number> implements OnChanges {
6796
}
6897
}
6998

70-
public getStyleClasses(): string[] {
71-
return [this.appearance, this.disabled ? 'disabled' : ''];
99+
private propagateValueChangeToFormControl(value: T | undefined): void {
100+
this.propagateControlValueChange?.(value);
101+
this.propagateControlValueChangeOnTouch?.(value);
72102
}
73103
}

projects/components/src/modal/modal-container.component.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ describe('Modal Container component', () => {
130130
content: TestComponent,
131131
size: ModalSize.LargeTall
132132
});
133-
checkSyles('640px', '800px');
133+
checkSyles('640px', '90vh');
134134
});
135135

136136
test('uses the requested medium-wide size', () => {

projects/components/src/modal/modal.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,18 @@ export const getModalDimensions = (modalSize: ModalSize): ModalDimension => {
3636
case ModalSize.Large:
3737
return getModalDimensionObject(640, 720);
3838
case ModalSize.LargeTall:
39-
return getModalDimensionObject(640, 800);
39+
return getModalDimensionObject(640, '90vh');
4040
case ModalSize.MediumWide:
4141
return getModalDimensionObject(840, 600);
4242
default:
4343
return getModalDimensionObject(420, 365);
4444
}
4545
};
4646

47-
const getModalDimensionObject = (width: number, height: number): ModalDimension => ({ width: width, height: height });
47+
const getModalDimensionObject = (width: number | string, height: number | string): ModalDimension => ({
48+
width: width,
49+
height: height
50+
});
4851

4952
export const MODAL_DATA = new InjectionToken<unknown>('MODAL_DATA');
5053

projects/components/src/multi-select/multi-select.component.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import {
99
Output,
1010
QueryList
1111
} from '@angular/core';
12+
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
1213
import { IconType } from '@hypertrace/assets-library';
13-
import { queryListAndChanges$ } from '@hypertrace/common';
14+
import { queryListAndChanges$, SubscriptionLifecycle } from '@hypertrace/common';
15+
import { isEqual } from 'lodash-es';
1416
import { BehaviorSubject, combineLatest, EMPTY, Observable, of, Subject } from 'rxjs';
1517
import { map } from 'rxjs/operators';
1618
import { ButtonRole, ButtonStyle } from '../button/button';
@@ -19,11 +21,18 @@ import { SearchBoxDisplayMode } from '../search-box/search-box.component';
1921
import { SelectOptionComponent } from '../select/select-option.component';
2022
import { SelectSize } from '../select/select-size';
2123
import { MultiSelectJustify } from './multi-select-justify';
22-
2324
@Component({
2425
selector: 'ht-multi-select',
2526
styleUrls: ['./multi-select.component.scss'],
2627
changeDetection: ChangeDetectionStrategy.OnPush,
28+
providers: [
29+
SubscriptionLifecycle,
30+
{
31+
provide: NG_VALUE_ACCESSOR,
32+
useExisting: MultiSelectComponent,
33+
multi: true
34+
}
35+
],
2736
template: `
2837
<div
2938
class="multi-select"
@@ -120,7 +129,7 @@ import { MultiSelectJustify } from './multi-select-justify';
120129
</div>
121130
`
122131
})
123-
export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
132+
export class MultiSelectComponent<V> implements ControlValueAccessor, AfterContentInit, OnChanges {
124133
@Input()
125134
public size: SelectSize = SelectSize.Medium;
126135

@@ -167,6 +176,9 @@ export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
167176
public popoverOpen: boolean = false;
168177
public triggerValues$: Observable<TriggerValues> = new Observable();
169178

179+
private propagateControlValueChange?: (value: V[] | undefined) => void;
180+
private propagateControlValueChangeOnTouch?: (value: V[] | undefined) => void;
181+
170182
public ngAfterContentInit(): void {
171183
this.allOptions$ = this.allOptionsList !== undefined ? queryListAndChanges$(this.allOptionsList) : EMPTY;
172184
this.filteredOptions$ = combineLatest([this.allOptions$, this.searchSubject]).pipe(
@@ -215,24 +227,37 @@ export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
215227
}
216228

217229
const selected = this.isSelectedItem(item)
218-
? this.selected?.filter(value => value !== item.value)
230+
? this.selected?.filter(value => !isEqual(value, item.value))
219231
: (this.selected ?? []).concat(item.value);
220232

221233
this.setSelection(selected ?? []);
222234
}
223235

224236
public isSelectedItem(item: SelectOptionComponent<V>): boolean {
225-
return this.selected !== undefined && this.selected.filter(value => value === item.value).length > 0;
237+
return this.selected !== undefined && this.selected.filter(value => isEqual(value, item.value)).length > 0;
226238
}
227239

228240
public preventClickDefault(event: Event): void {
229241
event.preventDefault();
230242
}
231243

244+
public writeValue(value?: V[]): void {
245+
this.setSelection(value ?? []);
246+
}
247+
248+
public registerOnChange(onChange: (value: V[] | undefined) => void): void {
249+
this.propagateControlValueChange = onChange;
250+
}
251+
252+
public registerOnTouched(onTouch: (value: V[] | undefined) => void): void {
253+
this.propagateControlValueChangeOnTouch = onTouch;
254+
}
255+
232256
private setSelection(selected: V[]): void {
233257
this.selected = selected;
234258
this.setTriggerLabel();
235259
this.selectedChange.emit(this.selected);
260+
this.propagateValueChangeToFormControl(this.selected);
236261
}
237262

238263
private setTriggerLabel(): void {
@@ -256,6 +281,11 @@ export class MultiSelectComponent<V> implements AfterContentInit, OnChanges {
256281
})
257282
);
258283
}
284+
285+
private propagateValueChangeToFormControl(value: V[] | undefined): void {
286+
this.propagateControlValueChange?.(value);
287+
this.propagateControlValueChangeOnTouch?.(value);
288+
}
259289
}
260290

261291
interface TriggerValues {

0 commit comments

Comments
 (0)