Skip to content

Rework design form functionality #395

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from

Conversation

SalihuDickson
Copy link
Contributor

@SalihuDickson SalihuDickson commented Jan 27, 2025

Description

This PR refactors the functionality of the utils-design-form component, addressing the issues outlined in #392.

Comparing Both Approaches

Currently, building a simple form component follows this approach:

const fields = [
    {
       key: "name",
       label: "Name",
       type: "text",
       fieldOptions: {
         required: true,
         tooltip: "Your name",
       },
    },
];
<ecc-utils-design-form .fields=${fields}></ecc-utils-design-form>;

At first glance, this configuration-driven approach appears straightforward. However, as forms become more complex, the configuration can quickly become bulky and difficult to manage.

Event Handling

For example, since all fields exist within a single component, event handling must be centralized. While this isn't an issue for simple fields, it becomes problematic in more intricate configurations.

const fields = [
    {
       key: "name",
       label: "Name",
       type: "text",
       fieldOptions: {
         required: true,
         tooltip: "Your name",
       },
    },
    {
       key: "address",
       label: "Address",
       type: "group",
       groupOptions: {
         collapsible: true,
         tooltip: "Your address",
       },
       fieldOptions: {
         tooltip: "Group for address",
       },
       arrayOptions: {
         defaultInstances: 1,
         max: 4,
         min: 1,
       },
       children: [
         {
           key: "Details",
           label: "Details",
           type: "array",
           fieldOptions: {
             tooltip: "Details for address",
           },
           arrayOptions: {
             defaultInstances: 0,
             max: 2,
           },
           children: [
             {
               key: "houseNumber",
               label: "House Number",
               type: "text",
               fieldOptions: {
                 required: true,
                 tooltip: "Your house number",
               },
             },
             {
               key: "street",
               label: "Street",
               type: "text",
               fieldOptions: {
                 default: "1601 Harrier Ln",
                 required: false,
                 tooltip: "Your street name",
               },
             },
           ],
         },
       ],
    },
];
<ecc-utils-design-form .fields=${fields} @ecc-utils-change={
  (event) => {
    // handle change event
  }
}></ecc-utils-design-form>;

In this example, fields are deeply nested. To listen for a change event on the first street field, we need a structured key like address.details[0].street to properly track its position within the form. This adds complexity, making it harder for developers to ensure they’re handling the correct event.

The Proposed Solution
To simplify event handling and improve maintainability, this PR introduces a new approach:

<ecc-d-form>
  <ecc-d-form-input
     key="name"
     label="Name"
     required
     tooltip="Enter name"
  ></ecc-d-form-input>

  <ecc-d-form-group type="group" label="Address" key="address">
    <ecc-d-form-group
      required
      type="array"
      label="Details"
      instances="1"
      max="2"
      min="1"
      @ecc-change={
           (event) => {
             // handle change event
           }
         }
    >
      <ecc-d-form-input
         key="houseNumber"
         label="House Number"
         tooltip="Your house number"
      ></ecc-d-form-input>
      <ecc-d-form-input
         required
         key="street"
         label="Street"
      ></ecc-d-form-input>
    </ecc-d-form-group>
  </ecc-d-form-group>
</ecc-d-form>

With this approach:

  • With the exception of Array type fields, events are attached directly to the relevant field.
  • The need for deeply nested key tracking is eliminated.
  • Developers can handle field-specific events more intuitively.

This PR simplifies form management while maintaining flexibility, making it easier to work with complex configurations.

Custom Elements Integration

This Approach Also allows for better custom element integration. By separating the individual components in this way, we can allow more technically inclined users to integrate their own elements and better handle specialized scenarios.

Let's take a look at an informative example.

Currently, our form component comes with a submit button, nothing too fancy, but we also provide some limited options to allow developers style the button. But sometimes, this is not enough for some developers. Considering that, the proposed approach allows developers to replace this button with a custom button of their own, like this;

