DEV Community

Cover image for I've open-sourced a form library — define metadata once, and it can be rendered anywhere.
wszgrcy
wszgrcy

Posted on

I've open-sourced a form library — define metadata once, and it can be rendered anywhere.

  • After reviewing all major form libraries on the market, I identified a critical flaw: they require repeated definitions to build a form.
  • For example, consider the following (pseudo-code):
interface Test {
  firstName: string;
}
Enter fullscreen mode Exit fullscreen mode
const form = useForm<Test>({
  defaultValues: {
    firstName: "default",
  },
  onSubmit: async ({ value }) => {
    console.log(value);
  },
});
Enter fullscreen mode Exit fullscreen mode
<form.Field
  name="firstName"
  //...
/>
Enter fullscreen mode Exit fullscreen mode
  • As we all know, the more times you define something, the higher the chance of introducing bugs.
  • In the above scenario, if we want to rename firstName to name, we’d need to update it in at least three places — significantly increasing code fragility.
  • That’s why I created Piying — a form library that achieves all the above functionality with just one definition.
v.object({ firstName: v.optional(v.string(), "default") });
Enter fullscreen mode Exit fullscreen mode

How Does Piying Achieve This?

  • First, thanks to valibot, the code above is simply a schema definition.
  • Piying implements a traversal engine that collects metadata from the schema.
  • This metadata is then transformed into components and form configurations, enabling full compatibility across any frontend framework.

But What About Layout Flexibility?

  • While the schema definition is fixed, Piying supports dynamic layout manipulation via the layout method.
  • You can move any field into a container schema that supports nesting — meaning you can freely rearrange the UI layout without changing the schema.
v.intersect([
  v.pipe(v.object({}), setAlias("scope1")),
  v.object({
    key1: v.pipe(
      v.object({
        test1: v.pipe(v.optional(v.string(), "value1"), layout({ keyPath: ["#", "@scope1"] })),
      }),
    ),
  }),
]);
Enter fullscreen mode Exit fullscreen mode
  • This effectively decouples definition from visual positioning.
  • Regarding field order in object, refer to the MDN documentation for details on JavaScript’s object property iteration order.

How to Achieve Advanced Layouts?

  • Sometimes, you need more than just field rendering — think labels, validation hints, tooltips, etc.
  • These can be achieved using wrappers:
v.pipe(v.number(), v.title("k2-label"), setWrappers(["label"]));
Enter fullscreen mode Exit fullscreen mode
  • If you want to customize the styling of a group of fields, you can define a custom component directly: > While wrappers can be used for field groups, direct component customization is often more convenient.
v.pipe(
  v.object({
    k1: v.pipe(v.string(), v.title("k1-label"), setWrappers(["label"])),
    k2: v.pipe(v.number(), v.title("k2-label"), v.minValue(10), setWrappers(["label", "validator"])),
  }),
  setComponent("fieldset"),
);
Enter fullscreen mode Exit fullscreen mode

How to Customize Wrappers or Components?

  • The code examples above don’t specify a particular frontend framework — because they’re framework-agnostic.
  • However, wrapper and component definitions are framework-specific.
  • You can find setup instructions for your preferred framework in the Quick Start Guide. Currently supported: React, Vue, and Angular. If you need support for another framework, feel free to open an issue.

Is It Ready for Production?

Project Repository

Contact Me

  • If you have any feedback, suggestions, or questions, feel free to reach out: wszgrcy@gmail.com

Top comments (0)