DEV: form-kit

This PR introduces FormKit, a component-based form library designed to simplify form creation and management. This library provides a single `Form` component, various field components, controls, validation mechanisms, and customization options. Additionally, it includes helpers to facilitate testing and writing specifications for forms.

1. **Form Component**:
   - The main component that encapsulates form logic and structure.
   - Yields various utilities like `Field`, `Submit`, `Alert`, etc.

   **Example Usage**:
   ```gjs
   import Form from "discourse/form";

   <template>
     <Form as |form|>
       <form.Field
         @name="username"
         @title="Username"
         @validation="required"
         as |field|
       >
         <field.Input />
       </form.Field>

       <form.Field @name="age" @title="Age" as |field|>
         <field.Input @type="number" />
       </form.Field>

       <form.Submit />
     </Form>
   </template>
   ```

2. **Validation**:
   - Built-in validation rules such as `required`, `number`, `length`, and `url`.
   - Custom validation callbacks for more complex validation logic.

   **Example Usage**:
   ```javascript
   validateUsername(name, value, data, { addError }) {
     if (data.bar / 2 === value) {
       addError(name, "That's not how maths work.");
     }
   }
   ```

   ```hbs
   <form.Field @name="username" @validate={{this.validateUsername}} />
   ```

3. **Customization**:
   - Plugin outlets for extending form functionality.
   - Styling capabilities through propagated attributes.
   - Custom controls with properties provided by `form` and `field`.

   **Example Usage**:
   ```hbs
   <Form class="my-form" as |form|>
     <form.Field class="my-field" as |field|>
       <MyCustomControl id={{field.id}} @onChange={{field.set}} />
     </form.Field>
   </Form>
   ```

4. **Helpers for Testing**:
   - Test assertions for form and field validation.

   **Example usage**:
   ```javascript
   assert.form().hasErrors("the form shows errors");
   assert.form().field("foo").hasValue("bar", "user has set the value");
   ```

   - Helper for interacting with he form

   **Example usage**:
   ```javascript
   await formKit().field("foo").fillIn("bar");
   ```

5. **Page Object for System Specs**:
   - Page objects for interacting with forms in system specs.
   - Methods for submitting forms, checking alerts, and interacting with fields.

   **Example Usage**:
   ```ruby
   form = PageObjects::Components::FormKit.new(".my-form")
   form.submit
   expect(form).to have_an_alert("message")
   ```

   **Field Interactions**:
   ```ruby
   field = form.field("foo")
   expect(field).to have_value("bar")
   field.fill_in("bar")
   ```


6. **Collections handling**:
   - A specific component to handle array of objects

   **Example Usage**:
   ```gjs
    <Form @data={{hash foo=(array (hash bar=1) (hash bar=2))}} as |form|>
      <form.Collection @name="foo" as |collection|>
        <collection.Field @name="bar" @title="Bar" as |field|>
          <field.Input />
        </collection.Field>
      </form.Collection>
    </Form>
   ```
This commit is contained in:
chapoi
2024-07-17 11:59:35 +02:00
committed by GitHub
parent bae492efee
commit 2ca06ba236
141 changed files with 6047 additions and 851 deletions

View File

