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",
|
||||||
"focus:tw-ring-offset-2",
|
"focus:tw-ring-offset-2",
|
||||||
"focus:tw-ring-primary-700",
|
"focus:tw-ring-primary-700",
|
||||||
|
"focus:tw-z-10",
|
||||||
this.block ? "tw-w-full tw-block" : "tw-inline-block",
|
this.block ? "tw-w-full tw-block" : "tw-inline-block",
|
||||||
buttonStyles[this.buttonType ?? "secondary"],
|
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 "./banner";
|
||||||
export * from "./button";
|
export * from "./button";
|
||||||
export * from "./callout";
|
export * from "./callout";
|
||||||
|
export * from "./form-field";
|
||||||
export * from "./menu";
|
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",
|
"module": "es2020",
|
||||||
"lib": ["es2020", "dom"],
|
"lib": ["es2020", "dom"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"jslib-common/*": ["../common/src/*"]
|
"jslib-common/*": ["../common/src/*"],
|
||||||
|
"jslib-angular/*": ["../angular/src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
|
Loading…
Reference in New Issue
Block a user