diff --git a/libs/vault/src/form-builder/builder-field.component.ts b/libs/vault/src/form-builder/builder-field.component.ts new file mode 100644 index 00000000000..5b878bd1edc --- /dev/null +++ b/libs/vault/src/form-builder/builder-field.component.ts @@ -0,0 +1,70 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; +import { AbstractControl, ReactiveFormsModule } from "@angular/forms"; + +import { FormFieldModule, IconButtonModule } from "@bitwarden/components"; + +import { FormConfig } from "./form-builder.component"; + +@Component({ + selector: "vault-field-text", + template: ` + + {{ config.label }} + + + `, + standalone: true, + imports: [ReactiveFormsModule, FormFieldModule], +}) +export class FieldTextComponent { + @Input({ required: true }) config!: FormConfig; + @Input({ required: true }) control!: AbstractControl; + + constructor() {} +} + +@Component({ + selector: "vault-field-password", + template: ` + + {{ config.label }} + + + + `, + standalone: true, + imports: [ReactiveFormsModule, FormFieldModule, IconButtonModule], +}) +export class FieldPasswordComponent { + @Input({ required: true }) config!: FormConfig; + @Input({ required: true }) control!: AbstractControl; + + constructor() {} +} + +@Component({ + selector: "vault-builder-field", + template: ``, + imports: [CommonModule], + standalone: true, +}) +export class BuilderFieldComponent { + @Input({ required: true }) config!: FormConfig; + @Input({ required: true }) control!: AbstractControl; + + constructor() {} + + getComponent() { + switch (this.config.control) { + case "text": + return FieldTextComponent; + case "password": + return FieldPasswordComponent; + default: + return null; + } + } +} diff --git a/libs/vault/src/form-builder/form-builder.component.html b/libs/vault/src/form-builder/form-builder.component.html new file mode 100644 index 00000000000..3373e759bb0 --- /dev/null +++ b/libs/vault/src/form-builder/form-builder.component.html @@ -0,0 +1,17 @@ +
+ @for (section of config; track $index) { + + +

{{ section.label }}

+
+ + @for (input of section.items; track $index) { + + } + +
+ } +
diff --git a/libs/vault/src/form-builder/form-builder.component.ts b/libs/vault/src/form-builder/form-builder.component.ts new file mode 100644 index 00000000000..6e7cdd68a03 --- /dev/null +++ b/libs/vault/src/form-builder/form-builder.component.ts @@ -0,0 +1,57 @@ +import { Component, Input, OnInit } from "@angular/core"; +import { FormGroup, ReactiveFormsModule } from "@angular/forms"; + +import { + CardComponent, + FormFieldModule, + SectionComponent, + SectionHeaderComponent, +} from "@bitwarden/components"; + +import { BuilderFieldComponent } from "./builder-field.component"; + +export type SectionConfig = { + label: string; + items: FormConfig[]; +}; + +export type FormConfig = { + label: string; + control: FormControlTypeType; + property: string; +}; + +const FormControlType = { + text: "text", + password: "password", + email: "email", + number: "number", +} as const; + +export type FormControlTypeType = (typeof FormControlType)[keyof typeof FormControlType]; + +@Component({ + selector: "vault-form-builder", + templateUrl: "form-builder.component.html", + imports: [ + ReactiveFormsModule, + + CardComponent, + FormFieldModule, + SectionComponent, + SectionHeaderComponent, + + BuilderFieldComponent, + ], + standalone: true, +}) +export class FormBuilderComponent implements OnInit { + @Input({ required: true }) config!: SectionConfig[]; + @Input({ required: true }) formGroup!: FormGroup; + + protected controlTypes = FormControlType; + + constructor() {} + + ngOnInit() {} +} diff --git a/libs/vault/src/form-builder/form-builder.stories.ts b/libs/vault/src/form-builder/form-builder.stories.ts new file mode 100644 index 00000000000..fd1a8fc3054 --- /dev/null +++ b/libs/vault/src/form-builder/form-builder.stories.ts @@ -0,0 +1,194 @@ +import { FormControl, FormGroup } from "@angular/forms"; +import { Meta, StoryObj, componentWrapperDecorator, moduleMetadata } from "@storybook/angular"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { I18nMockService } from "@bitwarden/components"; + +import { FormBuilderComponent } from "./form-builder.component"; + +export default { + title: "Vault/Form Builder", + component: FormBuilderComponent, + decorators: [ + moduleMetadata({ + imports: [FormBuilderComponent], + providers: [ + { + provide: I18nService, + useValue: new I18nMockService({ toggleVisibility: "Toggle visibility" }), + }, + ], + }), + componentWrapperDecorator((story) => `
${story}
`), + ], +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + config: [ + { + label: "Login credentials", + items: [ + { + label: "Username", + control: "text", + property: "username", + }, + { + label: "Password", + control: "password", + property: "password", + }, + ], + }, + ], + formGroup: new FormGroup({ + username: new FormControl("Example"), + password: new FormControl("Secret"), + }), + }, + argTypes: { + formGroup: { table: { disable: true } }, + }, +}; + +export const Identity: Story = { + args: { + config: [ + { + label: "Personal details", + items: [ + // title: EncString; + { + label: "First name", + control: "text", + property: "firstName", + }, + { + label: "Middle name", + control: "text", + property: "middleName", + }, + { + label: "Last name", + control: "text", + property: "lastName", + }, + { + label: "Username", + control: "text", + property: "username", + }, + { + label: "Company", + control: "text", + property: "company", + }, + ], + }, + { + label: "Identification", + items: [ + { + label: "Social Security number", + control: "text", + property: "ssn", + }, + { + label: "Passport number", + control: "text", + property: "passportNumber", + }, + { + label: "License number", + control: "text", + property: "licenseNumber", + }, + ], + }, + { + label: "Contact info", + items: [ + { + label: "Email", + control: "text", + property: "email", + }, + { + label: "Phone", + control: "text", + property: "phone", + }, + ], + }, + { + label: "Address", + items: [ + { + label: "Address 1", + control: "text", + property: "address1", + }, + { + label: "Address 2", + control: "text", + property: "address2", + }, + { + label: "Address 3", + control: "text", + property: "address3", + }, + { + label: "City / Town", + control: "text", + property: "city", + }, + { + label: "State / Province", + control: "text", + property: "state", + }, + { + label: "Zip / Postal code", + control: "text", + property: "postalCode", + }, + { + label: "Country", + control: "text", + property: "country", + }, + ], + }, + ], + formGroup: new FormGroup({ + // Personal details + firstName: new FormControl(""), + middleName: new FormControl(""), + lastName: new FormControl(""), + username: new FormControl(""), + company: new FormControl(""), + // Identification + ssn: new FormControl(""), + passportNumber: new FormControl(""), + licenseNumber: new FormControl(""), + // Contact info + email: new FormControl(""), + phone: new FormControl(""), + // Address + address1: new FormControl(""), + address2: new FormControl(""), + address3: new FormControl(""), + city: new FormControl(""), + state: new FormControl(""), + postalCode: new FormControl(""), + country: new FormControl(""), + }), + }, + argTypes: { + formGroup: { table: { disable: true } }, + }, +};