<ecc-d-form no-submit>
  <ecc-d-form-input
    key="name"
    label="Name"
    required
    tooltip="Enter name"
  ></ecc-d-form-input>

  <ecc-d-form-input
    key="email"
    label="Email"
    tooltip="Enter your email address"
  ></ecc-d-form-input>

  <button type="submit">Submit</button>
</ecc-d-form>

Or for example, I want to use a custom input field in my form, my proposed approach still gives the form component the ability to track custom input fields and include them in the form data. For example;

<ecc-d-form>
  <ecc-d-form-input
    required
    key="email"
    label="Email"
    tooltip="Enter your email address"
  ></ecc-d-form-input>

  <TextField
    required
    name="password"
    ecc-key="password"
    id="outlined-password-input"
    label="Password"
    type="password"
    autoComplete="current-password"
  />
</ecc-d-form>

In this example the form will still track the TextField component with the ecc-key value.

Copy link

changeset-bot bot commented Jan 27, 2025

⚠️ No Changeset found

Latest commit: 190b79f

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Contributor

sourcery-ai bot commented Jan 27, 2025

Reviewer's Guide by Sourcery

This pull request refactors the form component to use a more modular approach. It removes the fields property and instead uses the children of the form component to render the form fields. It also adds support for nested forms and arrays.

Sequence diagram for form submission flow

sequenceDiagram
    participant User
    participant Form as EccUtilsDesignForm
    participant Input as EccUtilsDesignFormInput
    participant Group as EccUtilsDesignFormGroup

    User->>Form: Submit form
    Form->>Form: handleSubmit()
    Form->>Form: Check validity
    alt form is valid
        Form->>Form: Set loading state
        Form-->>User: Show loading state
        Form->>Form: Emit submit event
        Form-->>User: Show success message
    else form is invalid
        Form-->>User: Show validation errors
    end
Loading

Class diagram showing the new form component structure

classDiagram
    class EccUtilsDesignForm {
        -form: object
        -formState: string
        -canSubmit: boolean
        -submitDisabledByUser: boolean
        -errorMessage: string
        -successMessage: string
        -items: Array~Element~
        +firstUpdated()
        +handleSubmit()
        +render()
    }

    class EccUtilsDesignFormInput {
        +label: string
        +key: string
        +type: FormItemType
        +disabled: boolean
        +tooltip: string
        +required: boolean
        +value: any
        -path: string
        +handleValueUpdate()
        +handleFileUpload()
        +render()
    }

    class EccUtilsDesignFormGroup {
        +label: string
        +key: string
        +type: string
        +required: boolean
        +tooltip: string
        +instances: number
        -arrayInstances: Array
        -items: Array~Element~
        +render()
    }

    EccUtilsDesignForm *-- EccUtilsDesignFormInput
    EccUtilsDesignForm *-- EccUtilsDesignFormGroup
Loading

State diagram for form component states

stateDiagram-v2
    [*] --> Idle
    Idle --> Loading: Submit form
    Loading --> Success: Submit successful
    Loading --> Error: Submit failed
    Success --> Idle: Reset
    Error --> Idle: Reset

    state Idle {
        [*] --> Valid
        Valid --> Invalid: Validation failed
        Invalid --> Valid: Validation passed
    }
Loading

File-Level Changes

Change Details Files
Removed the fields property from the form component.
  • Removed the fields property.
  • Removed the logic to render the form based on the fields property.
packages/ecc-utils-design/src/components/form/form.ts
Added support for nested forms and arrays.
  • Added ecc-d-form-input component to render form fields.
  • Added ecc-d-form-group component to render nested forms and arrays.
  • Added logic to handle nested form and array changes.
  • Added logic to handle nested form and array submissions.