@ -0,0 +1,250 @@
<h2>Controls</h2>
<StyleguideExample @title="Input">
<Form as |form|>
<form.Field @title="Username" @name="username" as |field|>
<field.Input placeholder="Username" />
</form.Field>
<form.Field @title="Age" @name="age" as |field|>
<field.Input placeholder="Age" @type="number" @format="small" />
</form.Field>
<form.Field @title="Website" @name="website" as |field|>
<field.Input @before="https://" @after=".com" @format="large" />
</form.Field>
<form.Field @title="After" @name="after" as |field|>
<field.Input @after=".com" />
</form.Field>
<form.Field @title="Before" @name="before" as |field|>
<field.Input @before="https://" />
</form.Field>
<form.Field @title="Secret" @name="secret" as |field|>
<field.Password />
</form.Field>
</Form>
</StyleguideExample>
<StyleguideExample @title="Question">
<Form as |form|>
<form.Field @title="Enabled" @name="enabled" as |field|>
<field.Question />
</form.Field>
</Form>
</StyleguideExample>
<StyleguideExample @title="Toggle">
<Form as |form|>
<form.Field @title="Enabled" @name="enabled" as |field|>
<field.Toggle />
</form.Field>
</Form>
</StyleguideExample>
<StyleguideExample @title="Composer">
<Form as |form|>
<form.Field @title="Query" @name="query" as |field|>
<field.Composer />
</form.Field>
</Form>
</StyleguideExample>
<StyleguideExample @title="Code">
<Form as |form|>
<form.Field @title="Query" @name="query" as |field|>
<field.Code />
</form.Field>
</Form>
</StyleguideExample>
<StyleguideExample @title="Textarea">
<Form as |form|>
<form.Field @title="Query" @name="query" as |field|>
<field.Textarea />
</form.Field>
</Form>
</StyleguideExample>
<StyleguideExample @title="Select">
<Form as |form|>
<form.Field @title="Enabled" @name="enabled" as |field|>
<field.Select as |select|>
<select.Option @value="true">Yes</select.Option>
<select.Option @value="false">No</select.Option>
</field.Select>
</form.Field>
</Form>
</StyleguideExample>
<StyleguideExample @title="Checkbox">
<Form as |form|>
<form.Field @title="Contract" @name="contract" as |field|>
<field.Checkbox>Accept the contract</field.Checkbox>
</form.Field>
</Form>
</StyleguideExample>
<StyleguideExample @title="Image">
<Form as |form|>
<form.Field @title="Image" @name="image" as |field|>
<field.Image />
</form.Field>
</Form>
</StyleguideExample>
<StyleguideExample @title="Icon">
<Form as |form|>
<form.Field @title="Icon" @name="icon" as |field|>
<field.Icon />
</form.Field>
</Form>
</StyleguideExample>
<StyleguideExample @title="Menu">
<Form as |form data|>
<form.Field @title="Enabled" @name="enabled" as |field|>
<field.Menu @selection={{data.enabled}} as |menu|>
<menu.Item @value="true">Yes</menu.Item>
<menu.Divider />
<menu.Item @value="false">No</menu.Item>
</field.Menu>
</form.Field>
</Form>
</StyleguideExample>
<StyleguideExample @title="RadioGroup">
<Form as |form|>
<form.Field @title="Enabled" @name="enabled" as |field|>
<field.RadioGroup as |radioGroup|>
<radioGroup.Radio @value="true">Yes</radioGroup.Radio>
<radioGroup.Radio @value="false">No</radioGroup.Radio>
</field.RadioGroup>
</form.Field>
</Form>
</StyleguideExample>
<h2>Layout</h2>
<StyleguideExample @title="Section">
<Form as |form|>
<form.Section @title="Section title">
Content
</form.Section>
</Form>
</StyleguideExample>
<StyleguideExample @title="Alert">
<Form as |form|>
<form.Alert @icon="pencil-alt">
You can edit this form.
</form.Alert>
</Form>
</StyleguideExample>
<StyleguideExample @title="InputGroup">
<Form as |form|>
<form.InputGroup as |inputGroup|>
<inputGroup.Field @title="Username" @name="username" as |field|>
<field.Input />
</inputGroup.Field>
<inputGroup.Field @title="Email" @name="email" as |field|>
<field.Input />
</inputGroup.Field>
</form.InputGroup>
</Form>
</StyleguideExample>
<StyleguideExample @title="Collection">
<Form
@data={{hash foo=(array (hash bar=1 baz=2) (hash bar=3 baz=4))}}
as |form|
>
<form.Button @action={{fn form.addItemToCollection "foo"}} @icon="plus" />
<form.Collection @name="foo" as |collection index|>
<form.Row as |row|>
<row.Col @size={{6}}>
<collection.Field @title="Bar" @name="bar" as |field|>
<field.Input />
</collection.Field>
</row.Col>
<row.Col @size={{4}}>
<collection.Field @title="Baz" @name="baz" as |field|>
<field.Input />
</collection.Field>
</row.Col>
<row.Col @size={{2}}>
<form.Button @action={{fn collection.remove index}} @icon="minus" />
</row.Col>
</form.Row>
</form.Collection>
</Form>
</StyleguideExample>
<StyleguideExample @title="Row/Col">
<Form as |form|>
<form.Row as |row|>
<row.Col @size={{6}}>
<form.Field
@title="Username"
@name="username"
@validation="required"
as |field|
>
<field.Input />
</form.Field>
</row.Col>
<row.Col @size={{4}}>
<form.Field @title="Email" @name="email" as |field|>
<field.Input />
</form.Field>
</row.Col>
<row.Col @size={{2}}>
<form.Submit />
</row.Col>
</form.Row>
</Form>
</StyleguideExample>
<StyleguideExample @title="Multiline">
<Form as |form|>
<form.Row as |row|>
<row.Col @size={{6}}>
<form.Field
@title="Username"
@name="username"
@validation="required"
as |field|
>
<field.Input />
</form.Field>
</row.Col>
<row.Col @size={{6}}>
<form.Field @title="Email" @name="email" as |field|>
<field.Input />
</form.Field>
</row.Col>
<row.Col @size={{12}}>
<form.Field @title="Adress" @name="adress" as |field|>
<field.Input />
</form.Field>
</row.Col>
</form.Row>
</Form>
</StyleguideExample>
<h2>Validation</h2>
<StyleguideExample @title="Input">
<Form @validateOn="change" as |form|>
<form.Field
@title="Username"
@name="username"
@validation="required"
as |field|
>
<field.Input />
</form.Field>
</Form>
</StyleguideExample>

