@gerandon/ngx-widgets 17.0.0 → 18.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
}
|