packages/ecc-utils-design/src/components/form/form.ts
packages/ecc-utils-design/src/components/form/formInput.ts
packages/ecc-utils-design/src/components/form/formGroup.ts
Refactored the form component to use a more modular approach.
  • Moved the logic to render form fields to the ecc-d-form-input component.
  • Moved the logic to render nested forms and arrays to the ecc-d-form-group component.
  • Refactored the form component to use the children to render the form fields.
packages/ecc-utils-design/src/components/form/form.ts
packages/ecc-utils-design/src/components/form/formInput.ts
packages/ecc-utils-design/src/components/form/formGroup.ts
Added support for TUS file uploads.
  • Added support for TUS file uploads using the @anurag_gupta/tus-js-client library.
  • Added logic to handle TUS file upload progress and errors.
packages/ecc-utils-design/src/components/form/formInput.ts
Added support for form events.
  • Added support for ecc-utils-change event to notify when a form field changes.
  • Added support for ecc-utils-submit event to notify when a form is submitted.
  • Added support for ecc-utils-array-add event to notify when a new array item is added.
  • Added support for ecc-utils-array-delete event to notify when an array item is deleted.
packages/ecc-utils-design/src/components/form/form.ts
packages/ecc-utils-design/src/events/index.ts
packages/ecc-utils-design/src/events/ecc-utils-array-add.ts
packages/ecc-utils-design/src/events/ecc-utils-array-delete.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!
  • Generate a plan of action for an issue: Comment @sourcery-ai plan on
    an issue to generate a plan of action for it.

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

vercel bot commented Jan 27, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
elixir-cloud-components ❌ Failed (Inspect) Apr 16, 2025 3:40pm

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @SalihuDickson - I've reviewed your changes - here's some feedback:

Overall Comments:

  • Consider consolidating duplicate utility functions (e.g. renderInTooltip, formatLabel) into a shared utilities module to improve maintainability
  • Error handling approach varies between components - recommend standardizing error handling and validation patterns across the codebase
Here's what I looked at during the review
  • 🟡 General issues: 3 issues found
  • 🟢 Security: all looks good
  • 🟢 Testing: all looks good
  • 🟢 Complexity: all looks good
  • 🟢 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@@ -83,499 +84,32 @@ export default class EccUtilsDesignForm extends LitElement {
formStyles,
];

