mirror of
https://github.com/bitwarden/browser
synced 2026-02-11 05:53:42 +00:00
Toy around with a form builder concept
This commit is contained in:
70
libs/vault/src/form-builder/builder-field.component.ts
Normal file
70
libs/vault/src/form-builder/builder-field.component.ts
Normal file
@@ -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: `
|
||||
<bit-form-field>
|
||||
<bit-label>{{ config.label }}</bit-label>
|
||||
<input bitInput type="text" [formControl]="control" />
|
||||
</bit-form-field>
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [ReactiveFormsModule, FormFieldModule],
|
||||
})
|
||||
export class FieldTextComponent {
|
||||
@Input({ required: true }) config!: FormConfig;
|
||||
@Input({ required: true }) control!: AbstractControl<any>;
|
||||
|
||||
constructor() {}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "vault-field-password",
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<bit-label>{{ config.label }}</bit-label>
|
||||
<input bitInput type="password" [formControl]="control" />
|
||||
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
||||
</bit-form-field>
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [ReactiveFormsModule, FormFieldModule, IconButtonModule],
|
||||
})
|
||||
export class FieldPasswordComponent {
|
||||
@Input({ required: true }) config!: FormConfig;
|
||||
@Input({ required: true }) control!: AbstractControl<any>;
|
||||
|
||||
constructor() {}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "vault-builder-field",
|
||||
template: `<ng-container
|
||||
*ngComponentOutlet="getComponent(); inputs: { config: config, control: control }"
|
||||
/>`,
|
||||
imports: [CommonModule],
|
||||
standalone: true,
|
||||
})
|
||||
export class BuilderFieldComponent {
|
||||
@Input({ required: true }) config!: FormConfig;
|
||||
@Input({ required: true }) control!: AbstractControl<any>;
|
||||
|
||||
constructor() {}
|
||||
|
||||
getComponent() {
|
||||
switch (this.config.control) {
|
||||
case "text":
|
||||
return FieldTextComponent;
|
||||
case "password":
|
||||
return FieldPasswordComponent;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
17
libs/vault/src/form-builder/form-builder.component.html
Normal file
17
libs/vault/src/form-builder/form-builder.component.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<div [formGroup]="formGroup">
|
||||
@for (section of config; track $index) {
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">{{ section.label }}</h2>
|
||||
</bit-section-header>
|
||||
<bit-card>
|
||||
@for (input of section.items; track $index) {
|
||||
<vault-builder-field
|
||||
[config]="input"
|
||||
[control]="formGroup.get(input.property)"
|
||||
></vault-builder-field>
|
||||
}
|
||||
</bit-card>
|
||||
</bit-section>
|
||||
}
|
||||
</div>
|
||||
57
libs/vault/src/form-builder/form-builder.component.ts
Normal file
57
libs/vault/src/form-builder/form-builder.component.ts
Normal file
@@ -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<any>;
|
||||
|
||||
protected controlTypes = FormControlType;
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnInit() {}
|
||||
}
|
||||
194
libs/vault/src/form-builder/form-builder.stories.ts
Normal file
194
libs/vault/src/form-builder/form-builder.stories.ts
Normal file
@@ -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) => `<div class="tw-bg-background-alt tw-p-3">${story}</div>`),
|
||||
],
|
||||
} as Meta;
|
||||
|
||||
type Story = StoryObj<FormBuilderComponent>;
|
||||
|
||||
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 } },
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user