Skip to content
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

Add fieldsets and tabs to fields API #64

Open
chrisvxd opened this issue Sep 10, 2023 · 5 comments
Open

Add fieldsets and tabs to fields API #64

chrisvxd opened this issue Sep 10, 2023 · 5 comments
Labels
ready Assumed ready enough to start type: feature

Comments

@chrisvxd
Copy link
Member

Currently, we have no way to create groups of fields. #62 adds object support, and spun out this discussion.

Inline fieldsets at the top-level (with headings) are something I've considered as a way to break the form up into separate areas of related fields. It strikes me that the Heading scenario here might also work with an inline Heading fieldset.

Proposals

Option 1

Add renderFields method

Example adding a Style fieldset to Heading

export const Heading: ComponentConfig<HeadingProps> = {
  renderFields: () => {
    return <>
      <Field name="text" />
      <Field name="level" />
      <Fieldset title="Style">
        <Field name="size" />
        <Field name="align" />
        <Field name="padding" />
      </Fieldset>
    </>
  },
  fields: {
    text: { type: "text" },
    size: {
      type: "select",
      options: sizeOptions,
    },
    level: {
      type: "select",
      options: levelOptions,
    },
    align: {
      type: "radio",
      options: [
        { label: "Left", value: "left" },
        { label: "Center", value: "center" },
        { label: "Right", value: "right" },
      ],
    },
    padding: { type: "text" },
  },
}

Option 2

Change fields API or add new fieldsets API

