@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.
Files changed (43) hide show
  1. package/ng-package.json +7 -0
  2. package/package.json +9 -19
  3. package/src/lib/basic-chips/basic-chips.component.html +42 -0
  4. package/src/lib/basic-chips/basic-chips.component.scss +31 -0
  5. package/src/lib/basic-chips/basic-chips.component.ts +83 -0
  6. package/src/lib/basic-input/basic-input.component.html +47 -0
  7. package/src/lib/basic-input/basic-input.component.scss +26 -0
  8. package/src/lib/basic-input/basic-input.component.ts +41 -0
  9. package/src/lib/core/base-input.ts +69 -0
  10. package/src/lib/core/base-text-input.ts +13 -0
  11. package/src/lib/core/base-value-accessor.ts +80 -0
  12. package/src/lib/core/component-unsubscribe.ts +58 -0
  13. package/src/lib/select/select.component.html +39 -0
  14. package/src/lib/select/select.component.scss +7 -0
  15. package/src/lib/select/select.component.ts +70 -0
  16. package/src/lib/textarea-input/textarea-input.component.html +48 -0
  17. package/src/lib/textarea-input/textarea-input.component.scss +27 -0
  18. package/src/lib/textarea-input/textarea-input.component.ts +30 -0
  19. package/{public-api.d.ts → src/public-api.ts} +5 -0
  20. package/tsconfig.lib.json +14 -0
  21. package/tsconfig.lib.prod.json +10 -0
  22. package/tsconfig.spec.json +14 -0
  23. package/esm2022/gerandon-ngx-widgets.mjs +0 -5
  24. package/esm2022/lib/basic-chips/basic-chips.component.mjs +0 -80
  25. package/esm2022/lib/basic-input/basic-input.component.mjs +0 -41
  26. package/esm2022/lib/core/base-input.mjs +0 -81
  27. package/esm2022/lib/core/base-text-input.mjs +0 -20
  28. package/esm2022/lib/core/base-value-accessor.mjs +0 -63
  29. package/esm2022/lib/core/component-unsubscribe.mjs +0 -54
  30. package/esm2022/lib/select/select.component.mjs +0 -61
  31. package/esm2022/lib/textarea-input/textarea-input.component.mjs +0 -39
  32. package/esm2022/public-api.mjs +0 -12
  33. package/fesm2022/gerandon-ngx-widgets.mjs +0 -409
  34. package/fesm2022/gerandon-ngx-widgets.mjs.map +0 -1
  35. package/index.d.ts +0 -5
  36. package/lib/basic-chips/basic-chips.component.d.ts +0 -17
  37. package/lib/basic-input/basic-input.component.d.ts +0 -9
  38. package/lib/core/base-input.d.ts +0 -38
  39. package/lib/core/base-text-input.d.ts +0 -8
  40. package/lib/core/base-value-accessor.d.ts +0 -28
  41. package/lib/core/component-unsubscribe.d.ts +0 -8
  42. package/lib/select/select.component.d.ts +0 -26
  43. package/lib/textarea-input/textarea-input.component.d.ts +0 -7
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3
+ "dest": "../../dist/ngx-widgets",
4
+ "lib": {
5
+ "entryFile": "src/public-api.ts"
6
+ }
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gerandon/ngx-widgets",
3
- "version": "17.0.0",
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": "^17.3.0",
32
- "@angular/core": "^17.3.0",
33
- "@angular/material": "^17.3.6",
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
- "module": "fesm2022/gerandon-ngx-widgets.mjs",
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,7 @@
1
+ gerandon-select {
2
+ mat-form-field {
3
+ width: 100%;
4
+ .mat-mdc-text-field-wrapper {
5
+ }
6
+ }
7
+ }
@@ -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
+ }