View File

@ -1,108 +0,0 @@
<StyleguideExample @title="text-field">
<TextField @placeholder="Placeholder" />
</StyleguideExample>
<StyleguideExample @title="password">
<PasswordField type="password" placeholder="Placeholder" />
</StyleguideExample>
<StyleguideExample @title="textarea">
<Textarea placeholder="Placeholder" />
</StyleguideExample>
<StyleguideExample @title="inline-form">
<div class="inline-form">
<TextField @placeholder="Placeholder" />
<DButton
@icon="search"
@translatedLabel="Submit"
type="submit"
class="btn-primary"
/>
</div>
</StyleguideExample>
<StyleguideExample @title="inline-form with icon button">
<div class="inline-form">
<TextField @placeholder="Placeholder" />
<DButton @icon="search" type="submit" class="btn-primary" />
</div>
</StyleguideExample>
<StyleguideExample @title="full-width inline-form with single input">
<div class="inline-form full-width">
<TextField @placeholder="Placeholder" />
</div>
</StyleguideExample>
<StyleguideExample @title="full-width inline-form with input and icon button">
<div class="inline-form full-width">
<TextField @placeholder="Placeholder" />
<DButton @icon="search" type="submit" class="btn-primary" />
</div>
</StyleguideExample>
<StyleguideExample
@title="inline-form with <ComboBox>"
@initialValue={{get @dummy "options.0.name"}}
as |value|
>
<div class="inline-form">
<TextField @placeholder="Placeholder" />
<ComboBox
@content={{@dummy.options}}
@value={{value}}
@onChange={{fn (mut value)}}
/>
<DButton
@icon="search"
@translatedLabel="Submit"
type="submit"
class="btn-primary"
/>
</div>
</StyleguideExample>
<StyleguideExample @title="inline-form with <MultiSelect>">
<div class="inline-form">
<TextField />
<MultiSelect @content={{@dummy.options}} @onChange={{@dummyAction}} />
<DButton
@icon="search"
@translatedLabel="Submit"
type="submit"
class="btn-primary"
/>
</div>
</StyleguideExample>
<StyleguideExample @title="inline-form with <MultiSelect> and label">
<div class="inline-form">
<label>Text:</label>
<TextField />
<MultiSelect @content={{@dummy.options}} @onChange={{@dummyAction}} />
<DButton
@icon="search"
@translatedLabel="Submit"
type="submit"
class="btn-primary"
/>
</div>
</StyleguideExample>
<StyleguideExample @title="full-width inline-form with search type input">
<div class="inline-form full-width">
<Input placeholder="Search type input" @type="search" />
</div>
</StyleguideExample>
<StyleguideExample @title="<CategoryNotificationsButton> and regular button">
<div class="inline-form">
<CategoryNotificationsButton
@category={{get @dummy.categories 0}}
@value={{1}}
@onChange={{@dummyAction}}
/>
<DButton @icon="reply" @type="submit" @translatedLabel="Button" />
</div>
</StyleguideExample>

View File

@ -3,7 +3,7 @@ import fontScale from "../components/sections/atoms/01-font-scale";
import buttons from "../components/sections/atoms/02-buttons";
import colors from "../components/sections/atoms/03-colors";
import icons from "../components/sections/atoms/04-icons";
import inputFields from "../components/sections/atoms/05-input-fields";
import forms from "../components/sections/atoms/05-forms";
import spinners from "../components/sections/atoms/06-spinners";
import dateTimeInputs from "../components/sections/atoms/date-time-inputs";
import dropdowns from "../components/sections/atoms/dropdowns";
@ -51,9 +51,9 @@ const SECTIONS = [
{ component: colors, category: "atoms", id: "colors", priority: 3 },
{ component: icons, category: "atoms", id: "icons", priority: 4 },
{
component: inputFields,
component: forms,
category: "atoms",
id: "input-fields",
id: "forms",
priority: 5,
},
{ component: spinners, category: "atoms", id: "spinners", priority: 6 },

View File

@ -30,8 +30,8 @@ en:
icons:
title: "Icons"
full_list: "See the full list of Font Awesome Icons"
input_fields:
title: "Input Fields"
forms:
title: "Forms"
buttons:
title: "Buttons"
dropdowns: