diff --git a/README.md b/README.md
index 4fa3df1..2cb9adf 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,17 @@
-
+
-
+
+ View Github Repository
+
+
+ An abstraction of Angular Material form controls that speeds up form building with drop in, standalone inputs, unified labels, hints, and errors, plus helpers like scroll to first error and focus on invalid. Built for modern Angular and plugs straight into Reactive Forms to cut boilerplate.
+
+
@@ -20,8 +26,6 @@
[](https://bundlephobia.com/result?p=ProAngular/pro-form)
[](https://www.ProAngular.com/demos/pro-form)
[](https://www.proangular.com)
-[](https://gitter.im/ProAngular/community)
-[](https://discord.com/channels/1003103094588055552)
[](https://github.com/sponsors/ProAngular)
[](/LICENSE)
[](https://github.com/ProAngular/pro-form/actions/workflows/on-merge-main-deploy-gpr.yml)
diff --git a/package-lock.json b/package-lock.json
index 434af10..bd3e711 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@proangular/pro-form",
- "version": "20.1.7",
+ "version": "20.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@proangular/pro-form",
- "version": "20.1.7",
+ "version": "20.2.0",
"hasInstallScript": true,
"license": "MIT",
"devDependencies": {
diff --git a/package.json b/package.json
index e04b45c..17ec354 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@proangular/pro-form",
- "version": "20.1.7",
+ "version": "20.2.0",
"description": "A predefined set of reactive and reusable form input components based on Angular Material.",
"author": "Pro Angular ",
"homepage": "https://www.proangular.com",
diff --git a/src/app/form/form.component.html b/src/app/form/form.component.html
index e491e0e..a6e33c2 100644
--- a/src/app/form/form.component.html
+++ b/src/app/form/form.component.html
@@ -1,205 +1,323 @@
-
+
-
-
- Reset
-
-
- Fill
-
-
- Submit
-
-
-
+
+
+
-
-
Form Values
-
-
+
+
+
+
+
+ {{ label }}
+
+
diff --git a/src/app/form/form.component.scss b/src/app/form/form.component.scss
index a16229c..c56e849 100644
--- a/src/app/form/form.component.scss
+++ b/src/app/form/form.component.scss
@@ -8,66 +8,156 @@ $margin: 0.5rem;
margin: 2rem 0;
}
-form {
+.form-section {
display: flex;
- flex-wrap: wrap;
- justify-content: space-between;
- padding: 0;
+ flex-direction: row;
+ width: 100%;
- pro-input,
- pro-input-checkbox,
- pro-input-chips,
- pro-input-datepicker,
- pro-input-dropdown,
- pro-input-radio,
- pro-input-timepicker,
- pro-input-toggle,
- pro-loading-input {
- flex: 0 0 calc(50% - ($gap/ 2));
+ .form-container {
+ flex: 0 0 calc(75% - ($gap/ 2));
margin: 0 $margin calc($margin * 2) $margin;
- max-width: calc(50% - ($gap / 2));
+ max-width: calc(75% - ($gap / 2));
width: 100%;
+
+ form {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ padding: 0;
+ margin-bottom: 1rem;
+
+ pro-input,
+ pro-input-checkbox,
+ pro-input-chips,
+ pro-input-datepicker,
+ pro-input-dropdown,
+ pro-input-radio,
+ pro-input-timepicker,
+ pro-input-toggle,
+ pro-loading-input {
+ flex: 0 0 calc(50% - ($gap/ 2));
+ margin: 0 $margin calc($margin * 2) $margin;
+ max-width: calc(50% - ($gap / 2));
+ width: 100%;
+ }
+
+ pro-input-textarea {
+ flex: 0 0 calc(100% - ($gap/ 2));
+ margin: 0 $margin calc($margin * 2) $margin;
+ max-width: calc(100% - ($gap / 2));
+ }
+ }
+
+ .form-buttons > button {
+ margin-right: 0.5rem;
+ }
}
- pro-input-textarea {
- flex: 0 0 calc(100% - ($gap/ 2));
+ .form-states {
+ flex: 0 0 calc(25% - ($gap/ 2));
margin: 0 $margin calc($margin * 2) $margin;
- max-width: calc(100% - ($gap / 2));
+ max-width: calc(25% - ($gap / 2));
+ width: 100%;
+ border-left: 1px solid var(--mat-sys-outline);
+ padding-left: 1rem;
+
+ .state {
+ display: flex;
+ margin-bottom: 0.25rem;
+ justify-content: flex-start;
+ align-items: center;
+ flex-wrap: nowrap;
+ flex-direction: row;
+
+ mat-icon:hover {
+ cursor: not-allowed;
+ }
+ }
}
}
-.actions > button {
- margin-right: 0.5rem;
+hr {
+ margin-top: 2rem;
}
p {
padding: 0.5rem;
}
-form {
- margin-bottom: 1rem;
-}
+.extra-options {
+ display: flex;
+ flex-wrap: wrap;
+ flex-direction: column;
-.disable-fields-checkbox {
- display: block;
- margin: 0.5rem 1rem 0 0;
+ h4 {
+ margin-bottom: 0.5rem;
+ }
+
+ .checkbox-container {
+ display: flex;
+ flex-wrap: wrap;
+ flex-direction: row;
+
+ .toggle-fields-checkbox,
+ .toggle-hide-hints-checkbox {
+ display: block;
+ margin-right: 1rem;
+ }
+ }
}
-.form-values {
+.form-stats {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 0;
- > textarea {
- flex: 0 0 calc(100% - ($gap/ 2));
+ $container-height: 320px;
+
+ .form-values {
+ flex: 0 0 calc(50% - ($gap/ 2));
+ margin: 0 $margin calc($margin * 2) $margin;
+ max-width: calc(50% - ($gap / 2));
+
+ textarea {
+ height: $container-height;
+ width: 100%;
+ border: 1px solid var(--mat-sys-outline);
+ font-size: 14px;
+ }
+ }
+
+ .form-errors {
+ flex: 0 0 calc(50% - ($gap/ 2));
margin: 0 $margin calc($margin * 2) $margin;
- max-width: calc(100% - ($gap / 2));
+ max-width: calc(50% - ($gap / 2));
+
+ .dl-container {
+ height: $container-height;
+ width: 100%;
+ overflow: auto;
+ border: 1px solid var(--mat-sys-outline);
+
+ dl {
+ display: flex;
+ flex-wrap: wrap;
+ flex-direction: row;
+ margin: 0;
+ font-size: 14px;
+
+ dt,
+ dd {
+ flex: 0 0 calc(50% - ($gap/ 2));
+ margin: 0;
+ max-width: calc(50% - ($gap / 2));
+ }
+ }
+ }
}
}
@include breakpoints.media('max', 'md') {
- :host form {
+ :host .form-section .form-container form {
> pro-input,
> pro-input-checkbox,
> pro-input-chips,
@@ -84,4 +174,39 @@ form {
width: 100%;
}
}
+
+ .form-section {
+ flex-direction: column;
+
+ .form-container,
+ .form-states {
+ margin-top: 1rem;
+ flex: 0 0 100%;
+ margin: 0 0 calc($margin * 2) 0;
+ max-width: 100%;
+ width: 100%;
+ padding: 0;
+ border-left: none;
+ }
+
+ .form-states {
+ border-top: 1px solid var(--mat-sys-outline);
+ padding-top: 1rem;
+ margin-top: 2rem;
+
+ .state {
+ padding-left: 1rem;
+ }
+ }
+ }
+
+ .form-stats {
+ .form-errors,
+ .form-values {
+ flex: 0 0 100%;
+ margin: 0 0 calc($margin * 2) 0;
+ max-width: 100%;
+ width: 100%;
+ }
+ }
}
diff --git a/src/app/form/form.component.ts b/src/app/form/form.component.ts
index f6fcf81..639fae9 100644
--- a/src/app/form/form.component.ts
+++ b/src/app/form/form.component.ts
@@ -4,6 +4,7 @@ import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
+import { MatIconModule } from '@angular/material/icon';
import { MatSnackBar } from '@angular/material/snack-bar';
import { FormDirective } from '../public/form.directive';
@@ -48,6 +49,7 @@ import {
InputTimepickerComponent,
InputToggleComponent,
MatButtonModule,
+ MatIconModule,
ReactiveFormsModule,
],
standalone: true,
@@ -60,12 +62,15 @@ export class FormComponent extends FormDirective {
this.today = DateTime.local().startOf('day');
this.thirtyDaysFromNow = this.today.plus({ months: 1 });
+
this.toggleDisabledFormControl = new FormControl(false);
+ this.toggleHideHintsFormControl = new FormControl(false);
}
private readonly snackBar = inject(MatSnackBar);
protected override readonly formGroup = formGroupExample;
+
protected readonly chips = chips;
protected readonly dropdownOptions = dropdownOptions;
protected readonly groupedDropdownOptions = groupedDropdownOptions;
@@ -73,6 +78,7 @@ export class FormComponent extends FormDirective {
protected readonly thirtyDaysFromNow: DateTime;
protected readonly today: DateTime;
protected readonly toggleDisabledFormControl: FormControl;
+ protected readonly toggleHideHintsFormControl: FormControl;
protected readonly validation = formGroupExampleValidation;
protected prefillForm(): void {
diff --git a/src/app/public/input-checkbox/input-checkbox.component.ts b/src/app/public/input-checkbox/input-checkbox.component.ts
index df05e32..f27826c 100644
--- a/src/app/public/input-checkbox/input-checkbox.component.ts
+++ b/src/app/public/input-checkbox/input-checkbox.component.ts
@@ -16,14 +16,10 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { InputDirective } from '../input.directive';
-const rF = { required: false };
-
@UntilDestroy()
@Component({
- selector: 'pro-input-checkbox',
+ selector: 'pro-input-checkbox[label]',
templateUrl: './input-checkbox.component.html',
- styleUrls: ['./input-checkbox.component.scss'],
- standalone: true,
imports: [
CommonModule,
FormsModule,
@@ -31,13 +27,15 @@ const rF = { required: false };
MatFormFieldModule,
ReactiveFormsModule,
],
+ styleUrl: './input-checkbox.component.scss',
changeDetection: ChangeDetectionStrategy.Default,
+ standalone: true,
})
export class InputCheckboxComponent
extends InputDirective
implements OnInit
{
- @Input(rF)
+ @Input()
public labelPosition: 'before' | 'after' = 'after';
// eslint-disable-next-line @angular-eslint/no-output-native
diff --git a/src/app/public/input-chips/input-chips.component.ts b/src/app/public/input-chips/input-chips.component.ts
index 8572100..077b357 100644
--- a/src/app/public/input-chips/input-chips.component.ts
+++ b/src/app/public/input-chips/input-chips.component.ts
@@ -1,4 +1,3 @@
-import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
@@ -7,6 +6,8 @@ import {
Input,
QueryList,
ViewChild,
+ booleanAttribute,
+ numberAttribute,
} from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatChipListbox, MatChipsModule } from '@angular/material/chips';
@@ -15,14 +16,9 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { InputDirective } from '../input.directive';
import { InputChipComponent } from './input-chip.component';
-const rF = { required: false };
-const rFc = { required: false, transform: coerceBooleanProperty };
-
@Component({
- selector: 'pro-input-chips',
+ selector: 'pro-input-chips[label]',
templateUrl: './input-chips.component.html',
- styleUrl: './input-chips.component.scss',
- changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
FormsModule,
@@ -30,18 +26,24 @@ const rFc = { required: false, transform: coerceBooleanProperty };
MatFormFieldModule,
ReactiveFormsModule,
],
+ styleUrl: './input-chips.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
})
export class InputChipsComponent extends InputDirective {
- @ContentChildren(InputChipComponent) public readonly chips =
- new QueryList();
- @ViewChild(MatChipListbox) public readonly matChipListbox?: MatChipListbox;
+ @ContentChildren(InputChipComponent)
+ public readonly chips = new QueryList();
+
+ @ViewChild(MatChipListbox)
+ public readonly matChipListbox: MatChipListbox | undefined;
- @Input(rF) public max: number | undefined;
+ @Input({ transform: numberAttribute })
+ public max: number | string | undefined;
- @Input(rFc) public multiple = false;
+ @Input({ transform: booleanAttribute })
+ public multiple = false;
- @Input(rF) public compareWith: MatChipListbox['compareWith'] = (
+ @Input() public compareWith: MatChipListbox['compareWith'] = (
optionValue,
selectedValue,
) => {
diff --git a/src/app/public/input-datepicker/input-datepicker.component.html b/src/app/public/input-datepicker/input-datepicker.component.html
index 5f83ee8..98b41c0 100644
--- a/src/app/public/input-datepicker/input-datepicker.component.html
+++ b/src/app/public/input-datepicker/input-datepicker.component.html
@@ -11,7 +11,9 @@
/>
- {{ hint }}
+ @if (!hideHint) {
+ {{ hint }}
+ }
@if (formControl.invalid && formControl.touched) {
@if (formControl.hasError('required')) {
diff --git a/src/app/public/input-datepicker/input-datepicker.component.scss b/src/app/public/input-datepicker/input-datepicker.component.scss
new file mode 100644
index 0000000..1ee092c
--- /dev/null
+++ b/src/app/public/input-datepicker/input-datepicker.component.scss
@@ -0,0 +1,4 @@
+::ng-deep pro-input-datepicker.hide-hint .mat-mdc-form-field-subscript-wrapper {
+ display: none;
+ visibility: hidden;
+}
diff --git a/src/app/public/input-datepicker/input-datepicker.component.ts b/src/app/public/input-datepicker/input-datepicker.component.ts
index 6c7a6f4..829c8c4 100644
--- a/src/app/public/input-datepicker/input-datepicker.component.ts
+++ b/src/app/public/input-datepicker/input-datepicker.component.ts
@@ -8,17 +8,14 @@ import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
-import { InputLoadingComponent } from '../input-loading/loading-input.component';
+import { InputLoadingComponent } from '../input-loading/input-loading.component';
import { InputDirective } from '../input.directive';
import { DateTimePipe } from '../pipes';
import { InputAppearance } from '../types';
-const rF = { required: false };
-
@Component({
- selector: 'pro-input-datepicker',
+ selector: 'pro-input-datepicker[label]',
templateUrl: './input-datepicker.component.html',
- changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
DateTimePipe,
@@ -29,10 +26,14 @@ const rF = { required: false };
ReactiveFormsModule,
],
providers: [provideLuxonDateAdapter()],
+ styleUrl: './input-datepicker.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
})
export class InputDatepickerComponent extends InputDirective {
- @Input(rF) public appearance: InputAppearance = 'outline';
- @Input(rF) public max: DateTime | undefined;
- @Input(rF) public min: DateTime | undefined;
+ @Input() public appearance: InputAppearance = 'outline';
+
+ @Input() public max: DateTime | undefined;
+
+ @Input() public min: DateTime | undefined;
}
diff --git a/src/app/public/input-dropdown/input-dropdown-option-group.component.ts b/src/app/public/input-dropdown/input-dropdown-option-group.component.ts
index ff90134..0fae852 100644
--- a/src/app/public/input-dropdown/input-dropdown-option-group.component.ts
+++ b/src/app/public/input-dropdown/input-dropdown-option-group.component.ts
@@ -9,7 +9,7 @@ import {
import { InputDropdownOptionComponent } from './input-dropdown-option.component';
@Component({
- selector: 'pro-input-dropdown-option-group',
+ selector: 'pro-input-dropdown-option-group[label]',
template: ` `,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
diff --git a/src/app/public/input-dropdown/input-dropdown.component.html b/src/app/public/input-dropdown/input-dropdown.component.html
index aad5c40..8c3abfb 100644
--- a/src/app/public/input-dropdown/input-dropdown.component.html
+++ b/src/app/public/input-dropdown/input-dropdown.component.html
@@ -22,7 +22,9 @@
}
- {{ hint }}
+ @if (!hideHint) {
+ {{ hint }}
+ }
@if (formControl.invalid && formControl.touched) {
@if (formControl.hasError('required')) {
diff --git a/src/app/public/input-dropdown/input-dropdown.component.scss b/src/app/public/input-dropdown/input-dropdown.component.scss
index a6e8bea..d7ca3a1 100644
--- a/src/app/public/input-dropdown/input-dropdown.component.scss
+++ b/src/app/public/input-dropdown/input-dropdown.component.scss
@@ -1,3 +1,8 @@
::ng-deep mat-form-field {
width: 100%;
}
+
+::ng-deep pro-input-dropdown.hide-hint .mat-mdc-form-field-subscript-wrapper {
+ display: none;
+ visibility: hidden;
+}
diff --git a/src/app/public/input-dropdown/input-dropdown.component.ts b/src/app/public/input-dropdown/input-dropdown.component.ts
index 29d81ec..023a27a 100644
--- a/src/app/public/input-dropdown/input-dropdown.component.ts
+++ b/src/app/public/input-dropdown/input-dropdown.component.ts
@@ -1,4 +1,3 @@
-import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
@@ -7,6 +6,8 @@ import {
Input,
QueryList,
ViewChild,
+ booleanAttribute,
+ numberAttribute,
} from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
@@ -16,20 +17,15 @@ import {
MatSelectModule,
} from '@angular/material/select';
-import { InputLoadingComponent } from '../input-loading/loading-input.component';
+import { InputLoadingComponent } from '../input-loading/input-loading.component';
import { InputDirective } from '../input.directive';
import { InputAppearance } from '../types';
import { InputDropdownOptionGroupComponent } from './input-dropdown-option-group.component';
import { InputDropdownOptionComponent } from './input-dropdown-option.component';
-const rF = { required: false };
-const rFc = { required: false, transform: coerceBooleanProperty };
-
@Component({
- selector: 'pro-input-dropdown',
+ selector: 'pro-input-dropdown[label]',
templateUrl: './input-dropdown.component.html',
- styleUrl: './input-dropdown.component.scss',
- changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
FormsModule,
@@ -38,9 +34,19 @@ const rFc = { required: false, transform: coerceBooleanProperty };
MatSelectModule,
ReactiveFormsModule,
],
+ styleUrl: './input-dropdown.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
})
export class InputDropdownComponent extends InputDirective {
+ @Input() public appearance: InputAppearance = 'outline';
+
+ @Input({ transform: numberAttribute })
+ public max: number | string | undefined;
+
+ @Input({ transform: booleanAttribute })
+ public multiple = false;
+
@ContentChildren(InputDropdownOptionComponent)
public readonly options: QueryList =
new QueryList();
@@ -51,15 +57,6 @@ export class InputDropdownComponent extends InputDirective {
@ViewChild(MatSelect) public readonly matSelect?: MatSelect;
- @Input(rF)
- public appearance: InputAppearance = 'outline';
-
- @Input() public max: number | undefined;
-
- private get exceedsMax(): boolean {
- return this.max !== undefined && this.selectedOptions.length > this.max;
- }
-
protected get selectedOptions(): readonly InputDropdownOptionComponent[] {
if (!this.matSelect) {
return [];
@@ -95,9 +92,7 @@ export class InputDropdownComponent extends InputDirective {
return [];
}
- @Input(rFc) public multiple = false;
-
- @Input(rF) public compareWith: MatSelect['compareWith'] = (
+ @Input() public compareWith: MatSelect['compareWith'] = (
optionValue,
selectedValue,
) => {
diff --git a/src/app/public/input-loading/input-loading.component.html b/src/app/public/input-loading/input-loading.component.html
new file mode 100644
index 0000000..9ca1821
--- /dev/null
+++ b/src/app/public/input-loading/input-loading.component.html
@@ -0,0 +1,6 @@
+
+
+ @if (!hideHint && hint) {
+ {{ hint }}
+ }
+
diff --git a/src/app/public/input-loading/input-loading.component.scss b/src/app/public/input-loading/input-loading.component.scss
new file mode 100644
index 0000000..b9bc65e
--- /dev/null
+++ b/src/app/public/input-loading/input-loading.component.scss
@@ -0,0 +1,3 @@
+:host {
+ width: 100%;
+}
diff --git a/src/app/public/input-loading/loading-input.component.ts b/src/app/public/input-loading/input-loading.component.ts
similarity index 71%
rename from src/app/public/input-loading/loading-input.component.ts
rename to src/app/public/input-loading/input-loading.component.ts
index a977cf2..50a7289 100644
--- a/src/app/public/input-loading/loading-input.component.ts
+++ b/src/app/public/input-loading/input-loading.component.ts
@@ -2,41 +2,47 @@ import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { interval } from 'rxjs';
import { CommonModule } from '@angular/common';
-import { Component, Input, OnInit } from '@angular/core';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ HostBinding,
+ Input,
+ OnInit,
+ booleanAttribute,
+} from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { InputAppearance } from '../types';
-const rF = { required: false };
-
@UntilDestroy()
@Component({
selector: 'pro-input-loading',
- template: `
-
-
- @if (hint) {
- {{ hint }}
- }
-
- `,
- styles: [':host { width: 100%; }'],
+ templateUrl: './input-loading.component.html',
imports: [
CommonModule,
MatFormFieldModule,
MatInputModule,
ReactiveFormsModule,
],
+ styleUrl: './input-loading.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
})
export class InputLoadingComponent implements OnInit {
- @Input(rF) public appearance: InputAppearance = 'outline';
- @Input(rF) public hint?: string;
- @Input(rF) public label?: string;
+ @Input() public appearance: InputAppearance = 'outline';
+
+ @HostBinding('class.hide-hint')
+ @Input({ transform: booleanAttribute })
+ public hideHint = false;
+
+ @Input() public hint: string | undefined;
+
+ @Input() public label: string | undefined;
protected readonly formControl = new FormControl(null);
+
protected loadingText = 'Loading';
public ngOnInit(): void {
diff --git a/src/app/public/input-radio/input-radio-option.component.ts b/src/app/public/input-radio/input-radio-option.component.ts
index 579aea6..a99d2ab 100644
--- a/src/app/public/input-radio/input-radio-option.component.ts
+++ b/src/app/public/input-radio/input-radio-option.component.ts
@@ -8,7 +8,7 @@ import {
} from '@angular/core';
@Component({
- selector: 'pro-input-radio-option',
+ selector: 'pro-input-radio-option[value]',
template: ` `,
changeDetection: ChangeDetectionStrategy.Default,
standalone: true,
diff --git a/src/app/public/input-radio/input-radio.component.html b/src/app/public/input-radio/input-radio.component.html
index 636af4f..8b890a8 100644
--- a/src/app/public/input-radio/input-radio.component.html
+++ b/src/app/public/input-radio/input-radio.component.html
@@ -11,7 +11,9 @@
}}
}
-{{ hint }}
+@if (!hideHint) {
+ {{ hint }}
+}
@if (formControl.invalid && formControl.touched) {
@if (formControl.hasError('required')) {
diff --git a/src/app/public/input-radio/input-radio.component.scss b/src/app/public/input-radio/input-radio.component.scss
index 61e2a1c..c3765d6 100644
--- a/src/app/public/input-radio/input-radio.component.scss
+++ b/src/app/public/input-radio/input-radio.component.scss
@@ -1,3 +1,8 @@
+::ng-deep pro-input-radio.hide-hint .mat-mdc-form-field-subscript-wrapper {
+ display: none;
+ visibility: hidden;
+}
+
mat-label {
display: block;
}
diff --git a/src/app/public/input-radio/input-radio.component.ts b/src/app/public/input-radio/input-radio.component.ts
index 12ef6da..7ddcc68 100644
--- a/src/app/public/input-radio/input-radio.component.ts
+++ b/src/app/public/input-radio/input-radio.component.ts
@@ -13,16 +13,16 @@ import { InputDirective } from '../input.directive';
import { InputRadioOptionComponent } from './input-radio-option.component';
@Component({
- selector: 'pro-input-radio',
+ selector: 'pro-input-radio[label]',
templateUrl: './input-radio.component.html',
- styleUrls: ['./input-radio.component.scss'],
- changeDetection: ChangeDetectionStrategy.Default,
imports: [
CommonModule,
MatFormFieldModule,
MatRadioModule,
ReactiveFormsModule,
],
+ styleUrl: './input-radio.component.scss',
+ changeDetection: ChangeDetectionStrategy.Default,
standalone: true,
})
export class InputRadioComponent extends InputDirective {
diff --git a/src/app/public/input-textarea/input-textarea.component.html b/src/app/public/input-textarea/input-textarea.component.html
index 47f44ea..6628a36 100644
--- a/src/app/public/input-textarea/input-textarea.component.html
+++ b/src/app/public/input-textarea/input-textarea.component.html
@@ -12,7 +12,9 @@
} @else {
Loading{{ label ? ' ' + label : '' }}...
}
- {{ hint }}
+ @if (!hideHint) {
+ {{ hint }}
+ }
@if (formControl.invalid && formControl.touched) {
@if (formControl.hasError('required')) {
diff --git a/src/app/public/input-textarea/input-textarea.component.scss b/src/app/public/input-textarea/input-textarea.component.scss
new file mode 100644
index 0000000..bc1d4f4
--- /dev/null
+++ b/src/app/public/input-textarea/input-textarea.component.scss
@@ -0,0 +1,4 @@
+::ng-deep pro-input-textarea.hide-hint .mat-mdc-form-field-subscript-wrapper {
+ display: none;
+ visibility: hidden;
+}
diff --git a/src/app/public/input-textarea/input-textarea.component.ts b/src/app/public/input-textarea/input-textarea.component.ts
index aae79da..b23997d 100644
--- a/src/app/public/input-textarea/input-textarea.component.ts
+++ b/src/app/public/input-textarea/input-textarea.component.ts
@@ -1,5 +1,10 @@
import { CommonModule } from '@angular/common';
-import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ Input,
+ numberAttribute,
+} from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
@@ -7,50 +12,29 @@ import { MatInputModule } from '@angular/material/input';
import { InputDirective } from '../input.directive';
import { InputAppearance, InputAutocomplete } from '../types';
-const rF = { required: false };
-
@Component({
- selector: 'pro-input-textarea',
+ selector: 'pro-input-textarea[label]',
templateUrl: './input-textarea.component.html',
- styleUrls: [],
- standalone: true,
imports: [
CommonModule,
MatFormFieldModule,
MatInputModule,
ReactiveFormsModule,
],
+ styleUrl: './input-textarea.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
+ standalone: true,
})
export class InputTextareaComponent extends InputDirective {
- @Input(rF) public appearance: InputAppearance = 'outline';
- @Input(rF) public autocomplete: InputAutocomplete = 'off';
- @Input(rF) public name: string | undefined;
+ @Input() public appearance: InputAppearance = 'outline';
+
+ @Input() public autocomplete: InputAutocomplete = 'off';
- @Input(rF) public set maxLength(value: number | string) {
- const maxLengthParsed = Number(value);
- if (isNaN(maxLengthParsed)) {
- this.#maxLength = undefined;
- return;
- }
- this.#maxLength = maxLengthParsed;
- }
- public get maxLength(): number | undefined {
- return this.#maxLength;
- }
- #maxLength: number | undefined;
+ @Input() public name: string | undefined;
- @Input(rF) public set minLength(value: number | string) {
- const minLengthParsed = Number(value);
- if (isNaN(minLengthParsed)) {
- this.#minLength = undefined;
- return;
- }
+ @Input({ transform: numberAttribute })
+ public maxLength: number | string | undefined;
- this.#minLength = minLengthParsed;
- }
- public get minLength(): number | undefined {
- return this.#minLength;
- }
- #minLength: number | undefined;
+ @Input({ transform: numberAttribute })
+ public minLength: number | string | undefined;
}
diff --git a/src/app/public/input-timepicker/input-timepicker.component.html b/src/app/public/input-timepicker/input-timepicker.component.html
index b335d2f..06829fe 100644
--- a/src/app/public/input-timepicker/input-timepicker.component.html
+++ b/src/app/public/input-timepicker/input-timepicker.component.html
@@ -11,7 +11,9 @@
/>
- {{ hint }}
+ @if (!hideHint) {
+ {{ hint }}
+ }
@if (formControl.invalid && formControl.touched) {
@if (formControl.hasError('required')) {
diff --git a/src/app/public/input-timepicker/input-timepicker.component.scss b/src/app/public/input-timepicker/input-timepicker.component.scss
new file mode 100644
index 0000000..977461a
--- /dev/null
+++ b/src/app/public/input-timepicker/input-timepicker.component.scss
@@ -0,0 +1,4 @@
+::ng-deep pro-input-timepicker.hide-hint .mat-mdc-form-field-subscript-wrapper {
+ display: none;
+ visibility: hidden;
+}
diff --git a/src/app/public/input-timepicker/input-timepicker.component.ts b/src/app/public/input-timepicker/input-timepicker.component.ts
index 721a462..91f5f96 100644
--- a/src/app/public/input-timepicker/input-timepicker.component.ts
+++ b/src/app/public/input-timepicker/input-timepicker.component.ts
@@ -8,18 +8,14 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatTimepickerModule } from '@angular/material/timepicker';
-import { InputLoadingComponent } from '../input-loading/loading-input.component';
+import { InputLoadingComponent } from '../input-loading/input-loading.component';
import { InputDirective } from '../input.directive';
import { DateTimePipe } from '../pipes';
import { InputAppearance } from '../types';
-const rF = { required: false };
-
@Component({
- selector: 'pro-input-timepicker',
+ selector: 'pro-input-timepicker[label]',
templateUrl: './input-timepicker.component.html',
- styleUrls: [],
- standalone: true,
imports: [
CommonModule,
DateTimePipe,
@@ -30,11 +26,16 @@ const rF = { required: false };
ReactiveFormsModule,
],
providers: [provideLuxonDateAdapter()],
+ styleUrl: './input-timepicker.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
+ standalone: true,
})
export class InputTimepickerComponent extends InputDirective {
- @Input(rF) public appearance: InputAppearance = 'outline';
- @Input(rF) public interval: string | number | null = null;
- @Input(rF) public max: DateTime | undefined;
- @Input(rF) public min: DateTime | undefined;
+ @Input() public appearance: InputAppearance = 'outline';
+
+ @Input() public interval: string | number | null = null;
+
+ @Input() public max: DateTime | undefined;
+
+ @Input() public min: DateTime | undefined;
}
diff --git a/src/app/public/input-toggle/input-toggle.component.ts b/src/app/public/input-toggle/input-toggle.component.ts
index 7203bea..330d57b 100644
--- a/src/app/public/input-toggle/input-toggle.component.ts
+++ b/src/app/public/input-toggle/input-toggle.component.ts
@@ -17,10 +17,8 @@ import { InputDirective } from '../input.directive';
@UntilDestroy()
@Component({
- selector: 'pro-input-toggle',
+ selector: 'pro-input-toggle[label]',
templateUrl: './input-toggle.component.html',
- styleUrls: ['./input-toggle.component.scss'],
- standalone: true,
imports: [
CommonModule,
FormsModule,
@@ -28,7 +26,9 @@ import { InputDirective } from '../input.directive';
MatSlideToggleModule,
ReactiveFormsModule,
],
+ styleUrl: './input-toggle.component.scss',
changeDetection: ChangeDetectionStrategy.Default,
+ standalone: true,
})
export class InputToggleComponent
extends InputDirective
diff --git a/src/app/public/input.directive.ts b/src/app/public/input.directive.ts
index 5d79ef6..79c259c 100644
--- a/src/app/public/input.directive.ts
+++ b/src/app/public/input.directive.ts
@@ -1,7 +1,9 @@
import {
Directive,
ElementRef,
+ HostBinding,
Input,
+ booleanAttribute,
forwardRef,
inject,
} from '@angular/core';
@@ -14,7 +16,6 @@ import {
} from '@angular/forms';
let id = 0;
-const rF = { required: false };
@Directive({
providers: [
@@ -33,6 +34,7 @@ export abstract class InputDirective implements ControlValueAccessor {
}
public elementRef = inject(ElementRef);
+
public ngControl = inject(NgControl, { self: true, optional: true });
public get formControl(): FormControl {
@@ -43,9 +45,16 @@ export abstract class InputDirective implements ControlValueAccessor {
return this.ngControl.control as FormControl;
}
- @Input(rF) public hint: string | number | null = null;
- @Input(rF) public id = `pro-input-${++id}`;
- @Input(rF) public placeholder: string | null = null;
+ @HostBinding('class.hide-hint')
+ @Input({ transform: booleanAttribute })
+ public hideHint = false;
+
+ @Input() public hint: string | number | null = null;
+
+ @Input() public id = `pro-input-${++id}`;
+
+ @Input() public placeholder: string | null = null;
+
@Input({ required: true }) public label!: string;
public get isRequired(): boolean {
@@ -53,7 +62,9 @@ export abstract class InputDirective implements ControlValueAccessor {
}
public onChange: (value: T) => void = () => undefined;
+
public onTouched: () => void = () => undefined;
+
public writeValue: (value: T) => void = () => undefined;
public registerOnChange(fn: (value: T) => void): void {
@@ -64,7 +75,7 @@ export abstract class InputDirective implements ControlValueAccessor {
this.onTouched = fn;
}
- public scrollIntoView(options?: ScrollIntoViewOptions): void {
+ public scrollIntoView(options: ScrollIntoViewOptions | undefined): void {
const defaultOptions: ScrollIntoViewOptions = { behavior: 'smooth' };
this.elementRef.nativeElement.scrollIntoView({
...defaultOptions,
diff --git a/src/app/public/input/input.component.html b/src/app/public/input/input.component.html
index 2900e45..ecaf2b5 100644
--- a/src/app/public/input/input.component.html
+++ b/src/app/public/input/input.component.html
@@ -11,7 +11,9 @@
[type]="type"
matInput
/>
- {{ hint }}
+ @if (!hideHint) {
+ {{ hint }}
+ }
@if (formControl.invalid && formControl.touched) {
@if (formControl.hasError('required')) {
diff --git a/src/app/public/input/input.component.scss b/src/app/public/input/input.component.scss
new file mode 100644
index 0000000..22a19db
--- /dev/null
+++ b/src/app/public/input/input.component.scss
@@ -0,0 +1,4 @@
+::ng-deep pro-input.hide-hint .mat-mdc-form-field-subscript-wrapper {
+ display: none;
+ visibility: hidden;
+}
diff --git a/src/app/public/input/input.component.ts b/src/app/public/input/input.component.ts
index 167f7ed..0851370 100644
--- a/src/app/public/input/input.component.ts
+++ b/src/app/public/input/input.component.ts
@@ -1,19 +1,21 @@
import { CommonModule } from '@angular/common';
-import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ Input,
+ numberAttribute,
+} from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
-import { InputLoadingComponent } from '../input-loading/loading-input.component';
+import { InputLoadingComponent } from '../input-loading/input-loading.component';
import { InputDirective } from '../input.directive';
import { InputAppearance, InputAutocomplete, InputType } from '../types';
-const rF = { required: false };
-
@Component({
- selector: 'pro-input',
+ selector: 'pro-input[label]',
templateUrl: './input.component.html',
- standalone: true,
imports: [
CommonModule,
InputLoadingComponent,
@@ -21,114 +23,31 @@ const rF = { required: false };
MatInputModule,
ReactiveFormsModule,
],
+ styleUrl: './input.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
+ standalone: true,
})
export class InputComponent extends InputDirective {
- @Input(rF) public appearance: InputAppearance = 'outline';
- @Input(rF) public autocomplete: InputAutocomplete = 'off';
-
- @Input(rF) public set max(value: number | string) {
- this.setMax(value);
- }
- public get max(): number | undefined {
- return this.getMax();
- }
- #max: number | undefined;
-
- @Input(rF) public set min(value: number | string) {
- this.setMin(value);
- }
- public get min(): number | undefined {
- return this.getMin();
- }
- #min: number | undefined;
-
- @Input(rF) public set maxLength(value: number | string) {
- this.setMaxLength(value);
- }
- public get maxLength(): number | undefined {
- return this.#maxLength;
- }
- #maxLength: number | undefined;
-
- @Input(rF) public set minLength(value: number | string) {
- this.setMinLength(value);
- }
- public get minLength(): number | undefined {
- return this.#minLength;
- }
- #minLength: number | undefined;
-
- @Input(rF) public name: string | undefined;
-
- @Input(rF) public set step(value: number | string) {
- const stepParsed = Number(value);
- if (isNaN(stepParsed)) {
- this.#step = undefined;
- return;
- }
-
- this.#step = stepParsed;
- }
- public get step(): number | undefined {
- if (this.type !== 'number') {
- return undefined;
- }
- return this.#step;
- }
- #step: number | undefined;
-
- @Input(rF) public type: InputType = 'text';
+ @Input() public appearance: InputAppearance = 'outline';
- private getMax(): number | undefined {
- if (this.type !== 'number') {
- return undefined;
- }
- return this.#max;
- }
+ @Input() public autocomplete: InputAutocomplete = 'off';
- private getMin(): number | undefined {
- if (this.type !== 'number') {
- return undefined;
- }
- return this.#min;
- }
+ @Input({ transform: numberAttribute })
+ public max: number | string | undefined;
- private setMax(value: number | string): void {
- const maxParsed = Number(value);
- if (isNaN(maxParsed)) {
- this.#max = undefined;
- return;
- }
- this.#max = maxParsed;
- }
+ @Input({ transform: numberAttribute })
+ public min: number | string | undefined;
- private setMaxLength(value: number | string): void {
- const maxLengthParsed = Number(value);
- if (isNaN(maxLengthParsed)) {
- this.#maxLength = undefined;
- return;
- }
- this.#maxLength = maxLengthParsed;
- }
+ @Input({ transform: numberAttribute })
+ public maxLength: number | string | undefined;
- private setMin(value: number | string): void {
- const minParsed = Number(value);
- if (isNaN(minParsed)) {
- this.#min = undefined;
- return;
- }
+ @Input({ transform: numberAttribute })
+ public minLength: number | string | undefined;
- this.#min = minParsed;
- }
+ @Input() public name: string | undefined;
- private setMinLength(value: number | string): void {
- const minLengthParsed = Number(value);
- if (isNaN(minLengthParsed)) {
- this.#minLength = undefined;
- return;
- }
+ @Input({ transform: numberAttribute })
+ public step: number | string | undefined;
- this.#minLength = minLengthParsed;
- }
+ @Input() public type: InputType = 'text';
}
diff --git a/src/app/public/public.ts b/src/app/public/public.ts
index 1b6b5cb..151c493 100644
--- a/src/app/public/public.ts
+++ b/src/app/public/public.ts
@@ -7,7 +7,7 @@ export { InputDatepickerComponent } from './input-datepicker/input-datepicker.co
export { InputDropdownComponent } from './input-dropdown/input-dropdown.component';
export { InputDropdownOptionComponent } from './input-dropdown/input-dropdown-option.component';
export { InputDropdownOptionGroupComponent } from './input-dropdown/input-dropdown-option-group.component';
-export { InputLoadingComponent } from './input-loading/loading-input.component';
+export { InputLoadingComponent } from './input-loading/input-loading.component';
export { InputRadioComponent } from './input-radio/input-radio.component';
export { InputRadioOptionComponent } from './input-radio/input-radio-option.component';
export { InputTextareaComponent } from './input-textarea/input-textarea.component';
diff --git a/src/app/public/utilities/loading-input.component.ts b/src/app/public/utilities/loading-input.component.ts
deleted file mode 100644
index c75727d..0000000
--- a/src/app/public/utilities/loading-input.component.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
-import { interval } from 'rxjs';
-
-import { Component, Input, OnInit } from '@angular/core';
-import { FormControl, ReactiveFormsModule } from '@angular/forms';
-import { MatFormFieldModule } from '@angular/material/form-field';
-import { MatInputModule } from '@angular/material/input';
-
-import { InputAppearance } from '../types';
-
-@UntilDestroy()
-@Component({
- selector: 'pro-loading-input',
- template: `
-
-
- @if (hint) {
- {{ hint }}
- }
-
- `,
- styles: [
- `
- :host {
- width: 100%;
- }
- `,
- ],
- imports: [MatFormFieldModule, MatInputModule, ReactiveFormsModule],
- standalone: true,
-})
-export class LoadingInputComponent implements OnInit {
- @Input({ required: false }) public appearance: InputAppearance = 'outline';
- @Input({ required: false }) public hint?: string;
- @Input({ required: false }) public label?: string;
-
- protected readonly formControl = new FormControl(null);
- protected loadingText = 'Loading';
-
- public ngOnInit(): void {
- this.formControl.disable();
-
- interval(500)
- .pipe(untilDestroyed(this))
- .subscribe(() => {
- if (this.loadingText === 'Loading') {
- this.loadingText = this.label
- ? `Loading "${this.label}".`
- : 'Loading.';
- } else if (this.loadingText.endsWith('...')) {
- this.loadingText = this.label ? `Loading "${this.label}"` : 'Loading';
- } else {
- this.loadingText = this.loadingText + '.';
- }
- this.formControl.setValue(this.loadingText);
- });
- }
-}
diff --git a/src/index.html b/src/index.html
index 2601b47..21709f1 100644
--- a/src/index.html
+++ b/src/index.html
@@ -6,6 +6,10 @@
+