mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-02 23:11:40 +01:00
[CL-23] Forms (#786)
This commit is contained in:
parent
911cf794f4
commit
78d2f957d5
@ -74,6 +74,7 @@ export class ButtonComponent implements OnInit, OnChanges {
|
||||
"focus:tw-ring",
|
||||
"focus:tw-ring-offset-2",
|
||||
"focus:tw-ring-primary-700",
|
||||
"focus:tw-z-10",
|
||||
this.block ? "tw-w-full tw-block" : "tw-inline-block",
|
||||
buttonStyles[this.buttonType ?? "secondary"],
|
||||
];
|
||||
|
39
components/src/form-field/error-summary.component.ts
Normal file
39
components/src/form-field/error-summary.component.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { AbstractControl, FormGroup } from "@angular/forms";
|
||||
|
||||
@Component({
|
||||
selector: "bit-error-summary",
|
||||
template: ` <ng-container *ngIf="errorCount > 0">
|
||||
<i class="bwi bwi-error"></i> {{ "fieldsNeedAttention" | i18n: errorString }}
|
||||
</ng-container>`,
|
||||
host: {
|
||||
class: "tw-block tw-text-danger tw-mt-2",
|
||||
"aria-live": "assertive",
|
||||
},
|
||||
})
|
||||
export class BitErrorSummary {
|
||||
@Input()
|
||||
formGroup: FormGroup;
|
||||
|
||||
get errorCount(): number {
|
||||
return this.getErrorCount(this.formGroup);
|
||||
}
|
||||
|
||||
get errorString() {
|
||||
return this.errorCount.toString();
|
||||
}
|
||||
|
||||
private getErrorCount(form: FormGroup): number {
|
||||
return Object.values(form.controls).reduce((acc: number, control: AbstractControl) => {
|
||||
if (control instanceof FormGroup) {
|
||||
return acc + this.getErrorCount(control);
|
||||
}
|
||||
|
||||
if (control.errors == null) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return acc + Object.keys(control.errors).length;
|
||||
}, 0);
|
||||
}
|
||||
}
|
78
components/src/form-field/error-summary.stories.ts
Normal file
78
components/src/form-field/error-summary.stories.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { InputModule } from "src/input/input.module";
|
||||
import { I18nMockService } from "src/utils/i18n-mock.service";
|
||||
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
|
||||
import { ButtonModule } from "../button";
|
||||
|
||||
import { BitFormFieldComponent } from "./form-field.component";
|
||||
import { FormFieldModule } from "./form-field.module";
|
||||
|
||||
export default {
|
||||
title: "Jslib/Form Error Summary",
|
||||
component: BitFormFieldComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [FormsModule, ReactiveFormsModule, FormFieldModule, InputModule, ButtonModule],
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () => {
|
||||
return new I18nMockService({
|
||||
required: "required",
|
||||
inputRequired: "Input is required.",
|
||||
inputEmail: "Input is not an email-address.",
|
||||
fieldsNeedAttention: "$COUNT$ field(s) above need your attention.",
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
parameters: {
|
||||
design: {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A17689",
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const fb = new FormBuilder();
|
||||
|
||||
const formObj = fb.group({
|
||||
name: ["", [Validators.required]],
|
||||
email: ["", [Validators.required, Validators.email]],
|
||||
});
|
||||
|
||||
function submit() {
|
||||
formObj.markAllAsTouched();
|
||||
}
|
||||
|
||||
const Template: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
|
||||
props: {
|
||||
formObj: formObj,
|
||||
submit: submit,
|
||||
...args,
|
||||
},
|
||||
template: `
|
||||
<form [formGroup]="formObj" (ngSubmit)="submit()">
|
||||
<bit-form-field>
|
||||
<bit-label>Name</bit-label>
|
||||
<input bitInput formControlName="name" />
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>Email</bit-label>
|
||||
<input bitInput formControlName="email" />
|
||||
</bit-form-field>
|
||||
|
||||
<button type="submit" bit-button buttonType="primary">Submit</button>
|
||||
<bit-error-summary [formGroup]="formObj"></bit-error-summary>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.props = {};
|
38
components/src/form-field/error.component.ts
Normal file
38
components/src/form-field/error.component.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { Component, HostBinding, Input } from "@angular/core";
|
||||
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
|
||||
// Increments for each instance of this component
|
||||
let nextId = 0;
|
||||
|
||||
@Component({
|
||||
selector: "bit-error",
|
||||
template: `<i class="bwi bwi-error"></i> {{ displayError }}`,
|
||||
host: {
|
||||
class: "tw-block tw-mt-1 tw-text-danger",
|
||||
"aria-live": "assertive",
|
||||
},
|
||||
})
|
||||
export class BitErrorComponent {
|
||||
@HostBinding() id = `bit-error-${nextId++}`;
|
||||
|
||||
@Input() error: [string, any];
|
||||
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
get displayError() {
|
||||
switch (this.error[0]) {
|
||||
case "required":
|
||||
return this.i18nService.t("inputRequired");
|
||||
case "email":
|
||||
return this.i18nService.t("inputEmail");
|
||||
default:
|
||||
// Attempt to show a custom error message.
|
||||
if (this.error[1]?.message) {
|
||||
return this.error[1]?.message;
|
||||
}
|
||||
|
||||
return this.error;
|
||||
}
|
||||
}
|
||||
}
|
17
components/src/form-field/form-field.component.html
Normal file
17
components/src/form-field/form-field.component.html
Normal file
@ -0,0 +1,17 @@
|
||||
<label class="tw-block tw-font-semibold tw-mb-1 tw-text-main" [attr.for]="input.id">
|
||||
<ng-content select="bit-label"></ng-content>
|
||||
<span *ngIf="input.required" class="tw-text-xs tw-font-normal"> ({{ "required" | i18n }})</span>
|
||||
</label>
|
||||
<div class="tw-flex">
|
||||
<div *ngIf="prefixChildren.length" class="tw-flex">
|
||||
<ng-content select="[bitPrefix]"></ng-content>
|
||||
</div>
|
||||
<ng-content></ng-content>
|
||||
<div *ngIf="prefixChildren.length" class="tw-flex">
|
||||
<ng-content select="[bitSuffix]"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container [ngSwitch]="input.hasError">
|
||||
<ng-content select="bit-hint" *ngSwitchCase="false"></ng-content>
|
||||
<bit-error [error]="input.error" *ngSwitchCase="true"></bit-error>
|
||||
</ng-container>
|
53
components/src/form-field/form-field.component.ts
Normal file
53
components/src/form-field/form-field.component.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import {
|
||||
AfterContentChecked,
|
||||
Component,
|
||||
ContentChild,
|
||||
ContentChildren,
|
||||
QueryList,
|
||||
ViewChild,
|
||||
} from "@angular/core";
|
||||
|
||||
import { BitInputDirective } from "../input/input.directive";
|
||||
|
||||
import { BitErrorComponent } from "./error.component";
|
||||
import { BitHintComponent } from "./hint.component";
|
||||
import { BitPrefixDirective } from "./prefix.directive";
|
||||
import { BitSuffixDirective } from "./suffix.directive";
|
||||
|
||||
@Component({
|
||||
selector: "bit-form-field",
|
||||
templateUrl: "./form-field.component.html",
|
||||
host: {
|
||||
class: "tw-mb-6 tw-block",
|
||||
},
|
||||
})
|
||||
export class BitFormFieldComponent implements AfterContentChecked {
|
||||
@ContentChild(BitInputDirective) input: BitInputDirective;
|
||||
@ContentChild(BitHintComponent) hint: BitHintComponent;
|
||||
|
||||
@ViewChild(BitErrorComponent) error: BitErrorComponent;
|
||||
|
||||
@ContentChildren(BitPrefixDirective) prefixChildren: QueryList<BitPrefixDirective>;
|
||||
@ContentChildren(BitSuffixDirective) suffixChildren: QueryList<BitSuffixDirective>;
|
||||
|
||||
ngAfterContentChecked(): void {
|
||||
this.input.hasPrefix = this.prefixChildren.length > 0;
|
||||
this.input.hasSuffix = this.suffixChildren.length > 0;
|
||||
|
||||
this.prefixChildren.forEach((prefix) => {
|
||||
prefix.first = prefix == this.prefixChildren.first;
|
||||
});
|
||||
|
||||
this.suffixChildren.forEach((suffix) => {
|
||||
suffix.last = suffix == this.suffixChildren.last;
|
||||
});
|
||||
|
||||
if (this.error) {
|
||||
this.input.ariaDescribedBy = this.error.id;
|
||||
} else if (this.hint) {
|
||||
this.input.ariaDescribedBy = this.hint.id;
|
||||
} else {
|
||||
this.input.ariaDescribedBy = undefined;
|
||||
}
|
||||
}
|
||||
}
|
54
components/src/form-field/form-field.module.ts
Normal file
54
components/src/form-field/form-field.module.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { NgModule, Pipe, PipeTransform } from "@angular/core";
|
||||
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
|
||||
import { BitInputDirective } from "../input/input.directive";
|
||||
import { InputModule } from "../input/input.module";
|
||||
|
||||
import { BitErrorSummary } from "./error-summary.component";
|
||||
import { BitErrorComponent } from "./error.component";
|
||||
import { BitFormFieldComponent } from "./form-field.component";
|
||||
import { BitHintComponent } from "./hint.component";
|
||||
import { BitLabel } from "./label.directive";
|
||||
import { BitPrefixDirective } from "./prefix.directive";
|
||||
import { BitSuffixDirective } from "./suffix.directive";
|
||||
|
||||
/**
|
||||
* Temporarily duplicate this pipe
|
||||
*/
|
||||
@Pipe({
|
||||
name: "i18n",
|
||||
})
|
||||
export class I18nPipe implements PipeTransform {
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
transform(id: string, p1?: string, p2?: string, p3?: string): string {
|
||||
return this.i18nService.t(id, p1, p2, p3);
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, InputModule],
|
||||
exports: [
|
||||
BitErrorComponent,
|
||||
BitErrorSummary,
|
||||
BitFormFieldComponent,
|
||||
BitHintComponent,
|
||||
BitInputDirective,
|
||||
BitLabel,
|
||||
BitPrefixDirective,
|
||||
BitSuffixDirective,
|
||||
],
|
||||
declarations: [
|
||||
BitErrorComponent,
|
||||
BitErrorSummary,
|
||||
BitFormFieldComponent,
|
||||
BitHintComponent,
|
||||
BitLabel,
|
||||
BitPrefixDirective,
|
||||
BitSuffixDirective,
|
||||
I18nPipe,
|
||||
],
|
||||
})
|
||||
export class FormFieldModule {}
|
212
components/src/form-field/form-field.stories.ts
Normal file
212
components/src/form-field/form-field.stories.ts
Normal file
@ -0,0 +1,212 @@
|
||||
import {
|
||||
AbstractControl,
|
||||
FormBuilder,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
ValidationErrors,
|
||||
ValidatorFn,
|
||||
Validators,
|
||||
} from "@angular/forms";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { InputModule } from "src/input/input.module";
|
||||
import { I18nMockService } from "src/utils/i18n-mock.service";
|
||||
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
|
||||
import { ButtonModule } from "../button";
|
||||
|
||||
import { BitFormFieldComponent } from "./form-field.component";
|
||||
import { FormFieldModule } from "./form-field.module";
|
||||
|
||||
export default {
|
||||
title: "Jslib/Form Field",
|
||||
component: BitFormFieldComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [FormsModule, ReactiveFormsModule, FormFieldModule, InputModule, ButtonModule],
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () => {
|
||||
return new I18nMockService({
|
||||
required: "required",
|
||||
inputRequired: "Input is required.",
|
||||
inputEmail: "Input is not an email-address.",
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
parameters: {
|
||||
design: {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A17689",
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const fb = new FormBuilder();
|
||||
const formObj = fb.group({
|
||||
test: [""],
|
||||
required: ["", [Validators.required]],
|
||||
});
|
||||
|
||||
const defaultFormObj = fb.group({
|
||||
name: ["", [Validators.required]],
|
||||
email: ["", [Validators.required, Validators.email, forbiddenNameValidator(/bit/i)]],
|
||||
});
|
||||
|
||||
// Custom error message, `message` is shown as the error message
|
||||
function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
const forbidden = nameRe.test(control.value);
|
||||
return forbidden ? { forbiddenName: { message: "forbiddenName" } } : null;
|
||||
};
|
||||
}
|
||||
|
||||
function submit() {
|
||||
defaultFormObj.markAllAsTouched();
|
||||
}
|
||||
|
||||
const Template: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
|
||||
props: {
|
||||
formObj: defaultFormObj,
|
||||
submit: submit,
|
||||
...args,
|
||||
},
|
||||
template: `
|
||||
<form [formGroup]="formObj" (ngSubmit)="submit()">
|
||||
<bit-form-field>
|
||||
<bit-label>Name</bit-label>
|
||||
<input bitInput formControlName="name" />
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>Email</bit-label>
|
||||
<input bitInput formControlName="email" />
|
||||
</bit-form-field>
|
||||
|
||||
<button type="submit" bit-button buttonType="primary">Submit</button>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.props = {};
|
||||
|
||||
const RequiredTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
|
||||
props: {
|
||||
formObj: formObj,
|
||||
...args,
|
||||
},
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<input bitInput required placeholder="Placeholder" />
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field [formGroup]="formObj">
|
||||
<bit-label>FormControl</bit-label>
|
||||
<input bitInput formControlName="required" placeholder="Placeholder" />
|
||||
</bit-form-field>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Required = RequiredTemplate.bind({});
|
||||
Required.props = {};
|
||||
|
||||
const HintTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
|
||||
props: {
|
||||
formObj: formObj,
|
||||
...args,
|
||||
},
|
||||
template: `
|
||||
<bit-form-field [formGroup]="formObj">
|
||||
<bit-label>FormControl</bit-label>
|
||||
<input bitInput formControlName="required" placeholder="Placeholder" />
|
||||
<bit-hint>Long hint text</bit-hint>
|
||||
</bit-form-field>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Hint = HintTemplate.bind({});
|
||||
Required.props = {};
|
||||
|
||||
const DisabledTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<input bitInput placeholder="Placeholder" disabled />
|
||||
</bit-form-field>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Disabled = DisabledTemplate.bind({});
|
||||
Disabled.args = {};
|
||||
|
||||
const GroupTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<input bitInput placeholder="Placeholder" />
|
||||
<span bitPrefix>$</span>
|
||||
<span bitSuffix>USD</span>
|
||||
</bit-form-field>
|
||||
`,
|
||||
});
|
||||
|
||||
export const InputGroup = GroupTemplate.bind({});
|
||||
InputGroup.args = {};
|
||||
|
||||
const ButtonGroupTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<input bitInput placeholder="Placeholder" />
|
||||
<button bitPrefix bit-button>Button</button>
|
||||
<button bitPrefix bit-button>Button</button>
|
||||
<button bitSuffix bit-button>
|
||||
<i aria-hidden="true" class="bwi bwi-lg bwi-eye"></i>
|
||||
</button>
|
||||
<button bitSuffix bit-button>
|
||||
<i aria-hidden="true" class="bwi bwi-lg bwi-clone"></i>
|
||||
</button>
|
||||
</bit-form-field>
|
||||
`,
|
||||
});
|
||||
|
||||
export const ButtonInputGroup = ButtonGroupTemplate.bind({});
|
||||
ButtonInputGroup.args = {};
|
||||
|
||||
const SelectTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<select bitInput>
|
||||
<option>Select</option>
|
||||
<option>Other</option>
|
||||
</select>
|
||||
</bit-form-field>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Select = SelectTemplate.bind({});
|
||||
Select.args = {};
|
||||
|
||||
const TextareaTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<bit-label>Textarea</bit-label>
|
||||
<textarea bitInput rows="4"></textarea>
|
||||
</bit-form-field>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Textarea = TextareaTemplate.bind({});
|
||||
Textarea.args = {};
|
14
components/src/form-field/hint.component.ts
Normal file
14
components/src/form-field/hint.component.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Directive, HostBinding } from "@angular/core";
|
||||
|
||||
// Increments for each instance of this component
|
||||
let nextId = 0;
|
||||
|
||||
@Directive({
|
||||
selector: "bit-hint",
|
||||
host: {
|
||||
class: "tw-text-muted tw-inline-block tw-mt-1",
|
||||
},
|
||||
})
|
||||
export class BitHintComponent {
|
||||
@HostBinding() id = `bit-hint-${nextId++}`;
|
||||
}
|
2
components/src/form-field/index.ts
Normal file
2
components/src/form-field/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./form-field.module";
|
||||
export * from "./form-field.component";
|
6
components/src/form-field/label.directive.ts
Normal file
6
components/src/form-field/label.directive.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Directive } from "@angular/core";
|
||||
|
||||
@Directive({
|
||||
selector: "bit-label",
|
||||
})
|
||||
export class BitLabel {}
|
28
components/src/form-field/prefix.directive.ts
Normal file
28
components/src/form-field/prefix.directive.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { Directive, HostBinding, Input } from "@angular/core";
|
||||
|
||||
export const PrefixClasses = [
|
||||
"tw-block",
|
||||
"tw-px-3",
|
||||
"tw-py-1.5",
|
||||
"tw-bg-background-alt",
|
||||
"tw-border",
|
||||
"tw-border-solid",
|
||||
"tw-border-secondary-500",
|
||||
"tw-text-muted",
|
||||
"tw-rounded",
|
||||
];
|
||||
|
||||
@Directive({
|
||||
selector: "[bitPrefix]",
|
||||
})
|
||||
export class BitPrefixDirective {
|
||||
@HostBinding("class") @Input() get classList() {
|
||||
return PrefixClasses.concat([
|
||||
"tw-border-r-0",
|
||||
"tw-rounded-r-none",
|
||||
!this.first ? "tw-rounded-l-none" : "",
|
||||
]).filter((c) => c != "");
|
||||
}
|
||||
|
||||
@Input() first = false;
|
||||
}
|
18
components/src/form-field/suffix.directive.ts
Normal file
18
components/src/form-field/suffix.directive.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Directive, HostBinding, Input } from "@angular/core";
|
||||
|
||||
import { PrefixClasses } from "./prefix.directive";
|
||||
|
||||
@Directive({
|
||||
selector: "[bitSuffix]",
|
||||
})
|
||||
export class BitSuffixDirective {
|
||||
@HostBinding("class") @Input() get classList() {
|
||||
return PrefixClasses.concat([
|
||||
"tw-rounded-l-none",
|
||||
"tw-border-l-0",
|
||||
!this.last ? "tw-rounded-r-none" : "",
|
||||
]).filter((c) => c != "");
|
||||
}
|
||||
|
||||
@Input() last = false;
|
||||
}
|
@ -2,4 +2,5 @@ export * from "./badge";
|
||||
export * from "./banner";
|
||||
export * from "./button";
|
||||
export * from "./callout";
|
||||
export * from "./form-field";
|
||||
export * from "./menu";
|
||||
|
65
components/src/input/input.directive.ts
Normal file
65
components/src/input/input.directive.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { Directive, HostBinding, Input, Optional, Self } from "@angular/core";
|
||||
import { NgControl, Validators } from "@angular/forms";
|
||||
|
||||
// Increments for each instance of this component
|
||||
let nextId = 0;
|
||||
|
||||
@Directive({
|
||||
selector: "input[bitInput], select[bitInput], textarea[bitInput]",
|
||||
})
|
||||
export class BitInputDirective {
|
||||
@HostBinding("class") @Input() get classList() {
|
||||
return [
|
||||
"tw-block",
|
||||
"tw-w-full",
|
||||
"tw-px-3",
|
||||
"tw-py-1.5",
|
||||
"tw-bg-background-alt",
|
||||
"tw-border",
|
||||
"tw-border-solid",
|
||||
"tw-rounded",
|
||||
"tw-text-main",
|
||||
"tw-placeholder-text-muted",
|
||||
"focus:tw-outline-none",
|
||||
"focus:tw-border-primary-700",
|
||||
"focus:tw-ring-1",
|
||||
"focus:tw-ring-primary-700",
|
||||
"focus:tw-z-10",
|
||||
"disabled:tw-bg-secondary-100",
|
||||
this.hasPrefix ? "tw-rounded-l-none" : "",
|
||||
this.hasSuffix ? "tw-rounded-r-none" : "",
|
||||
this.hasError ? "tw-border-danger-500" : "tw-border-secondary-500",
|
||||
].filter((s) => s != "");
|
||||
}
|
||||
|
||||
@HostBinding() id = `bit-input-${nextId++}`;
|
||||
|
||||
@HostBinding("attr.aria-describedby") ariaDescribedBy: string;
|
||||
|
||||
@HostBinding("attr.aria-invalid") get ariaInvalid() {
|
||||
return this.hasError ? true : undefined;
|
||||
}
|
||||
|
||||
@HostBinding()
|
||||
@Input()
|
||||
get required() {
|
||||
return this._required ?? this.ngControl?.control?.hasValidator(Validators.required) ?? false;
|
||||
}
|
||||
set required(value: any) {
|
||||
this._required = value != null && value !== false;
|
||||
}
|
||||
private _required: boolean;
|
||||
|
||||
@Input() hasPrefix = false;
|
||||
@Input() hasSuffix = false;
|
||||
|
||||
get hasError() {
|
||||
return this.ngControl?.status === "INVALID" && this.ngControl?.touched;
|
||||
}
|
||||
|
||||
get error(): [string, any] {
|
||||
const key = Object.keys(this.ngControl.errors)[0];
|
||||
return [key, this.ngControl.errors[key]];
|
||||
}
|
||||
constructor(@Optional() @Self() private ngControl: NgControl) {}
|
||||
}
|
11
components/src/input/input.module.ts
Normal file
11
components/src/input/input.module.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { BitInputDirective } from "./input.directive";
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
declarations: [BitInputDirective],
|
||||
exports: [BitInputDirective],
|
||||
})
|
||||
export class InputModule {}
|
@ -19,7 +19,8 @@
|
||||
"module": "es2020",
|
||||
"lib": ["es2020", "dom"],
|
||||
"paths": {
|
||||
"jslib-common/*": ["../common/src/*"]
|
||||
"jslib-common/*": ["../common/src/*"],
|
||||
"jslib-angular/*": ["../angular/src/*"]
|
||||
}
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
|
Loading…
Reference in New Issue
Block a user