@gerandon/ngx-widgets 17.0.0 → 18.0.1
Sign up to get free protection for your applications and to get access to all the features.
- package/ng-package.json +7 -0
- package/package.json +9 -19
- package/src/lib/basic-chips/basic-chips.component.html +42 -0
- package/src/lib/basic-chips/basic-chips.component.scss +31 -0
- package/src/lib/basic-chips/basic-chips.component.ts +83 -0
- package/src/lib/basic-input/basic-input.component.html +47 -0
- package/src/lib/basic-input/basic-input.component.scss +26 -0
- package/src/lib/basic-input/basic-input.component.ts +41 -0
- package/src/lib/core/base-input.ts +69 -0
- package/src/lib/core/base-text-input.ts +13 -0
- package/src/lib/core/base-value-accessor.ts +80 -0
- package/src/lib/core/component-unsubscribe.ts +58 -0
- package/src/lib/select/select.component.html +39 -0
- package/src/lib/select/select.component.scss +7 -0
- package/src/lib/select/select.component.ts +70 -0
- package/src/lib/textarea-input/textarea-input.component.html +48 -0
- package/src/lib/textarea-input/textarea-input.component.scss +27 -0
- package/src/lib/textarea-input/textarea-input.component.ts +30 -0
- package/{public-api.d.ts → src/public-api.ts} +5 -0
- package/tsconfig.lib.json +14 -0
- package/tsconfig.lib.prod.json +10 -0
- package/tsconfig.spec.json +14 -0
- package/esm2022/gerandon-ngx-widgets.mjs +0 -5
- package/esm2022/lib/basic-chips/basic-chips.component.mjs +0 -80
- package/esm2022/lib/basic-input/basic-input.component.mjs +0 -41
- package/esm2022/lib/core/base-input.mjs +0 -81
- package/esm2022/lib/core/base-text-input.mjs +0 -20
- package/esm2022/lib/core/base-value-accessor.mjs +0 -63
- package/esm2022/lib/core/component-unsubscribe.mjs +0 -54
- package/esm2022/lib/select/select.component.mjs +0 -61
- package/esm2022/lib/textarea-input/textarea-input.component.mjs +0 -39
- package/esm2022/public-api.mjs +0 -12
- package/fesm2022/gerandon-ngx-widgets.mjs +0 -409
- package/fesm2022/gerandon-ngx-widgets.mjs.map +0 -1
- package/index.d.ts +0 -5
- package/lib/basic-chips/basic-chips.component.d.ts +0 -17
- package/lib/basic-input/basic-input.component.d.ts +0 -9
- package/lib/core/base-input.d.ts +0 -38
- package/lib/core/base-text-input.d.ts +0 -8
- package/lib/core/base-value-accessor.d.ts +0 -28
- package/lib/core/component-unsubscribe.d.ts +0 -8
- package/lib/select/select.component.d.ts +0 -26
- package/lib/textarea-input/textarea-input.component.d.ts +0 -7
package/ng-package.json
ADDED
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@gerandon/ngx-widgets",
|
3
|
-
"version": "
|
3
|
+
"version": "18.0.1",
|
4
4
|
"description": "Angular widget (components) collection using CVA (ControlValueAccessor)",
|
5
5
|
"keywords": [
|
6
6
|
"CVA",
|
@@ -28,29 +28,19 @@
|
|
28
28
|
"url": "https://github.com/Gerandon/ngx-widgets"
|
29
29
|
},
|
30
30
|
"peerDependencies": {
|
31
|
-
"@angular/common": "^
|
32
|
-
"@angular/core": "^
|
33
|
-
"@angular/material": "^
|
31
|
+
"@angular/common": "^18.0.0",
|
32
|
+
"@angular/core": "^18.0.0",
|
33
|
+
"@angular/material": "^18.0.0",
|
34
34
|
"lodash-es": "^4.17.21"
|
35
35
|
},
|
36
36
|
"dependencies": {
|
37
37
|
"tslib": "^2.3.0"
|
38
38
|
},
|
39
|
+
"devDependencies": {
|
40
|
+
"@types/lodash-es": "^4.17.12"
|
41
|
+
},
|
39
42
|
"publishConfig": {
|
40
43
|
"access": "public"
|
41
44
|
},
|
42
|
-
"sideEffects": false
|
43
|
-
|
44
|
-
"typings": "index.d.ts",
|
45
|
-
"exports": {
|
46
|
-
"./package.json": {
|
47
|
-
"default": "./package.json"
|
48
|
-
},
|
49
|
-
".": {
|
50
|
-
"types": "./index.d.ts",
|
51
|
-
"esm2022": "./esm2022/gerandon-ngx-widgets.mjs",
|
52
|
-
"esm": "./esm2022/gerandon-ngx-widgets.mjs",
|
53
|
-
"default": "./fesm2022/gerandon-ngx-widgets.mjs"
|
54
|
-
}
|
55
|
-
}
|
56
|
-
}
|
45
|
+
"sideEffects": false
|
46
|
+
}
|
@@ -0,0 +1,42 @@
|
|
1
|
+
<mat-form-field appearance="outline" [subscriptSizing]="subscriptSizing" [floatLabel]="floatLabel">
|
2
|
+
@if (label) {
|
3
|
+
<mat-label [class.disabled]="isDisabled">{{ label }}</mat-label>
|
4
|
+
}
|
5
|
+
<mat-chip-grid #chipGrid class="w-100">
|
6
|
+
@for(item of control.value; track item) {
|
7
|
+
<mat-chip-row (removed)="remove(item)" color="primary" highlighted>
|
8
|
+
{{ labelProperty ? item[labelProperty] : item}}
|
9
|
+
<button matChipRemove [attr.aria-label]="(labelProperty ? item[labelProperty] : item) + ' eltávolítása'">
|
10
|
+
<mat-icon>cancel</mat-icon>
|
11
|
+
</button>
|
12
|
+
</mat-chip-row>
|
13
|
+
}
|
14
|
+
<input #inputElement
|
15
|
+
matInput
|
16
|
+
[placeholder]="placeholder || label"
|
17
|
+
[matAutocomplete]="auto"
|
18
|
+
[matChipInputFor]="chipGrid"
|
19
|
+
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
20
|
+
(matChipInputTokenEnd)="!labelProperty && add($event)"/>
|
21
|
+
<mat-autocomplete #auto="matAutocomplete"
|
22
|
+
(optionSelected)="selected($event)">
|
23
|
+
@for (filterItem of asyncOptions | async; track filterItem) {
|
24
|
+
<mat-option [value]="filterItem">
|
25
|
+
{{labelProperty ? filterItem[labelProperty] : filterItem}}
|
26
|
+
</mat-option>
|
27
|
+
}
|
28
|
+
</mat-autocomplete>
|
29
|
+
</mat-chip-grid>
|
30
|
+
<input #input="ngForm" [style.display]="'none'" [formControl]="control" />
|
31
|
+
@if (control.errors?.['server']) {
|
32
|
+
<mat-error>{{ control.errors?.['server'] }}</mat-error>
|
33
|
+
} @else if (control.errors?.['required']) {
|
34
|
+
@if (validationTranslations.required) {
|
35
|
+
<mat-error>{{ validationTranslations.required }}</mat-error>
|
36
|
+
} @else {
|
37
|
+
@for (error of validatorMessagesArray; track error) {
|
38
|
+
<mat-error>{{ error.value }}</mat-error>
|
39
|
+
}
|
40
|
+
}
|
41
|
+
}
|
42
|
+
</mat-form-field>
|
@@ -0,0 +1,31 @@
|
|
1
|
+
gerandon-basic-chips {
|
2
|
+
display: block;
|
3
|
+
.mat-mdc-standard-chip {
|
4
|
+
height: 28px !important;
|
5
|
+
}
|
6
|
+
mat-form-field {
|
7
|
+
width: 100%;
|
8
|
+
.mat-mdc-text-field-wrapper {
|
9
|
+
.mat-mdc-form-field-infix {
|
10
|
+
display: flex;
|
11
|
+
align-items: center;
|
12
|
+
min-height: 40px !important;
|
13
|
+
padding: unset !important;
|
14
|
+
}
|
15
|
+
.mat-mdc-floating-label {
|
16
|
+
&:not(.mdc-floating-label--float-above) {
|
17
|
+
top: 1.2rem !important;
|
18
|
+
}
|
19
|
+
}
|
20
|
+
}
|
21
|
+
mat-chip-row {
|
22
|
+
margin-top: 8px !important;
|
23
|
+
margin-bottom: 8px !important;
|
24
|
+
}
|
25
|
+
.mat-mdc-standard-chip .mdc-evolution-chip__cell--primary,
|
26
|
+
.mat-mdc-standard-chip .mdc-evolution-chip__action--primary,
|
27
|
+
.mat-mdc-standard-chip .mat-mdc-chip-action-label {
|
28
|
+
overflow: hidden !important;
|
29
|
+
}
|
30
|
+
}
|
31
|
+
}
|
@@ -0,0 +1,83 @@
|
|
1
|
+
import { COMMA, ENTER } from '@angular/cdk/keycodes';
|
2
|
+
import { AsyncPipe, JsonPipe } from '@angular/common';
|
3
|
+
import { Component, forwardRef, Input, ViewEncapsulation } from '@angular/core';
|
4
|
+
import { NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
|
5
|
+
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
6
|
+
import { MatChipInputEvent, MatChipsModule } from '@angular/material/chips';
|
7
|
+
import { MatIconModule } from '@angular/material/icon';
|
8
|
+
|
9
|
+
import { Observable } from 'rxjs';
|
10
|
+
import {BaseInput} from "../core/base-input";
|
11
|
+
import {MatError, MatFormField, MatLabel} from "@angular/material/form-field";
|
12
|
+
import {MatInput} from "@angular/material/input";
|
13
|
+
|
14
|
+
@Component({
|
15
|
+
selector: 'gerandon-basic-chips',
|
16
|
+
templateUrl: 'basic-chips.component.html',
|
17
|
+
styleUrls: ['basic-chips.component.scss'],
|
18
|
+
standalone: true,
|
19
|
+
encapsulation: ViewEncapsulation.None,
|
20
|
+
providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => BasicChipsComponent), multi: true }],
|
21
|
+
imports: [
|
22
|
+
MatChipsModule,
|
23
|
+
MatIconModule,
|
24
|
+
ReactiveFormsModule,
|
25
|
+
MatAutocompleteModule,
|
26
|
+
AsyncPipe,
|
27
|
+
JsonPipe,
|
28
|
+
MatFormField,
|
29
|
+
MatInput,
|
30
|
+
MatLabel,
|
31
|
+
MatError,
|
32
|
+
],
|
33
|
+
})
|
34
|
+
export class BasicChipsComponent<T> extends BaseInput<T[]> {
|
35
|
+
|
36
|
+
@Input() public asyncOptions?: Observable<T[]>;
|
37
|
+
@Input() public labelProperty?: keyof T;
|
38
|
+
public readonly separatorKeysCodes = [ENTER, COMMA] as const;
|
39
|
+
|
40
|
+
remove(item: T) {
|
41
|
+
const values: T[] = this.control.value;
|
42
|
+
const index = values.indexOf(item);
|
43
|
+
if (index >= 0) {
|
44
|
+
values.splice(index, 1);
|
45
|
+
this.control.setValue(values);
|
46
|
+
}
|
47
|
+
|
48
|
+
this.mark();
|
49
|
+
}
|
50
|
+
|
51
|
+
add(event: MatChipInputEvent): void {
|
52
|
+
const value = (event.value || '').trim();
|
53
|
+
if (value) {
|
54
|
+
this.updateValue(value as T);
|
55
|
+
}
|
56
|
+
event.chipInput!.clear();
|
57
|
+
|
58
|
+
this.mark();
|
59
|
+
}
|
60
|
+
|
61
|
+
selected(event: MatAutocompleteSelectedEvent): void {
|
62
|
+
if (!this.control.value?.includes(event.option.value)) {
|
63
|
+
this.updateValue(<T>event.option.value);
|
64
|
+
}
|
65
|
+
this.inputElement.nativeElement.value = '';
|
66
|
+
|
67
|
+
this.mark();
|
68
|
+
}
|
69
|
+
|
70
|
+
private updateValue(value: T) {
|
71
|
+
this.control.setValue([
|
72
|
+
...(this.control.value || []),
|
73
|
+
value,
|
74
|
+
]);
|
75
|
+
}
|
76
|
+
|
77
|
+
private mark() {
|
78
|
+
if (!this.control.touched) {
|
79
|
+
this.control.markAsTouched();
|
80
|
+
this.control.markAsDirty();
|
81
|
+
}
|
82
|
+
}
|
83
|
+
}
|
@@ -0,0 +1,47 @@
|
|
1
|
+
<div class="basic-input cva-input">
|
2
|
+
<mat-form-field appearance="outline" [subscriptSizing]="subscriptSizing" [hintLabel]="hintLabel" [floatLabel]="floatLabel">
|
3
|
+
@if (label) {
|
4
|
+
<mat-label [class.disabled]="isDisabled">{{label}}</mat-label>
|
5
|
+
}
|
6
|
+
<input
|
7
|
+
[id]="id"
|
8
|
+
#inputElement
|
9
|
+
#input="ngForm"
|
10
|
+
matInput
|
11
|
+
[style.padding-right]="(suffix || prefixIcon) && '35px'"
|
12
|
+
[type]="type"
|
13
|
+
[attr.disabled]="isDisabled || control.disabled ? '' : null"
|
14
|
+
[readonly]="isDisabled"
|
15
|
+
[placeholder]="placeholder"
|
16
|
+
[formControl]="control"
|
17
|
+
[maxLength]="maxLength"
|
18
|
+
[name]="name"
|
19
|
+
[required]="!!control.errors?.['required']"/>
|
20
|
+
@if (prefixIcon) {
|
21
|
+
<mat-icon matPrefix color="accent">
|
22
|
+
{{prefixIcon}}
|
23
|
+
</mat-icon>
|
24
|
+
}
|
25
|
+
@if (suffixIcon) {
|
26
|
+
<mat-icon matSuffix color="accent">
|
27
|
+
{{suffixIcon}}
|
28
|
+
</mat-icon>
|
29
|
+
}
|
30
|
+
@if (suffix) {
|
31
|
+
<span matSuffix style="margin-right: 10px">{{suffix}}</span>
|
32
|
+
}
|
33
|
+
@if (control.errors?.['server']) {
|
34
|
+
<mat-error>{{ control.errors?.['server'] }}</mat-error>
|
35
|
+
} @else if (control.errors?.['required']) {
|
36
|
+
@if (validationTranslations.required) {
|
37
|
+
<mat-error>{{ validationTranslations.required }}</mat-error>
|
38
|
+
} @else {
|
39
|
+
@for (error of validatorMessagesArray; track error) {
|
40
|
+
@if (control.errors?.[error.key]) {
|
41
|
+
<mat-error>{{ error.value }}</mat-error>
|
42
|
+
}
|
43
|
+
}
|
44
|
+
}
|
45
|
+
}
|
46
|
+
</mat-form-field>
|
47
|
+
</div>
|
@@ -0,0 +1,26 @@
|
|
1
|
+
gerandon-basic-input {
|
2
|
+
display: block;
|
3
|
+
.basic-input {
|
4
|
+
height: inherit;
|
5
|
+
.disabled {
|
6
|
+
color: #CED4DAFF;
|
7
|
+
}
|
8
|
+
mat-form-field {
|
9
|
+
width: 100%;
|
10
|
+
.mat-icon {
|
11
|
+
padding: unset;
|
12
|
+
margin-left: 10px;
|
13
|
+
margin-right: 10px;
|
14
|
+
}
|
15
|
+
input {
|
16
|
+
&::placeholder {
|
17
|
+
color: #ADB5BDFF;
|
18
|
+
font-style: italic;
|
19
|
+
}
|
20
|
+
&:disabled {
|
21
|
+
cursor: not-allowed;
|
22
|
+
}
|
23
|
+
}
|
24
|
+
}
|
25
|
+
}
|
26
|
+
}
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import {
|
2
|
+
Component,
|
3
|
+
EventEmitter,
|
4
|
+
forwardRef,
|
5
|
+
OnInit,
|
6
|
+
Output,
|
7
|
+
ViewEncapsulation,
|
8
|
+
} from '@angular/core';
|
9
|
+
import { NG_ASYNC_VALIDATORS, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
|
10
|
+
import { MatFormFieldModule } from '@angular/material/form-field';
|
11
|
+
import { MatIconModule } from '@angular/material/icon';
|
12
|
+
import { MatInputModule } from '@angular/material/input';
|
13
|
+
|
14
|
+
import { BaseTextInput } from '../core/base-text-input';
|
15
|
+
|
16
|
+
@Component({
|
17
|
+
selector: 'gerandon-basic-input',
|
18
|
+
templateUrl: './basic-input.component.html',
|
19
|
+
styleUrls: ['./basic-input.component.scss'],
|
20
|
+
encapsulation: ViewEncapsulation.None,
|
21
|
+
standalone: true,
|
22
|
+
imports: [
|
23
|
+
ReactiveFormsModule,
|
24
|
+
MatIconModule,
|
25
|
+
MatFormFieldModule,
|
26
|
+
MatInputModule,
|
27
|
+
],
|
28
|
+
providers: [
|
29
|
+
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => BasicInputComponent), multi: true },
|
30
|
+
{ provide: NG_ASYNC_VALIDATORS, useExisting: forwardRef(() => BasicInputComponent), multi: true },
|
31
|
+
],
|
32
|
+
})
|
33
|
+
export class BasicInputComponent extends BaseTextInput<string> implements OnInit {
|
34
|
+
|
35
|
+
@Output() iconClick = new EventEmitter();
|
36
|
+
|
37
|
+
override ngOnInit() {
|
38
|
+
super.ngOnInit();
|
39
|
+
this.id = this.id || this.name;
|
40
|
+
}
|
41
|
+
}
|
@@ -0,0 +1,69 @@
|
|
1
|
+
import {
|
2
|
+
AfterViewInit,
|
3
|
+
Directive, Inject, inject, InjectionToken,
|
4
|
+
Input, OnChanges,
|
5
|
+
OnInit, Optional, SimpleChanges,
|
6
|
+
} from '@angular/core';
|
7
|
+
import { FloatLabelType, SubscriptSizing } from '@angular/material/form-field';
|
8
|
+
|
9
|
+
import { BaseValueAccessor } from './base-value-accessor';
|
10
|
+
import { isEmpty, keys } from 'lodash-es';
|
11
|
+
|
12
|
+
export interface NgxWidgetsValidationErrorTypes {
|
13
|
+
required?: string;
|
14
|
+
selectGlobalPlaceholder?: string;
|
15
|
+
}
|
16
|
+
export const NGX_WIDGETS_VALIDATION_TRANSLATIONS = new InjectionToken<NgxWidgetsValidationErrorTypes>('NGX_WIDGETS_VALIDATION_TRANSLATIONS');
|
17
|
+
|
18
|
+
@Directive()
|
19
|
+
export class BaseInput<T> extends BaseValueAccessor<T> implements OnInit, AfterViewInit, OnChanges {
|
20
|
+
|
21
|
+
@Input() public id!: string;
|
22
|
+
@Input() public name!: string;
|
23
|
+
@Input() public label!: string;
|
24
|
+
@Input() public translateParams?: unknown;
|
25
|
+
@Input() public placeholder!: string;
|
26
|
+
@Input() public isDisabled? = false;
|
27
|
+
@Input() public floatLabel: FloatLabelType = 'auto';
|
28
|
+
@Input() public prefixIcon?: string;
|
29
|
+
@Input() public suffixIcon?: string;
|
30
|
+
@Input() public suffix?: string;
|
31
|
+
@Input() public formControlName?: string;
|
32
|
+
@Input() public validatorMessages?: { [key: string]: string };
|
33
|
+
@Input() public subscriptSizing: SubscriptSizing = 'fixed';
|
34
|
+
@Input() public hintLabel = '';
|
35
|
+
public validatorMessagesArray: { key: string, value: unknown }[] = [];
|
36
|
+
|
37
|
+
constructor(@Optional() @Inject(NGX_WIDGETS_VALIDATION_TRANSLATIONS) protected readonly validationTranslations: NgxWidgetsValidationErrorTypes) {
|
38
|
+
super();
|
39
|
+
}
|
40
|
+
|
41
|
+
ngOnInit() {
|
42
|
+
this.placeholder = this.placeholder === undefined ? this.label : this.placeholder;
|
43
|
+
if (!this.name) {
|
44
|
+
this.name = this.formControlName!;
|
45
|
+
/*
|
46
|
+
console.warn(`name attribute is not defined for ${this.formControlName}! Please beware, that using this control multiple
|
47
|
+
times with the same control name could result in wrong focus, clicking on the label!`);
|
48
|
+
*/
|
49
|
+
}
|
50
|
+
// *ngIf seems like does not re-render component when label is used with dynamic value (e.g.: translate pipe). Strange
|
51
|
+
this.label = this.label || ' ';
|
52
|
+
}
|
53
|
+
|
54
|
+
ngOnChanges(changes: SimpleChanges) {
|
55
|
+
if (changes['validatorMessages']) {
|
56
|
+
if (!isEmpty(this.validatorMessages)) {
|
57
|
+
this.validatorMessagesArray = keys(this.validatorMessages).map((key) => ({
|
58
|
+
key,
|
59
|
+
value: this.validatorMessages![key],
|
60
|
+
}));
|
61
|
+
}
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
override ngAfterViewInit() {
|
66
|
+
super.ngAfterViewInit();
|
67
|
+
this.cdr.detectChanges();
|
68
|
+
}
|
69
|
+
}
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import {
|
2
|
+
Directive,
|
3
|
+
Input,
|
4
|
+
} from '@angular/core';
|
5
|
+
|
6
|
+
import { BaseInput } from './base-input';
|
7
|
+
|
8
|
+
@Directive()
|
9
|
+
export class BaseTextInput<T> extends BaseInput<T> {
|
10
|
+
|
11
|
+
@Input() public type: ('text' | 'password' | 'number' | 'email' | 'tel') = 'text';
|
12
|
+
@Input() public maxLength? = 512;
|
13
|
+
}
|
@@ -0,0 +1,80 @@
|
|
1
|
+
import {
|
2
|
+
AfterViewInit,
|
3
|
+
ChangeDetectorRef, Directive,
|
4
|
+
ElementRef, inject,
|
5
|
+
Injector, Input, OnDestroy, Type,
|
6
|
+
ViewChild,
|
7
|
+
} from '@angular/core';
|
8
|
+
import {
|
9
|
+
AbstractControl,
|
10
|
+
ControlValueAccessor, FormControl,
|
11
|
+
NgControl,
|
12
|
+
ValidationErrors,
|
13
|
+
Validator, ValidatorFn,
|
14
|
+
} from '@angular/forms';
|
15
|
+
|
16
|
+
import {Observable, of, Subject} from 'rxjs';
|
17
|
+
|
18
|
+
@Directive()
|
19
|
+
export class BaseValueAccessor<T> implements ControlValueAccessor, AfterViewInit, Validator, OnDestroy {
|
20
|
+
|
21
|
+
@Input() public validator: Observable<ValidationErrors> = of({});
|
22
|
+
@ViewChild('inputElement') inputElement!: ElementRef;
|
23
|
+
@ViewChild('input') input!: NgControl;
|
24
|
+
|
25
|
+
public control: FormControl;
|
26
|
+
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
28
|
+
private onChange = (value: T) => {
|
29
|
+
};
|
30
|
+
private onTouched = () => {
|
31
|
+
};
|
32
|
+
private readonly injector: Injector = inject(Injector);
|
33
|
+
protected controlDir!: NgControl;
|
34
|
+
protected readonly cdr: ChangeDetectorRef = inject(ChangeDetectorRef);
|
35
|
+
protected _validate: ValidatorFn;
|
36
|
+
protected readonly _defaultValidate: ValidatorFn = () => null;
|
37
|
+
|
38
|
+
protected readonly destroy$ = new Subject<void>();
|
39
|
+
|
40
|
+
constructor() {
|
41
|
+
this._validate = this._defaultValidate;
|
42
|
+
// Temporarily, AfterViewInit will handle the correct setting
|
43
|
+
this.control = new FormControl();
|
44
|
+
}
|
45
|
+
|
46
|
+
validate(control: AbstractControl): Observable<ValidationErrors> {
|
47
|
+
control.setErrors({ ...control.errors, pending: true });
|
48
|
+
return this.validator;
|
49
|
+
}
|
50
|
+
|
51
|
+
ngAfterViewInit() {
|
52
|
+
this.controlDir = this.injector.get<NgControl>(NgControl as Type<NgControl>);
|
53
|
+
this.control = <FormControl>this.controlDir.control;
|
54
|
+
// For ng-valid expression changed error workaround purposes
|
55
|
+
this.cdr.detectChanges();
|
56
|
+
}
|
57
|
+
|
58
|
+
writeValue(obj: T): void {
|
59
|
+
this.valueAccessor?.writeValue(obj);
|
60
|
+
}
|
61
|
+
|
62
|
+
registerOnChange(fn: (value: T) => unknown): void {
|
63
|
+
this.onChange = fn;
|
64
|
+
this.valueAccessor?.registerOnChange(fn);
|
65
|
+
}
|
66
|
+
|
67
|
+
registerOnTouched(fn: () => unknown) {
|
68
|
+
this.onTouched = fn;
|
69
|
+
this.valueAccessor?.registerOnTouched(fn);
|
70
|
+
}
|
71
|
+
|
72
|
+
protected get valueAccessor(): ControlValueAccessor | null {
|
73
|
+
return this.input ? this.input.valueAccessor : null;
|
74
|
+
}
|
75
|
+
|
76
|
+
ngOnDestroy() {
|
77
|
+
this.destroy$.next();
|
78
|
+
this.destroy$.complete();
|
79
|
+
}
|
80
|
+
}
|
@@ -0,0 +1,58 @@
|
|
1
|
+
import { isDevMode } from '@angular/core';
|
2
|
+
|
3
|
+
import { Observable, Subject, takeUntil } from 'rxjs';
|
4
|
+
import { SafeSubscriber } from 'rxjs/internal/Subscriber';
|
5
|
+
|
6
|
+
/**
|
7
|
+
* Automatically unsubscribe from an Observable when the view is destroyed
|
8
|
+
* Tested with checking the "complete" event of a subscribe method
|
9
|
+
* @description
|
10
|
+
* An Annotation that should be used with an Observable typed variable to handle its subscriptions
|
11
|
+
* @author gergo.asztalos
|
12
|
+
*/
|
13
|
+
export function UnsubscribeOnDestroy<ObservableType>(): PropertyDecorator {
|
14
|
+
return function (target: any, propertyKey: string | symbol) {
|
15
|
+
const ngOnDestroy = target.ngOnDestroy;
|
16
|
+
|
17
|
+
const secretKey = `_${<string>propertyKey}$`;
|
18
|
+
// Probably with function we could use own context
|
19
|
+
const destroyKey = (_this: any) =>
|
20
|
+
_this.hasOwnProperty('destroy$') ? 'destroy$' : `${_this.constructor.name}_destroy$`;
|
21
|
+
Object.defineProperty(target, secretKey, { enumerable: false, writable: true });
|
22
|
+
Object.defineProperty(target, propertyKey, {
|
23
|
+
configurable: true,
|
24
|
+
enumerable: true,
|
25
|
+
get: function() {
|
26
|
+
return this[secretKey];
|
27
|
+
},
|
28
|
+
set: function(newValue: Observable<ObservableType> | SafeSubscriber<ObservableType>) {
|
29
|
+
if (!this[destroyKey(this)]) {
|
30
|
+
this[destroyKey(this)] = new Subject();
|
31
|
+
}
|
32
|
+
if (newValue instanceof Observable) {
|
33
|
+
this[secretKey] = newValue.pipe(
|
34
|
+
takeUntil(this[destroyKey(this)]),
|
35
|
+
);
|
36
|
+
} else {
|
37
|
+
this[secretKey] = newValue;
|
38
|
+
}
|
39
|
+
},
|
40
|
+
});
|
41
|
+
|
42
|
+
target.ngOnDestroy = function () {
|
43
|
+
if (this[propertyKey] instanceof SafeSubscriber) {
|
44
|
+
this[propertyKey].unsubscribe();
|
45
|
+
this[secretKey].unsubscribe();
|
46
|
+
} else if (this.hasOwnProperty(destroyKey(this))) {
|
47
|
+
this[destroyKey(this)].next();
|
48
|
+
this[destroyKey(this)].complete();
|
49
|
+
}
|
50
|
+
delete this[secretKey];
|
51
|
+
if (isDevMode()) {
|
52
|
+
// eslint-disable-next-line no-console,max-len
|
53
|
+
console.debug(`<UnsubscribeOnDestroy> - Observable/Subscription <${<string>propertyKey}> completed in class: ${this.constructor.name}`);
|
54
|
+
}
|
55
|
+
ngOnDestroy && ngOnDestroy.call(this);
|
56
|
+
};
|
57
|
+
};
|
58
|
+
}
|
@@ -0,0 +1,39 @@
|
|
1
|
+
<mat-form-field appearance="outline" [subscriptSizing]="subscriptSizing" [floatLabel]="floatLabel">
|
2
|
+
@if (label) {
|
3
|
+
<mat-label>{{ label }}</mat-label>
|
4
|
+
}
|
5
|
+
<mat-select #inputElement
|
6
|
+
#input="ngForm"
|
7
|
+
[multiple]="multiple"
|
8
|
+
[placeholder]="!floatLabel ? label : placeholder"
|
9
|
+
[formControl]="control"
|
10
|
+
[id]="id"
|
11
|
+
[class.input-disabled]="isDisabled || control.disabled"
|
12
|
+
[compareWith]="_isEqual"
|
13
|
+
[attr.disabled]="isDisabled || control.disabled ? '' : null">
|
14
|
+
@if (emptyOptionLabel) {
|
15
|
+
<mat-option (click)="control.reset()">
|
16
|
+
{{ emptyOptionLabel }}
|
17
|
+
</mat-option>
|
18
|
+
}
|
19
|
+
@for(option of options; track option) {
|
20
|
+
<mat-option [value]="option.value">
|
21
|
+
{{ option.label }}
|
22
|
+
</mat-option>
|
23
|
+
}
|
24
|
+
</mat-select>
|
25
|
+
@if (suffix) {
|
26
|
+
<span matSuffix>{{suffix}}</span>
|
27
|
+
}
|
28
|
+
@if (control.errors?.['server']) {
|
29
|
+
<mat-error>{{ control.errors?.['server'] }}</mat-error>
|
30
|
+
} @else if (control.errors?.['required']) {
|
31
|
+
@if (validationTranslations.required) {
|
32
|
+
<mat-error>{{ validationTranslations.required }}</mat-error>
|
33
|
+
} @else {
|
34
|
+
@for (error of validatorMessagesArray; track error) {
|
35
|
+
<mat-error>{{ error.value }}</mat-error>
|
36
|
+
}
|
37
|
+
}
|
38
|
+
}
|
39
|
+
</mat-form-field>
|
@@ -0,0 +1,70 @@
|
|
1
|
+
import {
|
2
|
+
Component,
|
3
|
+
ElementRef,
|
4
|
+
forwardRef,
|
5
|
+
Input,
|
6
|
+
OnInit,
|
7
|
+
QueryList,
|
8
|
+
ViewChildren,
|
9
|
+
ViewEncapsulation,
|
10
|
+
} from '@angular/core';
|
11
|
+
import { NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
|
12
|
+
import { MatInputModule } from '@angular/material/input';
|
13
|
+
import { MatSelectModule } from '@angular/material/select';
|
14
|
+
import { MatTooltipModule } from '@angular/material/tooltip';
|
15
|
+
|
16
|
+
import {BaseInput} from '../core/base-input';
|
17
|
+
import { isEqual } from 'lodash-es';
|
18
|
+
import { Observable } from 'rxjs';
|
19
|
+
import { takeUntil } from 'rxjs/operators';
|
20
|
+
|
21
|
+
export interface SelectOptionType {
|
22
|
+
label: string;
|
23
|
+
value: string | number | null | unknown;
|
24
|
+
}
|
25
|
+
|
26
|
+
@Component({
|
27
|
+
selector: 'gerandon-select',
|
28
|
+
templateUrl: './select.component.html',
|
29
|
+
styleUrls: ['./select.component.scss'],
|
30
|
+
encapsulation: ViewEncapsulation.None,
|
31
|
+
standalone: true,
|
32
|
+
providers: [
|
33
|
+
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SelectComponent), multi: true }
|
34
|
+
],
|
35
|
+
imports: [
|
36
|
+
MatInputModule,
|
37
|
+
MatSelectModule,
|
38
|
+
ReactiveFormsModule,
|
39
|
+
MatTooltipModule,
|
40
|
+
],
|
41
|
+
})
|
42
|
+
export class SelectComponent extends BaseInput<unknown> implements OnInit {
|
43
|
+
|
44
|
+
/**
|
45
|
+
* In this case, an empty option appears that resets the control, to an empty value state
|
46
|
+
*/
|
47
|
+
@Input() public emptyOptionLabel?: string;
|
48
|
+
@Input() public multiple?: boolean;
|
49
|
+
@Input() public options!: SelectOptionType[];
|
50
|
+
@Input() public asyncOptions!: Observable<SelectOptionType[]>;
|
51
|
+
@ViewChildren('optionElements') public optionElements!: QueryList<ElementRef>;
|
52
|
+
|
53
|
+
/**
|
54
|
+
* Angular Material - Select component comparsion is only '===', does not work with Array values
|
55
|
+
* https://github.com/angular/components/blob/a07c0758a5ec2eb4de1bb822354be08178c66aa4/src/lib/select/select.ts#L242C48-L242C58
|
56
|
+
*/
|
57
|
+
public readonly _isEqual = isEqual;
|
58
|
+
|
59
|
+
override ngOnInit() {
|
60
|
+
this.placeholder = !this.placeholder ? (this.validationTranslations.selectGlobalPlaceholder || this.label) : this.placeholder;
|
61
|
+
super.ngOnInit();
|
62
|
+
this.id = this.id || this.formControlName || this.name;
|
63
|
+
if (this.asyncOptions) {
|
64
|
+
this.asyncOptions.pipe(takeUntil(this.destroy$)).subscribe((resp) => {
|
65
|
+
this.options = resp;
|
66
|
+
this.cdr.detectChanges();
|
67
|
+
});
|
68
|
+
}
|
69
|
+
}
|
70
|
+
}
|