1
0
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:
Hinton
2025-02-10 15:26:31 +01:00
parent 1bd8a22c63
commit 5d3428739a
4 changed files with 338 additions and 0 deletions

View 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;
}
}
}

View 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>

View 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() {}
}

View 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 } },
},
};