export const Heading: ComponentConfig<HeadingProps> = {
  fieldsets: [
    {
      title: "",
      fields: {
        text: { type: "text" },
        level: {
          type: "select",
          options: levelOptions,
        },
      },
    },
    {
      title: "Style",
      fields: {
        size: {
          type: "select",
          options: sizeOptions,
        },

        align: {
          type: "radio",
          options: [
            { label: "Left", value: "left" },
            { label: "Center", value: "center" },
            { label: "Right", value: "right" },
          ],
        },
        padding: { type: "text" },
      },
    },
  ],

Option 3

Something else; could be some combination of the above or another proposal.


Related #62

@Danm72
Copy link

Danm72 commented Sep 10, 2023

This is the way we currently model our page data, maybe its worth also considering multiple tabs within the right panel e.g content, styles, etc

On the above Option 2 definitely reads better to me. Option 1 sort of feels like a ceremony that can be skipped.
https://github.com/measuredco/puck/assets/1926968/ad9c3e97-512d-43e7-95ef-e729db571b2f

@chrisvxd
Copy link
Member Author

Interesting! I've spoken to @monospaced and I think we leaning the other way towards Option 1 (renderFields).

In terms of ceremony, you could merge the fields and renderFields APIs when using renderFields to reduce repetition.

But this model also means we can support tabs and other compositional UI in the future:

export const Heading: ComponentConfig<HeadingProps> = {
  renderFields: () => {
    return (
      <Tabs>
        <Tabs.Tab label="Main tab">
          <Field name="text" type="text" />
          <Field name="level" type="select" options={levelOptions} />
          <Fieldset title="Style">
            <Field name="size" type="select" options={sizeOptions} />
            <Field name="align" type="radio" options={alignOptions} />
            <Field name="padding" type="text" />
          </Fieldset>
        </Tabs.Tab>

        <Tabs.Tab label="Secondary tab">
          Content here
        </Tabs.Tab>
      </Tabs>
    );
  },
};

@chrisvxd
Copy link
Member Author

Going with renderFields. Marking as ready for dev.

@chrisvxd chrisvxd changed the title RFC: Add fieldset support to fields API Add fieldset support to fields API Sep 13, 2023
@chrisvxd chrisvxd added the ready Assumed ready enough to start label Sep 13, 2023
@chrisvxd chrisvxd changed the title Add fieldset support to fields API Add groups and tabs to fields API Oct 10, 2023
@chrisvxd chrisvxd changed the title Add groups and tabs to fields API Add fieldsets and tabs to fields API Oct 10, 2023
@chrisvxd
Copy link
Member Author

It's been sometime since we decided on option 1, and we may wish to revisit that decision.

@jperasmus
Copy link
Contributor

Throwing my 2 cents into the discussion. Consider the following component config for "field groups" that follows a very similar pattern to how component categories are configured:

export const Heading: ComponentConfig<HeadingProps> = {
  fields: {
    heading: {
      type: "text",
    },
    level: {
      type: "select",
      label: "Level (1-6)",
      options: [
        { label: "Heading 1", value: "1" },
        { label: "Heading 2", value: "2" },
        { label: "Heading 3", value: "3" },
        { label: "Heading 4", value: "4" },
        { label: "Heading 5", value: "5" },
        { label: "Heading 6", value: "6" },
      ],
    },
    textColor: {
      type: "custom",
      render: TextColorField,
    },
    backgroundColor: {
      type: "custom",
      render: BackgroundColorField,
    },
    fontFamily: {
      type: "custom",
      render: FontFamilyField,
    },
    fontSize: {
      type: "custom",
      render: FontSizeField,
    },
    fontStyle: {
      type: "custom",
      render: FontStyleField,
    },
    // ...more fields
  },
  fieldGroups: {
    basics: {
      title: "Basics",
      fields: ["heading", "level"],
    },
    typography: {
      title: "Typography",
      fields: ["fontFamily", "fontSize", "fontStyle"],
    },
    other: {
      title: "Other",
    },
  },
  defaultProps: {},
  render() {},
}

This is backwards compatible since nothing changes within the fields config object. When no fieldGroups object is provided it just renders as-is.

Sidenote: the reason I prefer fieldGroups over fieldset is that fieldset is ambiguous to me because it is an existing HTML element.

Another advantage of this approach is that you can guarantee the order of the fields within a group by using the fieldGroups.{field}.fields array. More modern versions of JavaScript don't seem to have too many inconsistencies, but I've been burned in the past assuming the order would be consistent when using objects when it wasn't. Another option (probably outside of this topic) is to introduce an order prop that could be used to specify the order of each field, fieldGroup, etc within the config.

To take this a step further, the tabs concept can also be implemented with an optional tabs addition to the config.

export const Heading: ComponentConfig<HeadingProps> = {
  fields: {
    heading: {
      type: "text",
    },
    level: {
      type: "select",
      label: "Level (1-6)",
      options: [
        { label: "Heading 1", value: "1" },
        { label: "Heading 2", value: "2" },
        { label: "Heading 3", value: "3" },
        { label: "Heading 4", value: "4" },
        { label: "Heading 5", value: "5" },
        { label: "Heading 6", value: "6" },
      ],
    },
    textColor: {
      type: "custom",
      render: TextColorField,
    },
    backgroundColor: {
      type: "custom",
      render: BackgroundColorField,
    },
    fontFamily: {
      type: "custom",
      render: FontFamilyField,
    },
    fontSize: {
      type: "custom",
      render: FontSizeField,
    },
    fontStyle: {
      type: "custom",
      render: FontStyleField,
    },
    // ...more fields
  },
  fieldGroups: {
    basics: {
      title: "Basics",
      fields: ["heading", "level"],
    },
    typography: {
      title: "Typography",
      fields: ["fontFamily", "fontSize", "fontStyle"],
    },
    other: {
      title: "Other",
    },
  },
+  tabs: {
+    settings: {
+      title: "Settings",
+      groups: ["basics", "typography"],
+    },
+    other: {
+      title: "Other",
+      groups: ["other"],
+    },
+  },
  defaultProps: {},
  render() {},
}

Each of these different configs can be overridden using the overrides API:

  • fieldGroup: Render individual field group - takes group's key, title and rendered children
  • fieldsTab: Render individual tab: takes the tab's key, title and rendered children

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ready Assumed ready enough to start type: feature
Projects
None yet
Development

No branches or pull requests

3 participants