@property({ type: Array, reflect: true }) fields: Array<Field> = [];
@state() private form: object = {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Consider initializing canSubmit to false by default and enabling it only after validating form state

The current implementation may allow submission of invalid forms. Consider restoring form validation logic or documenting why immediate submission should be allowed.

Suggested implementation:

  @state() private canSubmit = false;
  @state() private form: object = {};

  private validateForm(): void {
    // Check if all required fields are filled
    const hasEmptyRequired = this.requiredButEmpty.length > 0;

    // Update canSubmit based on validation
    this.canSubmit = !hasEmptyRequired && Object.keys(this.form).length > 0;
  }

  protected updated(changedProperties: Map<string, unknown>): void {
    if (changedProperties.has('form') || changedProperties.has('requiredButEmpty')) {
      this.validateForm();
    }
  }

Note: You may need to:

  1. Adjust the validation logic in validateForm() if there are additional validation requirements specific to your forms
  2. Ensure the updated lifecycle method properly merges with any existing implementation if it already exists

@@ -644,6 +178,7 @@ export default class EccUtilsDesignForm extends LitElement {

private handleSubmit(e: Event) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The error handling in handleSubmit could be improved to provide better feedback

Consider adding more specific error handling and user feedback when form validation fails.

Suggested implementation:

  private handleSubmit(e: Event) {
    e.preventDefault();

    try {
      // Check for required fields
      const emptyRequired = this.items
        .filter((item: any) => item.hasAttribute('required') && !item.value)
        .map((item: any) => item.name || 'Unknown field');

      if (emptyRequired.length > 0) {
        this.formState = "error";
        this.errorMessage = `Please fill in required fields: ${emptyRequired.join(', ')}`;
        this.requiredButEmpty = emptyRequired;
        return;
      }

      // Check for invalid fields
      const invalidFields = this.items
        .filter((item: any) => !item.checkValidity())
        .map((item: any) => item.name || 'Unknown field');

      if (invalidFields.length > 0) {
        this.formState = "error";
        this.errorMessage = `Please correct invalid fields: ${invalidFields.join(', ')}`;
        return;
      }

To fully implement this change, you'll also need to:

  1. Add error message display in the template, likely using sl-alert for errors
  2. Add CSS styles for highlighting invalid fields
  3. Ensure the rest of the handleSubmit method (which I can't see) properly sets formState back to "idle" after successful submission
  4. Consider adding field-level error messages using the sl-input or equivalent component's error states

@@ -1,13 +1,12 @@
# ecc-utils-design

The `@elixir-cloud/design` package is a foundational utility library that powers the ELIXIR Cloud Component's (ECC) ecosystem.
The `@elixir-cloud/design` package is a foundational utility library that powers the ELIXIR Cloud Component's (ECC) ecosystem.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (typo): Typo: "Component's" should be "Components"

The possessive should be plural: "ELIXIR Cloud Components".

Suggested change
The `@elixir-cloud/design` package is a foundational utility library that powers the ELIXIR Cloud Component's (ECC) ecosystem.
The `@elixir-cloud/design` package is a foundational utility library that powers the ELIXIR Cloud Components (ECC) ecosystem.

import "@shoelace-style/shoelace/dist/components/tooltip/tooltip.js";

export const getListData = (input: string) => {
if (typeof input !== "string") return input;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Use block braces for ifs, whiles, etc. (use-braces)

Suggested change
if (typeof input !== "string") return input;
if (typeof input !== "string") {


ExplanationIt is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).

}

private findNearestFormGroup(element: HTMLElement | null = this): void {
if (!element) return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Use block braces for ifs, whiles, etc. (use-braces)

Suggested change
if (!element) return;
if (!element) {


ExplanationIt is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).

}

private findNearestFormGroup(element: HTMLElement | null = this): void {
if (!element) return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Use block braces for ifs, whiles, etc. (use-braces)

Suggested change
if (!element) return;
if (!element) {


ExplanationIt is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).

}

const { parentElement } = element;
if (!parentElement) return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Use block braces for ifs, whiles, etc. (use-braces)

Suggested change
if (!parentElement) return;
if (!parentElement) {


ExplanationIt is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).


private renderTemplate(): TemplateResult {
const { type } = this;
if (type === "switch") return this.renderSwitchTemplate();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Use block braces for ifs, whiles, etc. (use-braces)

Suggested change
if (type === "switch") return this.renderSwitchTemplate();
if (type === "switch") {


ExplanationIt is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).

private renderTemplate(): TemplateResult {
const { type } = this;
if (type === "switch") return this.renderSwitchTemplate();
if (type === "file") return this.renderFileTemplate();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Use block braces for ifs, whiles, etc. (use-braces)

Suggested change
if (type === "file") return this.renderFileTemplate();
if (type === "file") {


ExplanationIt is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).

const { type } = this;
if (type === "switch") return this.renderSwitchTemplate();
if (type === "file") return this.renderFileTemplate();
if (type === "select") return this.renderSelectTemplate();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Use block braces for ifs, whiles, etc. (use-braces)

Suggested change
if (type === "select") return this.renderSelectTemplate();
if (type === "select") {


ExplanationIt is recommended to always use braces and create explicit statement blocks.

Using the allowed syntax to just write a single statement can lead to very confusing
situations, especially where subsequently a developer might add another statement
while forgetting to add the braces (meaning that this wouldn't be included in the condition).

@SalihuDickson
Copy link
Contributor Author

@anuragxxd, can you please take a quick look at the demo of the form component in this PR and tell me what you think.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant